分享了Kubernetes项目中的大部分编排对象,比如Deployment StatefulSet DaemonSet以及Job,最后介绍了有状态应用的管理方法,还说明了Kubernetes添加自定义API对象和编写自定义控制器的原理和流程

在Kubernetes中 ,管理有状态应用是一个比较复杂的过程,尤其Pod模板编写的时候,总有一种YAML文件中编写程序的感觉,很不舒服

Etcd Operator为例,讲解一下Operator的工作原理和编写方法,Etcd Operator使用方法需要将指定的Operator的代码进行一次Clone到本地

$ git clone https://github.com/coreos/etcd-operator

然后将这个Operator部署在Kubernetes集群中

在部署Operator的Pod之前,先执行一个脚本

$ example/rbac/create_role.sh

这个脚本的作用,就是Etcd Operator创建RBAC规则,这是因为,Etcd Operator需要访问Kubernetes的APIServer来创建对象

上面的脚本为Etcd Operator定义如下的权限

首先,可以对Pod进行创建修改,有创建修改删除Service PVC Deployment Secret等API对象

对CRD对象,有所有权限

对属于etcd.database.coreos.com这个API Groups这个CR的对象,有所有权限

而Etcd Operator本身,就是一个Deployment,这个Deployment对象,就需要绑定上面创建的Role规则,Operator规则的YAML定义如下

apiVersion: extensions/v1beta1

kind: Deployment

metadata:

name: etcd-operator

spec:

replicas: 1

template:

metadata:

labels:

name: etcd-operator

spec:

containers:

– name: etcd-operator

image: quay.io/coreos/etcd-operator:v0.9.2

command:

– etcd-operator

env:

– name: MY_POD_NAMESPACE

valueFrom:

fieldRef:

fieldPath: metadata.namespace

– name: MY_POD_NAME

valueFrom:

fieldRef:

fieldPath: metadata.name

接下来就可以去创建Etcd Operator,直接apply -f即可

而一旦Etcd Operator的pod进入Running状态,就会发现,有一个CRD被自动的创建了出来,如下所示

$ kubectl get pods

NAME                              READY     STATUS      RESTARTS   AGE

etcd-operator-649dbdb5cb-bzfzp    1/1       Running     0          20s

$ kubectl get crd

NAME                                    CREATED AT

etcdclusters.etcd.database.coreos.com   2018-09-18T11:42:55Z

这个kubectl describe命令看到这个细节

$ kubectl describe crd  etcdclusters.etcd.database.coreos.com

Group:   etcd.database.coreos.com

Names:

Kind:       EtcdCluster

List Kind:  EtcdClusterList

Plural:     etcdclusters

Short Names:

etcd

Singular:  etcdcluster

Scope:       Namespaced

Version:     v1beta2

这就声明了其对应的CR是API Group为etcd.database.coreos.com 资源类型为EtcdCluster的API对象

所以,我们利用拉取一个代码,运行一个脚本,运行一个Deployment,创建了一个名为EtcdCluster的资源类型,Etcd Operator本身,就是这个自定义资源对应的自定义控制器

就当Etcd Operator部署好之后,这个Kubernetes中创建一个Etcd集群工作就很简单了,只需要编写一个EtcdCluster的Yaml,然后提交就行了,提交的YAML详情为

apiVersion: “etcd.database.coreos.com/v1beta2”

kind: “EtcdCluster”

metadata:

name: “example-etcd-cluster”

spec:

size: 3

version: “3.2.13”

这个文件中,最主要的定义就是一个size为3

这个yaml文件生效后,就会有三个Etcd的Pod被运行起来了,如下所示

$ kubectl get pods

NAME                            READY     STATUS    RESTARTS   AGE

example-etcd-cluster-dp8nqtjznc   1/1       Running     0          1m

example-etcd-cluster-mbzlg6sd56   1/1       Running     0          2m

example-etcd-cluster-v6v6s6stxd   1/1       Running     0          2m

如果,才能让这个Etcd的集群如此的简单的运行呢?

必然是这个Etcd Operator实现的工作了

在说明Etcd的实现手段之前,我们先说下Etcd本身的集群部署方式

Etcd的静态集群方式

这个静态的集群部署方式,需要在部署的开始,就规划好这个集群的拓补信息,并且能够根据这些节点固定的IP地址来编写对应的执行命令

$ etcd –name infra0 –initial-advertise-peer-urls http://10.0.1.10:2380 \

–listen-peer-urls http://10.0.1.10:2380 \

–initial-cluster-token etcd-cluster-1 \

–initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \

–initial-cluster-state new

$ etcd –name infra1 –initial-advertise-peer-urls http://10.0.1.11:2380 \

–listen-peer-urls http://10.0.1.11:2380 \

–initial-cluster-token etcd-cluster-1 \

–initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \

–initial-cluster-state new

$ etcd –name infra2 –initial-advertise-peer-urls http://10.0.1.12:2380 \

–listen-peer-urls http://10.0.1.12:2380 \

–initial-cluster-token etcd-cluster-1 \

–initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \

–initial-cluster-state new

这样,利用了这三个编写好的固定的Etcd进程,组成了一个三节点的Etcd集群

其中,这些节点中的-initial-cluster参数,非常值得关注,其在启动的时候表明了集群的拓补结构,就是这个节点启动的时候,需要和哪些节点组成集群

比如,infra2节点的-initial-cluster的值,如下所示

–initial-cluster infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380,infra2=http://10.0.1.12:2380 \

这个参数的配置中,我们声明了这些集群中的节点地址和名字,这样这些ETCD就通过这些地址组成集群,来进行通信

而且,一个Etcd的节点,需要通过2380端口进行通信来组成集群

一个集群还需要一个-inital-cluster-token字段,来声明一个该集群独一无二的Token名字

这样一个Etcd集群就自动创建出来了,而我们的Operator,就是将上述的过程自动化,用代码生成组成Etcd的命令,从而启动起来

在编写自定义控制器之前,我们需要先完成EtcdCluster这个CRD的定义,对应的types.go文件如下

// +genclient

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type EtcdCluster struct {

metav1.TypeMeta   `json:”,inline”`

metav1.ObjectMeta `json:”metadata,omitempty”`

Spec              ClusterSpec   `json:”spec”`

Status            ClusterStatus `json:”status”`

}

type ClusterSpec struct {

// Size is the expected size of the etcd cluster.

// The etcd-operator will eventually make the size of the running

// cluster equal to the expected size.

// The vaild range of the size is from 1 to 7.

Size int `json:”size”`

}

在这个EtcdCluster中是一个有Status字段的CRD,不必关心ClusterSpec中的其他字段,只关注Size字段即可

size字段的存在,就说明,可以利用YAML文件中的size的值,来进行集群大小的调整

利用这种扩展方式,Etcd完成了自动化运维的主要的功能

而为了支持这个功能,就不能像是之前那样写死在-initial-cluster参数中,并且固定死了

所以Etcd Operator的实现,选择也是静态集群的搭建手段,但是组建方式,是逐个节点的动态添加的方式

首先,Etcd Operator会创建一个种子节点

然后不断的创建新的Etcd节点,然后逐一的加入到这个集群当中,直到集群的节点数等于size

而这个

这就好比我们之前的mysql集群搭建一样

我们有一个名为-initial-cluster-state的启动参数

这个参数为new的时候,这个节点为种子节点,种子节点再声明一个独一无二的的Token

如果这个参数是existing,这个节点是一个普通节点,Etcd Operator需要将其加入到已有集群里

initial-cluster字段的值如何生成呢?

这就需要种子节点先启动,然后启动后的节点只有自己

-initial-cluster=infra0=http://10.0.1.10:2380

对于接下来要加入的节点,比如infra1的话,集群中就有两个节点了,其-initial-cluster参数的值就应该是:

infra0=http://10.0.1.10:2380,infra1=http:..10.0.1.11:2380

其他节点,都依次类推

这样,就是集群的部署方式

假如我们声明了一个YAML,创建一个Etcd集群,那么这个Etcd集群的Operator,需要先创建一个单节点的种子节点,并进行启动

这个种子节点的地址是10.0.1.10,启动命令如下

$ etcd

–data-dir=/var/etcd/data

–name=infra0

–initial-advertise-peer-urls=http://10.0.1.10:2380

–listen-peer-urls=http://0.0.0.0:2380

–listen-client-urls=http://0.0.0.0:2379

–advertise-client-urls=http://10.0.1.10:2379

–initial-cluster=infra0=http://10.0.1.10:2380

–initial-cluster-state=new

–initial-cluster-token=4b5215fa-5401-4a95-a8c6-892317c9bef8

指定了cluster-state为new,并且,指定了唯一的initial-cluster-token的参数

这个创建种子节点的阶段,被称为Bootstrap

对于剩下的每一个节点,Operator只需要执行两个操作即可,以infra1为例

首先添加一个新成员

etcdctl member add infra1 http://10.0.1.11:2380

我们为这个成员节点生成对应的启动参数.并进行启动

$ etcd

–data-dir=/var/etcd/data

–name=infra1

–initial-advertise-peer-urls=http://10.0.1.11:2380

–listen-peer-urls=http://0.0.0.0:2380

–listen-client-urls=http://0.0.0.0:2379

–advertise-client-urls=http://10.0.1.11:2379

–initial-cluster=infra0=http://10.0.1.10:2380,infra1=http://10.0.1.11:2380

–initial-cluster-state=existing

对于跟随者节点,我们可以看到的是,initial-cluster-state为existing,加入已有集群,并且将先由集群的initial-cluster的值变为了infra0和infra1两个节点的IP地址

依次类推,不断的将infra2等后续成员添加到集群中,直到集群的数目等于用户指定的size

有了这个部署思路,然后讲解Etcd Operator的工作原理,就简单多了

我们的Etcd Operator也是围绕着Informer展开的

基本如下

func (c *Controller) Start() error {

for {

err := c.initResource()

time.Sleep(initRetryWaitTime)

}

c.run()

}

func (c *Controller) run() {

_, informer := cache.NewIndexerInformer(source, &api.EtcdCluster{}, 0, cache.ResourceEventHandlerFuncs{

AddFunc:    c.onAddEtcdClus,

UpdateFunc: c.onUpdateEtcdClus,

DeleteFunc: c.onDeleteEtcdClus,

}, cache.Indexers{})

ctx := context.TODO()

// TODO: use workqueue to avoid blocking

informer.Run(ctx.Done())

}

在声明一个EtcdCluster的第一件事,就是创建对应的CRD,即为之前说到的etcdclusters.etcd.database.cores.com

这样kuberentes就能够认识EtcdCluster对象的Informer了

由于Etcd Operator的时间比较早,所以很多编写方式是不同的,在具体实践的时候,以模板为主

在上面的代码中,有一个注释

use workqueue to avoid blocking

在一开始的Etcd Operator中,并没有使用工作队列来协调Informer和控制循环

在EventHandler部分,不会有什么入队操作,直接是每种事件对应的具体业务逻辑了

不过,在Etcd Operator在业务逻辑上的实现方式,与常规的自定义控制器略有不同,将这一部分的工作原理,提炼为了详细流程

图片

Etcd Operator的输出在于,为每一个EtcdCluster对象,都启动了一个控制循环,并发的响应这些对象的变化,这种做法不仅可以简化Etcd Operator的代码实现,还能提高其的响应速度

当这个YAML文件中第一次被提交Kuberentes之后,Etcd Operator的Informer,就会知道一个新的EtcdCluster对象被创建了出来,EventHandler里的添加事件会被触发

Handler做的操作很简单,Etcd Operator内部创建对应的Cluster对象,比如流程图里的Cluster1

这个Cluster对象,这个Etcd集群在Operator内部的描述,和真实的Etcd集群的生命周期是一致的

这个Cluster的主要工作有两个

一个是用于真正的创建集群

在我们Cluster对象第一次被创建的时候去执行,这个工作,就是我们提到的Bootstrap,创建一个单节点的种子集群,并且维护集群的状态,方便在后面添加节点时候添加对应的信息

种子集群中只有一个节点,这一步会生成一个Etcd的Pod,这个Pod中有一个InitContainer,负责检查的Pod的DNS记录是否正常,正常才会启动Etcd

然后这个Etcd的部分,就是启动命令

种子节点的容器启动命令如下

/usr/local/bin/etcd

–data-dir=/var/etcd/data

–name=example-etcd-cluster-mbzlg6sd56

–initial-advertise-peer-urls=http://example-etcd-cluster-mbzlg6sd56.example-etcd-cluster.default.svc:2380

–listen-peer-urls=http://0.0.0.0:2380

–listen-client-urls=http://0.0.0.0:2379

–advertise-client-urls=http://example-etcd-cluster-mbzlg6sd56.example-etcd-cluster.default.svc:2379

–initial-cluster=example-etcd-cluster-mbzlg6sd56=http://example-etcd-cluster-mbzlg6sd56.example-etcd-cluster.default.svc:2380

–initial-cluster-state=new

–initial-cluster-token=4b5215fa-5401-4a95-a8c6-892317c9bef8

这个启动命令中各个参数的含义,已经介绍过了

在Operator生成上述启动命令时候,Etcd的Pod还没有创建出来,其IP地址也就没有出现,这就意味着每个Cluster对象,都会先创建一个和EtcdCluster同名的Headless Service,然后,Etcd Operator在接下来所有的创建Pod的步骤里,就可以使用Pod的DNS命令来代替IP地址

然后Cluster对象的第二个工作,就是启动这个集群所对应的控制循环

这个控制流程会每过一段时间,就获取到所有正在运行的,属于这个Cluster的Pod数量,这个Etcd的实际状态

然后从用户在EtcdCluster中定义的size,就是Etcd的期望状态

然后就会比较这两者的状态,如果实际的Pod数量不够,控制循环就会执行一个添加成员节点的操作,反之,就执行删除操作

比如addOneMember方法为例

首先是生成一个新节点的Pod的名字,比如:example-etcd-cluster-v6v6s6sss

然后调用Etcd Client,执行之前的etcdctl memeber add example-etcd-cluster-v6v6s6sss

然后将自己的Service和所有已经存在的节点列表,组成新的initial-cluster字段的值

使用这个initial-cluster的值,生成对应的Pod的Etcd容器的启动命令

/usr/local/bin/etcd

–data-dir=/var/etcd/data

–name=example-etcd-cluster-v6v6s6stxd

–initial-advertise-peer-urls=http://example-etcd-cluster-v6v6s6stxd.example-etcd-cluster.default.svc:2380

–listen-peer-urls=http://0.0.0.0:2380

–listen-client-urls=http://0.0.0.0:2379

–advertise-client-urls=http://example-etcd-cluster-v6v6s6stxd.example-etcd-cluster.default.svc:2379

–initial-cluster=example-etcd-cluster-mbzlg6sd56=http://example-etcd-cluster-mbzlg6sd56.example-etcd-cluster.default.svc:2380,example-etcd-cluster-v6v6s6stxd=http://example-etcd-cluster-v6v6s6stxd.example-etcd-cluster.default.svc:2380

–initial-cluster-state=existing

这样,在这个容器启动之后,新的Etcd成员节点被加入了集群中,控制循环会重复这个过程,直到运行中的Pod和EtcdCluster的Size一致

然后不断的循环过程中,不断维护这个对应的size对应的关系,这就是Operator的工作原理

但是,有两个问题,一个是在StatefulSet中,为Pod创建的名字是带有编号的,比如web-1 web-2

但是,为何在Etcd Operator中,使用随机名字就可以了?

这是因为,Etcd Operator在每次添加Etcd节点时候,会先执行etcdctl memeber add <Pod名字>

删除时候,也会执行etcdctl member remove<Pod名字>的操作,更新Etcd内部维护的拓补信息,无需要在集群外部通过编号固定这个拓补信息

第二个问题是,为何没有在EtcdCluster中声明Persisent Volume

难道不会担心Etcd的数据丢失吗

Etcd基于的是Raft协议实现的高可用的Key-Value存储,根据Raft协议的设计原则,只有Etcd集群半数以下节点失效的时候,集群才不能工作,但Etcd Operator可以保证,在循环控制中,不断的进行期望状态和实际状态的调谐,所以保证集群的高可用

在这个Etcd集群有半数以上的节点失效的时候,集群就会丧失数据写入的能力,从而进入不可用的状态,这时候即时再去恢复Pod,Etcd集群也不会再起

有了Operaotr机制之后,上述的Etcd备份操作,是由一个单独的Etcd Backup Operator负责完成的

# 首先,创建etcd-backup-operator

$ kubectl create -f example/etcd-backup-operator/deployment.yaml

# 确认etcd-backup-operator已经在正常运行

$ kubectl get pod

NAME                                    READY     STATUS    RESTARTS   AGE

etcd-backup-operator-1102130733-hhgt7   1/1       Running   0          3s

# 可以看到,Backup Operator会创建一个叫etcdbackups的CRD

$ kubectl get crd

NAME                                    KIND

etcdbackups.etcd.database.coreos.com    CustomResourceDefinition.v1beta1.apiextensions.k8s.io

# 我们这里要使用AWS S3来存储备份,需要将S3的授权信息配置在文件里

$ cat $AWS_DIR/credentials

[default]

aws_access_key_id = XXX

aws_secret_access_key = XXX

$ cat $AWS_DIR/config

[default]

region = <region>

# 然后,将上述授权信息制作成一个Secret

$ kubectl create secret generic aws –from-file=$AWS_DIR/credentials –from-file=$AWS_DIR/config

# 使用上述S3的访问信息,创建一个EtcdBackup对象

$ sed -e ‘s|<full-s3-path>|mybucket/etcd.backup|g’ \

-e ‘s|<aws-secret>|aws|g’ \

-e ‘s|<etcd-cluster-endpoints>|”http://example-etcd-cluster-client:2379″|g’ \

example/etcd-backup-operator/backup_cr.yaml \

| kubectl create -f –

这个EtcdBackup对象,每次一创建,就会为指定的Etcd集群做一个备份,EtcdBackup对象的etcdEtcdpoints的字段,就会指定备份的Etcd的访问地址

所以,在实际的环境中,这个备份操作,编写一个Kubernetes的CronJob进行定时运行

当Etcd集群发生了故障之后,可以创建一个EtcdRestore对象完成恢复操作,也需要启动一个Etcd Restore Operartor

# 创建etcd-restore-operator

$ kubectl create -f example/etcd-restore-operator/deployment.yaml

# 确认它已经正常运行

$ kubectl get pods

NAME                                     READY     STATUS    RESTARTS   AGE

etcd-restore-operator-4203122180-npn3g   1/1       Running   0          7s

# 创建一个EtcdRestore对象,来帮助Etcd Operator恢复数据,记得替换模板里的S3的访问信息

$ sed -e ‘s|<full-s3-path>|mybucket/etcd.backup|g’ \

-e ‘s|<aws-secret>|aws|g’ \

example/etcd-restore-operator/restore_cr.yaml \

| kubectl create -f –

上面例子的EtcdRestore对象,就会指定恢复的集群的名字,和备份数据所在的S3存储的访问信息

当一个EtcdRestore对象成功创建后,Etcd Restoe Operator就会通过上述信息,恢复一个全新的Etcd集群,然后Etcd Operator就会将这个新集群直接接管过来,从而重新进入可用状态

那么在这个文章汇总,以Etcd Operator为例,详细介绍了一个Operator的工作原理和编写过程,可以看到Operator将一个Etcd集群,抽象为了一个具有一定自治能力的整体,这个自治能力不足以解决问题的时候,可以通过两个专门负责备份和恢复的Operator进行修正,这种实现方式,更加贴近Etcd的设计思想

我们在使用Operator的实现过程中,再一次的用到了CRD,可是CRD并不是万能的,很多场景并不适用,还有一些性能瓶颈,有哪些不使用于CRD的场景呢?

发表评论

邮箱地址不会被公开。 必填项已用*标注