Kubernetes Operator #
Kubernetes 是一个高度可扩展的系统,它的扩展点包括 kubectl、APIServer、Kubernetes 资源、Controller 控制器、Schedule 调度器、CNI 网络插件、CSI 存储插件、CRI 容器运行时,虽然它的扩展点这么多,但是一般来说我们接触的比较多的还是 自定义资源,控制器,准入控制,有些还会对 kubectl 和 调度器做一些扩展,其他的大部分使用成熟的开源组件就可以了。Operator 通常使用自定义资源和控制器;准入 Webhook 按需要使用,并不是必需组件。
常见范围: #
- 按需部署应用
- 获取 / 还原应用状态的备份
- 处理应用代码的升级以及相关改动。例如 数据库的 schema 或额外的配置设置
- 发布一个 service,要求不支持 kubernetes API 的应用也能发现它
- 模拟整个活部份集群中的故障来测试其稳定性
- 在没有内部成员选举程序的情况下, 为分布式应用选取首领角色
开发工具: #
- CoreOS 开源的 operator-sdk
- k8s sig 小组维护的 kubebuilder
- 可以在 https://operatorhub.io/ 上找到别人开发的现成的 Operator 进行使用
基础定义 #
Kubernetes API #
在 kubernetes 集群中,所有需要数据存取的组件都需要和 kube-apiserver 组件通信,而集群数据都是保存在 etcd 中。同时,kubernetes 也大量使用了声明式 api 来提高用户开发和使用效率,而其 api 分别由 **Group(API 组)、Version(API 版本)和 Resource(API 资源类型)**组成。如下图所示:
Resource、ResourceType、Controller #
Kubernetes 有以下几个特点:
- Kubernetes 可以类比成一个 “数据库”(数据实际持久存储在etcd中)
- 其 API 就是 “sql语句”
- API 设计采用基于 resource 的 Restful 风格,resource type 是 API 的端点(endpoint)
- 每一类 resource(即 Resource Type)是一张 “表”,资源的 schema 类似“表结构”,spec 则保存具体对象的期望配置
- 每张 “表” 里的一行记录就是一个 resource,即该表对应的 Resource Type 的一个实例(instance)
- Kubernetes 这个 “数据库” 内置了很多 “表”,比如 Pod、Deployment、DaemonSet、ReplicaSet 等
resource type 有两类,一类的 namespace 相关的(namespace-scoped),另外一类则是 namespace 无关,即 cluster 范围(cluster-scoped)的
Kubernetes 的基本调度单元是 Pod。Pod 可以由 ReplicaSet、StatefulSet、DaemonSet、Job 等控制器管理,也可以直接创建。以 ReplicaSet 为例:ReplicaSet 对象记录希望运行几个 Pod,ReplicaSet controller 则是实际检查数量、创建或删除 Pod 的程序。资源对象和控制器程序不是同一个东西,也不是每类资源只对应一个控制器。控制器的运行逻辑如下图所示:
控制器一旦启动,将尝试获得 resource 的当前状态(current state),并与存储在 k8s 中的 resource 的期望状态(desired state,即 spec)做比对,如果不一致,controller 就会调用相应 API 进行调整,尽力使得 current state 与期望状态达成一致。这个达成一致的过程被称为协调(reconciliation)
根据前面我们对 resource type 理解,定义 CRD 相当于建立新 “表”(resource type),一旦 CRD 建立,k8s 会为我们自动生成对应 CRD 的 API endpoint,我们就可以通过 yaml 或 API 来操作这个 “表”。我们可以向 “表” 中 “插入” 数据,即基于 CRD 创建 Custom Resource(CR),这就好比我们创建 Deployment 实例,向 Deployment “表” 中插入数据一样
和原生内置的 resource type 一样,光有存储对象状态的 CR 还不够,原生 resource type 有对应 controller 负责协调(reconciliation)实例的创建、伸缩与删除,CR 也需要这样的 “协调者”,即我们也需要定义一个 controller 来负责监听 CR 状态并管理 CR 创建、伸缩、删除以及保持期望状态(spec)与当前状态(current state)的一致。这个 controller 不再是面向原生 Resource type 的实例,而是面向 CRD 的实例 CR 的 controller
Controller 监听(Watch)资源的实际状态,当其与声明的 Spec 中的预期状态不一致时,就会进行调谐(Reconcile),进行一系列的逻辑使预期和实际的状态一致。kubernetes 中有多种类型的 Controller,如 Deployment、StatefulSet、ReplicaSet 等,它们都有各自的 Controller。 我们也可以自定义 controller,编写实现自己需要的逻辑
有了自定义的操作对象类型(CRD),有了面向操作对象类型实例的 controller,我们将其打包为一个概念:“Operator模式”,operator 模式中的 controller 也被称为 operator,它是在集群中对 CR 进行维护操作的主体
GV GVK GVR #
下面是一个标准的 k8s deployment 的 yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
...
上述指定了 apiVersion: apps/v1,其中包括了 Group(apps)、Version(v1),即 GV。
kind 字段标识了资源类型 Deployment(Kind),和上述集合称为 GVK
Resource 是 API 路径中的资源集合名称,不是 spec 里的字段。Deployment 的 GVK 为 apps/v1 + Deployment,GVR 为 apps/v1 + deployments,例如集合路径为 /apis/apps/v1/namespaces/default/deployments。
Client-go #
Operator 使用 client-go。 如果我们需要对 kubernetes 中的资源进行增删查改等,则需要通过操作 api 接口进行操作,我们不需要自己去调用各种 api 接口来实现,官方有开源的 SDK 来供我们使用,即 client-go。
client-go 提供四种客户端对象来和 apiserver 进行交互:
- RESTClient:这是最基础的客户端对象,仅对 HTTPRequest 进行了封装,实现 RESTFul 风格 API,这个对象的使用并不方便,因为很多参数都要使用者来设置,于是 client-go 基于 RESTClient 又实现了三种新的客户端对象;
- ClientSet:把 Resource 和 Version 也封装成方法了,一个资源是一个客户端,多个资源就对应了多个客户端,所以 ClientSet 就是多个客户端的集合了,内置的 kubernetes.Clientset 面向内置资源;自定义资源也可以生成对应的 typed clientset
- DynamicClient:是一种动态客户端,它可以动态的指定资源的组,版本和资源。因此它可以对任意 K8S 资源进行 RESTful 操作,包括自定义资源。它封装了 RESTClient。所以同样提供 RESTClient 的各种方法。该类型的官方例子。
- DiscoveryClient:用于发现 kubernetes 的 API Server 支持的 Group、Version、Resources 等信息;
List & Watch #
Kubernetes 的 Apiserver 提供了获取资源集合的 API,同时提供了接口对资源状态进行持续监控。
以 pod 示例可以看到 List 和 Watch 的使用:
# 通过 kubectl proxy 启动 apiserver 代理服务器
kubectl proxy --port 8080
# list pod 资源
curl http://localhost:8080/api/v1/namespaces/default/pods
# 添加 watch 参数,以及资源版本号,来 watch pod 资源变化
curl -N "http://localhost:8080/api/v1/namespaces/default/pods?watch=true&resourceVersion=770715"
有三种类型的事件:ADDED,MODIFIED 和 DELETED。
这里列的是三种对象变化事件,Watch 还可能返回 BOOKMARK 和 ERROR。示例中的版本号需要替换为刚刚 List 返回的列表 metadata.resourceVersion;URL 加引号是为了避免 shell 将 & 解释成后台运行符号。
- ADDED 表示创建了新的 Pod
- MODIFIED 表示 Pod 的状态变化
- DELETED 则表示 Pod 被删除。
Informer #
以下按经典 List 后 Watch 的方式介绍;新版本可能使用 WatchList 发送初始对象。缓存只覆盖相应资源类型、命名空间和筛选范围,不是集群全部数据的副本。
获取集群中的资源对象时,集群中可能存在大量资源数据,如果每次都从 apiServer 获取都会占用大量资源,对 apiserver 产生较大压力,网络可能也存在抖动或延迟。为了解决此类问题,client-go 提供了 informer 机制。 Informer 在初始化的时先通过 List 去从 Kubernetes API 中取出资源的全部 object 对象,并同时缓存,然后通过 Watch 的机制去监控资源。
图中分为上下两部分,上半为 Informer 库的组件,下半则为需要编写的自定义 Controller 中的组件。
Reflector(反射器) 定义在 /tools/cache 包内。监视(Watch) Kubernetes API 以获取指定的资源类型 (Kind),当监控的资源发生变化时,触发相应的变更事件,例如Add 事件、Update 事件、Delete 事件,并将其资源对象存放到本地缓存 DeltaFIFO 中
DeltaFIFO: DeltaFIFO 是一个生产者-消费者的队列,生产者是 Reflector,消费者是 Pop 函数,FIFO 是一个先进先出的队列,而 Delta 是一个资源对象存储,它可以保存资源对象的操作类型,例如 Add 操作类型、Update 操作类型、Delete 操作类型、Sync 操作类型等
Indexer: Indexer 是 client-go 用来存储资源对象并自带索引功能的本地存储,Informer 从 DeltaFIFO 中将消费出来的资源对象存储至 Indexer。以此,我们便可从 Indexer 中读取数据,而无需从 apiserver 读取
WorkQueue: Informer 从 DeltaFIFO 消费变化,先更新 Indexer 缓存,再通知 ResourceEventHandler。业务 Handler 通常把对象的 namespace/name 放进 WorkQueue,Worker 取出名称后读取最新状态并执行协调。DeltaFIFO 不会直接把事件 Pop 到业务 WorkQueue。
流程示例:
如果删除一个 pod,Informer 执行流程:
- 首先初始化 Informer,Reflector 采用 K8s HTTP API List/Watch API Server 中指定的资源。Reflector 通过 List 接口获取所有的 Pod 对象
- Reflector 将初始列表和后续变化交给 DeltaFIFO;由 Informer 的消费流程取出并更新 Indexer。Indexer 底层使用 ThreadSafeStore 保存对象和索引。业务通过 Lister 的 List/Get 从本地缓存读取;普通 Clientset 的 Get/List 不会自动改为读缓存。
- 回调 Controller 的 ResourceEventHandler,将资源对象变化通知到应用逻辑。然后在 handler 中对收到的资源对象变化做处理。ResourceEventHandler 处于用户的 Controller 代码中,k8s 推荐的编程范式是将收到的消息放入到一个队列中,然后在一个循环中处理该队列中的消息,执行调谐逻辑,这样可以解耦消息生产者(Informer)和消费者(Controller 调谐逻辑),避免消费者阻塞生产者。
- 此时如果我们删除 Pod1,那么 Reflector 会监听到这个事件,然后将这个事件发送到 DeltaFIFO 中,随后更新到内部缓存的资源数据
- 将这个事件到事件处理器(资源事件处理器)中进行处理
SharedInformer #
如果应用中多个业务逻辑要监控同一资源,可能会编写多个 Informer,产生了冗余请求以及资源占用,于是 kubernetes 对 Informer 进行封装,提供了 SharedInformer 机制,多个 Informer 共享同一个缓存,在同一进程、同一 Factory 和相应监听配置范围内共享监听与缓存,而不是整个集群只请求一次。其中 SharedInformerFactory 中有一个 Informer map,当业务通过 InformerFactory 获取一个 Informer 时,会先从 map 中查找,判断该类型 Informer 是否存在,如果不存在则创建一个新的 Informer,并将其添加到 map 中,然后返回该 Informer。
Leader Election #
实际应用部署时,为了保证高可用,实际会运行多个 Controller 实例。此时需要进行 Leader 选举,被选中的实例执行需要独占的调谐工作;其他副本是否启动缓存、监听资源或提供 Webhook,取决于框架和配置。如果 Leader 实例出现问题,将发起新的一轮 Leader 选举。Kubernetes client-go 提供了 leaderelection 包,通过将 Kubernetes 的 lease 资源作为分布式锁来实现了选主逻辑。
Kubernetes 为 Controller 的 Leader Election 创建一个 Lease 对象,该对象 spec 中的 holderIdentity 是当前的 Leader,一般会使用 Leader 的 pod name 作为 Identity。leaseDurationSeconds 是锁的租赁时间,renewTime 则是上一次的更新时间。参与选举的实例会判断当前是否存在该 Lease 对象,如果不存在,则会创建一个 Lease 对象,并将 holderIdentity 设为自己,成为 Leader 并执行调谐逻辑。其他实例则会定期检测该 Lease 对象,如果发现租赁过期,则会试图将 holderIdentity 设为自己,成为新的 Leader。
Scheme #
Scheme 是 Go 程序内部的类型登记簿,维护 GVK 与 Go 类型的映射,用于编解码和类型转换等。一个 Scheme 可以登记多种资源,不是每种资源都要单独创建一个。
Schema 定义了自定义资源的字段、验证规则、默认值等,确保数据的一致性和合法性。
Scheme、Schema 和 CRD 的区别:
- 功能:Scheme 主要用于类型管理和序列化/反序列化,而 CRD 用于定义和扩展 Kubernetes API。
- 使用场景:Scheme 供 Kubernetes Go 程序使用,并不限于 Kubebuilder;Schema 描述字段类型、校验和默认值;CRD 是 Kubernetes 集群中的资源,用于定义自定义资源的结构和行为。
- 实现方式:Scheme 是通过 Go 代码定义和管理的,而 CRD 是通过 YAML 文件定义并应用到 Kubernetes 集群中的。在 Kubebuilder 项目中:
- Scheme 通常在
pkg/apis/目录下定义,包含了所有自定义资源(CR)的 Go 结构体和相应的类型信息。 - CRD 是通过代码生成工具自动生成的。用户在定义自定义资源的 Go 结构体后,Kubebuilder 会生成相应的 CRD YAML 文件,用户可以将这些文件应用到 Kubernetes 集群中。
- Scheme 通常在
下面保留原 Go 类型与 CRD Schema 的对照示例。通常先定义 Go 类型和校验标记,再由 controller-gen 生成 CRD;这不是把 runtime.Scheme 序列化成 CRD。示例为结构节选,不是可直接编译或应用的完整配置。
// 此处为定义instance的schema
type Instance struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Spec InstanceSpec `json:"spec"`
Status InstanceStatus `json:"status,omitempty"`
}
type InstanceSpec struct {
RegionID string `json:"regionID"`
ZoneID string `json:"zoneID"`
VpcID string `json:"vpcID"`
SubnetID string `json:"subnetID"`
}
生成的 CRD 为:
# 此处为 CRD 结构节选
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
...
spec:
names:
...
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Instance is a specification for a Instance Instance resource
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: Spec defines the desired state of Instance
properties:
regionID:
description: RegionID is the location that the Instance lives in.
type: string
subnetID:
description: SubnetID is the id of VPC subnet
type: string
vpcID:
description: VpcID is the id of user VPC
type: string
zoneID:
description: ZoneID is the id of target available-zone
type: string
required:
- regionID
- subnetID
- vpcID
- zoneID
type: object
status:
...
required:
- metadata
- spec
type: object
served: true
storage: true
subresources: {}
status:
...
Operator 工作方式 #
Operator 通过 Kubernetes API 工作。创建 CRD 后,API Server 提供该资源类型的端点;CR 是通过端点保存的具体对象,控制器则是独立运行、监听并维护这些对象的程序。端点本身不包含控制器。整个操作原理如下图所示:
当 Operator 接收任何信息时,它将采取行动将 Kubernetes 集群或外部系统调整到所需的状态,作为其在自定义 controller 中的和解循环(reconciliation loop)的一部分
调谐幂等性 #
同一个对象可能因为重试、多个事件或控制器重启,被反复处理。因此 Reconcile 应检查当前状态,再做必要的动作。例如“确保 orders-db-0 存在”,应先查询,已存在且符合要求时就不重复创建。这样重复执行也不会不断增加资源,通常称为幂等性。
事件只是提醒“这个对象值得再检查”,不能假定每次回调都对应一个必须执行一次的业务动作。处理到队列任务时,对象可能已经更新或删除。
Spec、Status 与 Finalizer #
spec 保存用户希望的配置,status 保存控制器观察到的结果,例如就绪副本数、当前版本和失败原因。CRD 需要启用相应 status 子资源,代码和 RBAC 也要允许更新 status;写状态还应避免没有变化也不停更新,造成重复协调。
如果 CR 管理的是 Kubernetes 子资源,可以按规则设置 ownerReferences,交给垃圾回收机制处理依赖。如果创建的是云数据库或外部存储桶,则通常需要 finalizer:收到 deletionTimestamp 后先清理外部资源,成功后再移除自己的 finalizer,让对象最终删除。清理动作本身也要能安全重试。Kubebuilder 示例
RBAC 权限 #
本地能运行不代表放进集群就能运行。控制器使用的 ServiceAccount 要有对应资源的 get/list/watch,以及实际创建、更新、删除所需权限;更新 status、维护 finalizers 和参加 Lease 选举也需相应权限。遇到 Forbidden,先确认报错中的身份、资源、命名空间与操作,不要直接授予 cluster-admin。
Kubebuilder #
Kubebuilder 是一个基于 CRD 来构建 Kubernetes API 的框架,可以使用 CRD 来构建 API、Controller 和 Admission Webhook。参考文档:The Kubebuilder Book
deepcopy #
从 Informer/Lister 取得的对象通常由缓存共享,应视为只读。修改前先 DeepCopy,再用 API 客户端写回;修改副本本身不会改变集群对象。
DeepCopy 方法对数据结构进行深拷贝,当你需要在代码中对该一个对象进行修改,而又不希望影响其他使用到该对象的代码时,可以先对对象进行一次 DeepCopy,拿到该对象的一个副本后再进行操作。