分享了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的场景呢?