DockOne微信分享(二四三):如何在Kubernetes中编写自定义控制器
【编者的话】随着云原生技术生态的日趋完善和各大云计算技术厂商提供PaaS平台能力的日臻成熟,创建Kubernetes集群以及在集群上部署应用变得非常容易。尽管Kubernetes Deployment可以实现对应用滚动升级和回滚的管理,但事实上程序的发布流程往往千差万别。在遵循Kubernetes的控制器模型和API编程范式的前提下,从“在Kubernetes中部署代码”晋级到“使用Kubernetes编写代码”是Kubernetes用户进阶的过程。
Kubernetes的控制器模型和声明式API对象
容器的本质是进程,因此容器里PID=1的进程是应用本身,其它的进程都是这个PID=1的进程的子进程。Pod只是一个逻辑概念,Kubernetes真正要处理的还是宿主机的Linux容器和Namespace和Cgroups。因此也可以认为Pod在扮演传统基础设施里“虚拟机”的角色,而容器,则是运行在这个虚拟机里的用户程序。Kubernetes提供一种实现Pod自动伸缩、滚动升级、回滚的机制叫控制器。
Kubernetes中提供很多控制器,如果我们查看pkg/controller目录:
ls **/pgk/controller deployment/ job/ podautoscaler/ cloud/ disruption/ namespace/ replicaset/ serviceaccount/ volume/ cronjob/ garbagecollector/ nodelifecycle/ replication/ statefulset/ daemon/ ...
尽管以上每一个控制器负责不同资源资源的编排工作,但是它们都遵循最基本的控制循环(Control Loop)的原理,本节我将主要介绍Deployment。
首先我们一起来看一个deployment yaml文件:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment labels: app: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.7.9 ports: - containerPort: 80
简单将上述Deployment的作用就是为了确保携带app:nginx标签的Pod数量永远等于spec.replicass指定的数量3。
可以将控制器控制循环的实现原理作如下归纳:
- Deployment控制器从etcd中获取集群中携带特定标签的Pod数量(Pod的实际数量)
- Deployent Yaml文件中描述的Replicas字段的值 (Pod的期望数量)
- Deployment比较以上结果,确定是创建新的Pod还是删除老的Pod
像上面Deployment Yaml文件那样,具备以下几个特点的资源对象,就是声明式API对象:
- 通过一个定义好的API对象来“声明”期望的资源状态是什么样子
- 允许有多个API写端,以PATCH的方式对API对象进行修改,而无需关心原始YAML文件的内容
- 基于对API对象的增删改查在无需外接干预的情况下,完成对“实际状态”和“期望状态”的调谐过程
读到这里想必你已经发现声明式API对象与控制器模型相辅相成,声明式API对象定义出期望的资源状态,控制器模型则通过控制循环(Control Loop)将Kubernetes内部的资源调整为声明式API对象期望的样子。因此可以认为声明式API对象和控制器模型,才是Kubernetes项目编排能力“赖以生存”的核心所在。
声明式API对象的编程范式
API对象的组织方式
API对象在etcd里的完整资源路径是由 Group(API组)、Version(API版本)和Resource(API资源类型)三部分组成。
Kubernetes创建资源对象的流程:
- 首先Kubernetes读取用户提交的yaml文件
- 然后Kubernetes去匹配yaml文件中API对象的组
- 再次Kubernetes去匹配yaml文件中API对象的版本号
- 最后Kubernetes去匹配yaml文件中API对象的资源类型
因此我们需要根据需求先进行自定义资源(CRD – Custom Resource Definition),它将包括API对象组、版本号、资源类型:
apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: myresources.spursyy spec: group: spursy version: v1 names: kind: MyResource plural: myresources scope: Namespaced
在上面的yaml文件中指定group: spursyy和version: v1API的组和版本号信息、也指定了CR资源类型叫做myResource,复数是myResources、同时还声明该资源是Namespaced的对象。
然后我们就可以使用刚才定义的资源对象:
- 资源类型指定为myResource
- 资源组为spursy
- 资源的版本号为v1
apiVersion: spursy/v1 kind: MyResource metadata: name: myresources.spursyy spec: message: hello world someValue: 13
如果只定义资源对象,而不定义相应的控制,资源对象并不能发挥任何效用。接下来我们一起看看如何自定义资源控制器。
自定义控制器的原理
控制器如何与APIServer通信
- Informer是APIServer与Kubernetes相互通信的桥梁,它通过Reflector实现ListAndWatch方法来“获取”和“监听”对象实例的变化
- 每当APIServer接收到创建、更新和删除实例的请求,Refector都会收到“事件通知”,然后将变更的事件推送到先进先出的队列中
- Informer会不断从上一队列中读取增量,然后根据增量事件的类型创建或者更新本地对象的缓存
- Informer会根据事件类型触发事先定义好的ResourceEventHandler(具体为AddFunc、UpdatedFunc和DeleteFunc,分别对应API对象的“添加”、“更新”和“删除”事件)
- 同时每隔一定的时间Informer也会对本地的缓存进行一次强制更新
WorkQueue同步Informer跟控制循环(Control Loop)交互的数据
Controller Loop扮演这Kubernetes控制器的角色,确保期望与实际的运行的状态是一致的
以上工作原理如下图(引用 深入剖析Kubernetes ):
综上所述,如何使用控制器模式,同Kubernetes里API对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。这就是“Kubernetes 编程范式”。
编写自定义控制器
Operator
Operator是由CoreOS开发的,用来扩展Kubernetes API,特定的应用程序控制器,它用来创建、配置和管理复杂的有状态应用,如数据库、缓存和监控系统。接下来我将使用Operator SDK,自定义用来控制Pod数量的特定资源。简而言之就是实现类似Kubernetes ReplicaSet类型的资源。
在使用Operator SDK自定义资源前,我们需要明确两点:
Operator SDK的工作流
- 使用SDK创建一个新的Operator项目
- 通过添加自定义资源(CRD)定义新的资源API
- 指定使用SDK API来watch的资源
- 定义Operator的协调(reconcile)逻辑
- 使用Operator SDK构建并生成Operator部署清单文件
明确第一资源和第二资源
像上述我们即将实现类ReplicaSet自定义资源中,第一资源是ReplicaSet自身(明确指定运行的Docker镜像和ReplicaSet中Pod的数量)、第二资源是运行的Pod。当ReplicaSet中属性发生变化(如自定的Docker镜像,或者指定Pod副本的数量)或者Pod的发生变化(如Pod的实际运行数量减少),Controller控制器通过前文讲的控制循环一旦发现上述变化,就会通过变更Pod中镜像的版本或者伸缩Pod的数量调谐(reconcile)集群中ReplicaSet资源的状态。
实践Operator SDK
安装Operator SDK
可参见官方文档: https://github.com/operator-framework/operator-sdk
生成Go项目框架
operator-sdk new podset-operator
添加自定义API
operator-sdk add api –api-version=app.example.com/v1alpha1 –kind=PodSet
添加自定义控制器
operator-sdk add controller –api-version=app.example.com/v1alpha1 –kind=PodSet
修改*/podset-operator/pkg/apis/app/v1alpha1/podset_types.go文件中的PodSetSpec 和 PodSetStatus
type PodSetSpec struct { Replicas int32 `json:"replicas"` } type PodSetStatus struct { Replicas int32 `json:"replicas"` PodNames []string `json:"podNames"` }
注意:我们一旦对Operator SDK生成的框架做任何修改,都需要执行operator-sdk generate k8s,重新生成相应的pkg/apis/app/v1alpha1/zz_generated.deepcopy.go文件。
最后我们需要实现控制器中自动伸缩的代码
代码修改是在控制器Reconcile的函数中/podset-operator/pkg/controller/podset/podset_controller.go。
我需要明确一下逻辑:
- PodSet或者归属于PodSet的Pod一旦发生变化都会触发reconcile函数
- 无论是增加还是删除Pod,Reconcile函数每次都只能增删一个Pod,然后返回,等待下一次触发Reconcile函数
- 确保归属于PodSet第一资源的Pod使用controllerutil.SetControllerReference()函数,这样当第一资源删除时,系统会自动将相应的Pod删除
以上代码实现可参见: https://github.com/spursy/pods … 23L86
生成部署文件
- 将Operator项目打包成镜像:operator-sdk build spursyy/podset-operator
- 推送到docker hub:docker push spursyy/podset-operator
- 修改operator.yaml文件:sed -i “” ‘s|REPLACE_IMAGE|spursyy/podset-operator|g’ deploy/operator.yaml
部署到集群中
创建service account:create -f deploy/service_account.yaml
为service account做RBAC认证:
kubectl create -f deploy/role.yaml kubectl create -f deploy/role_binding.yaml
部署CRD和Operator文件:
kubectl create -f deploy/crds/app_v1alpha1_podset_crd.yaml kubectl create -f deploy/operator.yaml
最后部署一个3副本的podset:
echo "apiVersion: app.example.com/v1alpha1 kind: PodSet metadata: name: example-podset spec: replicas: 3" | oc create -f -
以上示例可参见: https://github.com/spursy/podset-operator
Q&A
Q:yaml文件如果最小化展示?
A:Kubernetes有很多默认属性的,这需要具体查文档。
Q:Operator本身挂了怎么办?依赖其他系统重新调度吗?
A:Operator本身要是挂了,自定义资源的控制器是不能工作的,这时其他的调度系统也不能代替的。
Q:怎么解决CRD升级的问题,比如更改字段,但是已有服务已经运行?
A:这是Kubernetes的控制器通过获取资源资源的实际运行状态与期望的状态对比,如果不相同则删除老的Pod然后拉起期望的Pod。
Q:假如我想通过CPU负载的预测值改变Pod数量,我如何将预测函数加入自定义控制器,从而改变Pod数量?
A:理论上讲这部分逻辑写到控制器的reconcile函数中是没问题的。但是我觉得更应该从调度策略上解决这个问题,即在调度时通过策略选择合适的节点。
Q:接着上面问题,改变Pod数量时,可否通过函数判断,先使用垂直伸缩在进行水平伸缩,那请问如何实现垂直伸缩?
A:不知你说的垂直伸缩是不是意味给Pod分配更多的资源。如果是可以通过自编控制时定义资源属性,用户可以自定义对应的资源属性实现垂直伸缩。
Q:能否讲解下reflector具体工作原理?
A:大致为下面:1. 通过反射器实现对指定类型对象的监控;2. DeltaFiFo队列,将上一步监控到有变化的对象加入到队列中;3.并将队列缓存到本地,并根据事件类型注册相应的事件;4. 最后将对象pop到work queue供control loop触发上一步注册的事件函数。
Q:如何在一个对象控制器的eventHander中触发另外一个资源controller的Reconcile入队呢?
A:理论上Kubernetes控制器是在监测到资源的创建/更新/删除事件后,会自动去触发reconcile函数。我觉得我们做Kubernetes二次开发首先要遵循Kubernetes的编程规范。
以上内容根据2019年12月26日晚微信群分享内容整理。 分享人 阿布,云原生技术爱好者 。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。