随着云原生技术生态的日趋完善和各大云计算技术厂商提供PASS平台能力的日臻成熟,创建Kubernetes集群以及在集群上部署应用变得非常容易。尽管Kubernetes Deployment可以实现对应用滚动升级和回滚的管理,但事实上程序的发布流程往往千差万别。在遵循Kubernetes的控制器模型和API编程范式的前提下,从“在Kubernetes中部署代码”晋级到“使用Kubernetes编写代码”是Kubernetes用户进阶的过程。
接下来我将从一下三个方面介绍如何编写一个自定义控制器:
- Kubernetes的控制器模型和声明式API对象
- Kubernetes API编程范式
- 如何编写一个自定控制器
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
如果只定义资源对象,而不定义相应的控制,资源对象并不能发挥任何效用。接下来我们一起看看如何自定义资源控制器。
自定义控制器的原理
a. 控制器如何与APIServer通信
- Informer是APIServer与Kubernetes相互通信的桥梁,它通过Reflector实现ListAndWatch方法来“获取”和“监听”对象实例的变化
- 每当APIServer接收到创建、更新和删除实例的请求,Refector都会收到“事件通知”,然后将变更的事件推送到先进先出的队列中
- Informer会不断从上一队列中读取增量,然后根据增量事件的类型创建或者更新本地对象的缓存
- Informer会根据事件类型触发事先定义好的ResourceEventHandler(具体为AddFunc、UpdatedFunc和DeleteFunc,分别对应API对象的“添加”、“更新”和“删除”事件)
- 同时每隔一定的时间Informer也会对本地的缓存进行一次强制更新
b. WorkQueue同步Informer跟控制循环(Control Loop)交互的数据
c. Controller Loop 扮演这Kubernetes控制器的角色,确保期望与实际的运行的状态是一致的
以上工作原理如下图(引用深入剖析Kubernetes):
综上所述,如何使用控制器模式,同 Kubernetes 里 API 对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。这就是“Kubernetes 编程范式”。
编写自定义控制器
Operator
Operator 是由 CoreOS 开发的,用来扩展 Kubernetes API,特定的应用程序控制器,它用来创建、配置和管理复杂的有状态应用,如数据库、缓存和监控系统。接下来我将使用Operator SDK,自定义用来控制Pod数量的特定资源。简而言之就是实现类似Kubernetes ReplicaSet类型的资源。
在使用Operator SDK自定义资源前,我们需要明确两点:
1. Operator SDK的工作流
- 使用 SDK 创建一个新的 Operator 项目
- 通过添加自定义资源(CRD)定义新的资源 API
- 指定使用 SDK API 来 watch 的资源
- 定义 Operator 的协调(reconcile)逻辑
- 使用 Operator SDK 构建并生成 Operator 部署清单文件
2. 明确第一资源和第二资源
像上述我们即将实现类ReplicaSet自定义资源中,第一资源是ReplicaSet自身(明确指定运行的Docker镜像和ReplicaSet中Pod的数量)、第二资源是运行的Pod。当ReplicaSet中属性发生变化(如自定的Docker镜像,或者指定Pod副本的数量)或者Pod的发生变化(如Pod的实际运行数量减少),Controller控制器通过前文讲的控制循环一旦发现上述变化,就会通过变更Pod中镜像的版本或者伸缩Pod的数量调谐(reconcile)集群中ReplicaSet资源的状态。
实践Operator SDK
- 安装Operator SDK
可参见官方文档github.com/operator-fr…
- 生成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删除
以上代码实现可参见github.com/spursy/pods…
- 生成部署文件
- 将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 -
以上示例可参见githubgithub.com/spursy/pods…