kubernetes 已经成为事实上的容器编排标准,其管理的内容都抽象为资源,常见的资源有 pod、deployment、statefulset、service、job 等。无状态服务很容易通过 Deployment 实现容器化;有状态服务一般通过 statusfulset 方式实现容器化,但有状态服务的管理一般很复杂,在集群初始化、扩缩容、故障处理的时候,需要执行各自不同的操作,这些特殊的操作在 kubernetes 中并没有提供。
CoreOS 提出了 operator 的概念,即通过CRD(Custom Resource Definition) 自定义资源,并编写对应的controller 对 CRD 管理。现在已经有很多 operator,如 etcd-operator、mysql-operator、redis-operator 等。
本文将简要介绍如果创建自定义资源 CRD 及对应的控制器 controller,本文例子中的代码见:https://github.com/9sheng/foobar-operator。
假设资源为 FooBar、Group 为 test.example.com,通过 CustomResourceDefinition 创建 CRD,对应的 yaml 如下,我们也可以在 controller 里通过调用 api 创建 CRD。
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: FooBars.test.example.com
spec:
group: test.example.com
names:
kind: Foobar
listKind: FooBarList
plural: FooBars
singular: FooBar
scope: Namespaced # 为全局变量为 Cluster
version: v1
自定义资源的设置见这里foobar_types.go,主要定义了 FooBar 这个结构,主要有2个部分:
此外还有 FooBarList,这个在 list 资源时候用到。
定义好基本数据结构后,使用工具 code-generator 生成客户端配套代码,主要有clientset、deepcopy、informer,通过这些配套代码,我们可以像使用 client-go 一样处理我们的自定义资源。code-generator 根据 foobar_types.go中的导言注释生成配套代码:
// +genclient # 生成客户端
// +genclient:nonNamespaced # Cluster 范围的资源增加该注释
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object # 生成 deepcopy
生成命令见这里
资源的主要处理逻辑见foobar_controller.go,controller 通过设置 informer 监控资源的新增(AddFunc)、更新(UpdateFunc)、删除(DeleteFunc)三种事件的回调函数监控事件,监控到事件后,controller 并不立即处理相应的对象,而是放到一个队列 workqueue 中,然后由多个 worker 从队列中取出对象进行处理。通过这样的处理方式,有很多好处:
FooBarController 中的 Run
、runWorker
、processNextWorkItem
比较固定,实现自己的 controller 时,只要复制粘贴即可;具体的处理逻辑放在 syncHandler
中,syncHandler
一般通过获取 spec 的数据,通过 k8s api 获取相关资源的状态,进行对比处理,使相关的状态达到一致,因涉及到具体处理逻辑,这里不再赘述。
创建了CRD之后,我们可以提交对应的 CR 到 k8s api-server,但 CR 的内容可以是任意格式,k8s 并不会对 CR 的格式校验,如果某一 CR 的格式错误,在 controller 启动时,WaitForCacheSync
会因对象格式错误不能返回,我们可以通过 CRD 的 validation 属性对 CR 的格式做一个基本的校验,避免这个问题,更新的 CRD 如下:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: FooBars.test.example.com
spec:
group: test.example.com
names:
kind: Foobar
listKind: FooBarList
plural: FooBars
singular: FooBar
scope: Namespaced
version: v1
validation: # 格式校验
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- target
properties:
target:
type: string
type:
type: string
schema 校验规则见这里。k8s 在将数据写入 ectd 之前,会进行 schema 校验,如果不通过,则不写入 etcd,并将错误返回给用户。
syncHandler
返回 error
类型,如果 err 不是 nil,则会重新加入到队列中处理。在处理对象时,有各种各样的错误,需要区分错误的类型,只有需要重复处理的错误才能从 syncHandler
中返回。
默认情况下,如果一个资源被删除了,controller 会收到删除事件,但 worker 去获取资源时,很可能获取不到资源了,为避免这种情况,k8s 提供 finalizer 机制,让 controller 有机会处理删除的资源。如果资源使用了 finalizer,删除资源时,k8s 会标记资源的 DeletionTimestamp 而不立即去删除资源,controller可以通过 foobar.ObjectMeta.GetDeletionTimestamp().IsZero()
判断资源是否是删除状态。
当处理完一个资源时,controller一般会更新资源的状态,一般通过 Update 方法实现,Update 之后 informer 会收到更新状态,controller 再一次处理该资源,这样就进入了死循环。因此 informer 的 Update 回调函数中需要判断资源的状态,如果资源不需要处理,就不要放入队列。
另一方面,我们的资源接口直接面向用户,用户可以直接更新资源的属性,甚至可以把我们记录在资源的状态直接删除掉(如执行了kubectl replace
命令),因为我们在处理 update 事件的时候,需要特别注意以下几点:
带来这种处理复杂性的原因主要是,CRD是直接面向用户的。如果面向用户的 CRD 处理逻辑特别复杂,operator 可以创建内部使用的 CRD,一个 controller 处理用户提交的 CR,创建内部的 CR;另一个 controller 处理内部的 CR,进行实际的业务处理,这样可以降低 operator 的实现逻辑。
kubernetes 1.16 中提供了 GA 版本的 subresource 机制,通过设置 status subresource,在更新资源的时候,可通过 UpdateStatus
方法只更新资源的 status 字段。
Update
更新资源时,资源的 metadata.generation
会增加;而 UpdateStatus
不会更新该字段。
TODO
本文简要介绍了如何使用 CRD,如何编写一个 kubernetes controller,以及实践中的注意事项。