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。

可以将控制器控制循环的实现原理作如下归纳:

  1. Deployment控制器从etcd中获取集群中携带特定标签的Pod数量(Pod的实际数量)
  2. Deployent Yaml文件中描述的Replicas字段的值 (Pod的期望数量)
  3. Deployment比较以上结果,确定是创建新的Pod还是删除老的Pod

像上面Deployment Yaml文件那样,具备以下几个特点的资源对象,就是声明式API对象:

  1. 通过一个定义好的API对象来“声明”期望的资源状态是什么样子
  2. 允许有多个API写端,以PATCH的方式对API对象进行修改,而无需关心原始YAML文件的内容
  3. 基于对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,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。