21

DockOne微信分享(二四三):如何在Kubernetes中编写自定义控制器

 4 years ago
source link: http://dockone.io/article/9588
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【编者的话】随着云原生技术生态的日趋完善和各大云计算技术厂商提供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 ):

Qbay6rI.png!web

综上所述,如何使用控制器模式,同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

Jbe22eY.png!web

添加自定义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,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK