Skip to main content
  1. Docs/

Kubernetes Operator

·11 mins· ·
Owl Dawn
Author
Owl Dawn
Table of Contents

Kubernetes Operator
#

Kubernetes 是一个高度可扩展的系统,它的扩展点包括 kubectl、APIServer、Kubernetes 资源、Controller 控制器、Schedule 调度器、CNI 网络插件、CSI 存储插件、CRI 容器运行时,虽然它的扩展点这么多,但是一般来说我们接触的比较多的还是 自定义资源,控制器,准入控制,有些还会对 kubectl 和 调度器做一些扩展,其他的大部分使用成熟的开源组件就可以了。而 Operator 就会涉及到 自定义资源,控制器和准入控制。

  • Kubernetes 的 Operator 模式概念允许你在不修改 Kubernetes 自身代码的情况下, 通过为一个或多个自定义资源关联控制器来扩展集群的能力。
  • Operator 是 Kubernetes API 的客户端, 充当自定义资源的控制器。
  • Operator模式 = 操作对象(CRD) + 控制逻辑(controller)

常见范围:
#

  • 按需部署应用
  • 获取 / 还原应用状态的备份
  • 处理应用代码的升级以及相关改动。例如 数据库的 schema 或额外的配置设置
  • 发布一个 service,要求不支持 kubernetes API 的应用也能发现它
  • 模拟整个活部份集群中的故障来测试其稳定性
  • 在没有内部成员选举程序的情况下, 为分布式应用选取首领角色

开发工具:
#

基础定义
#

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)是一张 “表”,Resource Type 的 spec 对应 “表结构” 信息(schema)
  • 每张 “表” 里的一行记录就是一个 resource,即该表对应的 Resource Type 的一个实例(instance)
  • Kubernetes 这个 “数据库” 内置了很多 “表”,比如 Pod、Deployment、DaemonSet、ReplicaSet 等

resource type 有两类,一类的 namespace 相关的(namespace-scoped),另外一类则是 namespace 无关,即 cluster 范围(cluster-scoped)的

Kubernetes 是服务编排和容器调度的平台标准,它的基本调度单元是Pod(也是一个resource type),即一组容器的集合。那么 Pod 又是如何被创建、更新和删除的呢?这就离不开控制器(controller)了。每一类 resource type 都有自己对应的控制器(controller)。以 pod 这个 resource type 为例,它的 controller 为 ReplicasSet 的实例。控制器的运行逻辑如下图所示:

控制器一旦启动,将尝试获得 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

spec 下定义了众多字段资源(即 Resource,是 Kind 的对象标识,存储的是 Kind 的 API 对象的一个集合)即 GVR

Client-go
#

Operator 使用 client-go。 如果我们需要对 kubernetes 中的资源进行增删查改等,则需要通过操作 api 接口进行操作,我们不需要自己去调用各种 api 接口来实现,官方有开源的 SDK 来供我们使用,即 client-go。

client-go 提供四种客户端对象来和 apiserver 进行交互:

  1. RESTClient:这是最基础的客户端对象,仅对 HTTPRequest 进行了封装,实现 RESTFul 风格 API,这个对象的使用并不方便,因为很多参数都要使用者来设置,于是 client-go 基于 RESTClient 又实现了三种新的客户端对象;
  2. ClientSet:把 Resource 和 Version 也封装成方法了,一个资源是一个客户端,多个资源就对应了多个客户端,所以 ClientSet 就是多个客户端的集合了,不过 ClientSet 只能访问内置资源,访问不了自定义资源
  3. DynamicClient:是一种动态客户端,它可以动态的指定资源的组,版本和资源。因此它可以对任意 K8S 资源进行 RESTful 操作,包括自定义资源。它封装了 RESTClient。所以同样提供 RESTClient 的各种方法。该类型的官方例子
  4. 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 http://localhost:8080/api/v1/namespaces/default/pods?watch=true&resourceVersion=770715

有三种类型的事件:ADDED,MODIFIED 和 DELETED。

  • ADDED 表示创建了新的 Pod
  • MODIFIED 表示 Pod 的状态变化
  • DELETED 则表示 Pod 被删除。

Informer
#

获取集群中的资源对象时,集群中可能存在大量资源数据,如果每次都从 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: DeltaIFIFO 收到时间后会先将时间存储在自己的数据结构中,然后直接操作 Store(本地缓存) 中存储的数据,更新完 store 后 DeltaIFIFO 会将该事件 pop 到 WorkQueue 中,Controller 收到 WorkQueue 中的事件会根据对应的类型触发对应的回调函数(这是在控制器代码中创建的队列,用于将对象的分发与处理解耦)

流程示例:

如果删除一个 pod,Informer 执行流程:

  1. 首先初始化 Informer,Reflector 采用 K8s HTTP API List/Watch API Server 中指定的资源。Reflector 通过 List 接口获取所有的 Pod 对象
  2. Reflector 将得到的所有资源列表(即 pod 列表)和后续资源变化加入到一个 FIFO 队列中,并循环从队列中取出资源即 pod 进行处理,将资源对象加入到 Indexer 中。(Indexer 是一个本地缓存,并提供了检索能力,允许基于特定条件查找资源),Indexer 将 pod 加入到内部缓存 ThreadSafeStore 中。如果有人调用 Lister 的 List/Get 方法获取 Pod,那么 Lister 直接从 Store 中去拿数据
  3. 回调 Controller 的 ResourceEventHandler,将资源对象变化通知到应用逻辑。然后在 handler 中对收到的资源对象变化做处理。ResourceEventHandler 处于用户的 Controller 代码中,k8s 推荐的编程范式是将收到的消息放入到一个队列中,然后在一个循环中处理该队列中的消息,执行调谐逻辑,这样可以解耦消息生产者(Informer)和消费者(Controller 调谐逻辑),避免消费者阻塞生产者。
  4. 此时如果我们删除 Pod1,那么 Reflector 会监听到这个事件,然后将这个事件发送到 DeltaFIFO 中,随后更新到内部缓存的资源数据
  5. 将这个事件到事件处理器(资源事件处理器)中进行处理

SharedInformer
#

如果应用中多个业务逻辑要监控同一资源,可能会编写多个 Informer,产生了冗余请求以及资源占用,于是 kubernetes 对 Informer 进行封装,提供了 SharedInformer 机制,多个 Informer 共享同一个缓存,对相同资源对象只会有一次调用。其中 SharedInformerFactory 中有一个 Informer map,当业务通过 InformerFactory 获取一个 Informer 时,会先从 map 中查找,判断该类型 Informer 是否存在,如果不存在则创建一个新的 Informer,并将其添加到 map 中,然后返回该 Informer。

Leader Election
#

实际应用部署时,为了保证高可用,实际会运行多个 Controller 实例。此时需要进行 Leader 选举,被选中的实例方可进行资源监听以及调谐。如果 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,Scheme 结构体包含 gvkToType 和 typeToGVK 的字段映射关系,APIServer 根据 Scheme 来进行资源的序列化和反序列化。

Schema 定义了自定义资源的字段、验证规则、默认值等,确保数据的一致性和合法性。

Schema和CRD的联系:

  • 功能:Scheme 主要用于类型管理和序列化/反序列化,而 CRD 用于定义和扩展 Kubernetes API。
  • 使用场景:Scheme 是 Kubebuilder 项目内部使用的,用于确保控制器和 API 服务器之间的类型一致性;CRD 是 Kubernetes 集群中的资源,用于定义自定义资源的结构和行为。
  • 实现方式:Scheme 是通过 Go 代码定义和管理的,而 CRD 是通过 YAML 文件定义并应用到 Kubernetes 集群中的。在 Kubebuilder 项目中:
    • Scheme 通常在 pkg/apis/ 目录下定义,包含了所有自定义资源(CR)的 Go 结构体和相应的类型信息。
    • CRD 是通过代码生成工具自动生成的。用户在定义自定义资源的 Go 结构体后,Kubebuilder 会生成相应的 CRD YAML 文件,用户可以将这些文件应用到 Kubernetes 集群中。

schema 定义了资源的参数模型,并通过序列化方式转化为 CRD 等配置,存储至 etcd 中,以下为示例:

// 此处为定义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 进行工作。Operator 将一个 endpoint(称为自定义资源 CR)添加到 Kubernetes API 中,该 endpoint 还包含一个监控和维护新类型资源的控制平面组件。整个操作原理如下图所示:

当 Operator 接收任何信息时,它将采取行动将 Kubernetes 集群或外部系统调整到所需的状态,作为其在自定义 controller 中的和解循环(reconciliation loop)的一部分

Kubebuilder
#

Kubebuilder 是一个基于 CRD 来构建 Kubernetes API 的框架,可以使用 CRD 来构建 API、Controller 和 Admission Webhook。参考文档:The Kubebuilder Book

deepcopy
#

DeepCopy 方法对数据结构进行深拷贝,当你需要在代码中对该一个对象进行修改,而又不希望影响其他使用到该对象的代码时,可以先对对象进行一次 DeepCopy,拿到该对象的一个副本后再进行操作。

参考
#

Related

Golang Channel
·5 mins
Golang sync.Map
·4 mins
About Me
About
摄影调色
·2 mins
containerd
·12 mins
Cgroup
·5 mins
Cgroup Cgroup