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个部分:
FooBarSpec:用户输入的 spec,即期望状态FooBarStatus:controller 处理的状态,记录当前状态此外还有 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 从队列中取出对象进行处理。通过这样的处理方式,有很多好处:
workqueue 提供了滤重、限速等功能,能有效减少 controller 的工作量,一段时间内,可能收到多个事件,但通过过滤之后,我们只需要处理一次;通过 workqueue,简化了并发处理逻辑,从 workqueue 中获取一个对象时,处理结束前没有 forget 该对象,该对象不能再加入到队列中,因此该对象不会被其他线程取到处理;如果处理对象失败,只需要重新将对象丢到队列即可,后面我们有机会再一次处理,大大降低了错误处理逻辑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: stringschema 校验规则见这里。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 事件的时候,需要特别注意以下几点:
如果需要清理资源的旧状态,处理失败时怎么办,如果简单放入队列中,下一次处理时,获取不到老的状态;controller 重启,未处理成功的状态可能丢失;带来这种处理复杂性的原因主要是,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,以及实践中的注意事项。