kube-apiserver源码剖析与开发(七):自定义资源控制器开发(一)

56 阅读6分钟

在前面的文章中,我们讲了 kube-apiserver 的基本原理,包括 kube-apiserver 的初始化流程和请求处理步骤,重点放在了分析内置 API 资源的路由初始化和请求处理,如 Pod、Node等。k8s 支持对 API 扩展,用户可以创建一个自定义的 API 类型,这在 k8s 中叫 custom resource definitions(CRD),基于这个 CRD 创建的 API 资源叫做 custom resource(CR),可类比 deployment 是一个内置的 API 类型,你创建了一个具体的 deployment 实例。

不过,仅仅创建 CRD 和 CR 并没有实际的作用,需要有一个对应的控制器对资源变化做出反应,使得最终能够达到 CR 中描述的状态,这在 k8s 中叫调谐(Reconcile)。我们以 deployment 为例说明,当你创建了副本数为 1 的 deployment 时,控制器检测到 deployment 创建后,就开始调谐创建一个 Pod;当你修改副本数为 2 时,控制器监测到 deployment 发生了变化,就开始调谐,达到最终的副本数为 2 的这么一个状态。

每一个内置的 API 类型都有一个控制器,你可以查看 k8s 源码 kubernetes/pkg/controller 目录

pPpw0EQ.jpg

虽然我们平时说 kube-controller-manager 是 k8s 的一个核心组件,实际上上图中所有的 API 资源控制器都是按集成在 kube-controller-manager 中,他们各司其职,监听各自负责的资源变化,调谐状态,达到预期的结果,组成了一个完整的 kube-controller-manager 组件。

所以自定义资源也需要一个控制器,对自定义资源进行调谐。

能实现自定义 API 类型扩展主要依托于 k8s 的两个能力:

  • apiExtensionsServer 在前面分析 kube-apiserver 初始化的时候我们说过,kube-apiserver 由三个 server 组成:aggregatorServer、kubeAPIServer、apiExtensionsServer。其中的 apiExtensionsServer 就是专门为了用户自定义 API 资源扩展而生的,例如下面是一个 calico bgp 配置的一个 crd,当我们使用 kubectl apply 创建这个 API 类型时,Kubectl 就会访问 http://host:port/apis/apiextensions.k8s.io/v1/customresourcedefinitions,而这个 url 是注册在 apiExtensionsServer,所以最终 apiExtensionsServer 会去调用 etcd 的接口,在 etcd 中写了这个资源的定义。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: bgpconfigurations.crd.projectcalico.org
spec:
  conversion:
    strategy: None
  group: crd.projectcalico.org
  names:
    kind: BGPConfiguration
    listKind: BGPConfigurationList
    plural: bgpconfigurations
    singular: bgpconfiguration
  preserveUnknownFields: true
  scope: Cluster
  versions:
  - name: v1
    served: true
    storage: true
  • list/watch list/watch 是实现调谐的基石,也是 k8s 的精髓所在,它能够感知资源的变化。顾名思义,lilst/watch 由 list 和 watch 两部分组成,一个控制器启动时首先会对自己负责的资源 list 一遍,list 操作会把存在 etcd 中全量的该资源同步到本地缓存中,后续控制器感知到的资源增量变化(增、删、改)都会在缓存中更新;watch 是用于监测资源变化的, watch 长链接是通过 http 协议 chunked 机制实现的,在响应头里加一个 Transfer-Encoding: chunked 就可以实现分段响应。

那么为什么要以这种方式进行资源变化的感知和调谐呢?

试想一个场景,一个集群有 1000 个节点,节点上的 kubelet 需要感知到哪些 Pod 被调度到本机上,如果采用轮询的机制,kubelet 需要定时向 kube-apiserver 发起请求获取 Pod,那么 kube-apiserver 的压力是巨大的,最坏的情况就是需要处理 QPS 为 1000 的 get pod 请求;并且,并不是时时刻刻都会有 Pod 需要调度的,如果是轮询的方式,很多时候获取到的数据或请求都是无效的,这无疑是浪费网络和计算资源。

而 list/watch模式,在 kubelet 启动的时候先把全量的 pod list下来,后续通过 watch 和 kube-apiserver 建立长链接,只有当 kube-apiserver 有数据需要下发给控制器的时候才会通过 watch 建立的长链接下发,同样是 1000 个节点,这种模式 kube-apiserver 只需要维护 1000 个链接即可,这样即降低了 kube-apiserver 的压力,也减少了网络带宽的浪费。

说了这么多,这个 list/watch需要用户自己实现吗?

k8s 已经在 client-go 中实现了 informer 机制,它的主要功能就是 list/watch。

下面我们来看下 informer 的主要原理,下图是一张经典的 informer 工作机制原理图

pP9ZSzt.png

这张图分上下两部分,上半部分就是 client-go 的 informer 部分,下半部分则是需要用户实现的控制器。

上半部分主要由:Reflector、DeltaFIFO、LocalStore、Informer 组成,下面我们来看看它的具体工作流程:

  1. 控制器启动时 Informer 首先会向 kube-apiserver list 注册在 Informer 上的 API 资源,由 Reflector 和 kube-apiserver 建立长链接,将 list 到的全量 API 资源存储在 LocalStore(它是一个线程安全的 map),后续用户的控制器想要获取该 API 类型的实例,就不需要访问 kube-apiserver,而是直接可以从本地缓存 LocalStore 汇总获取到;

  2. 后续如果该 API 类型的资源有增、删、改等行为,Reflector 都能通过长链接(由 http2 头部增加 Transfer-Encoding: chunked 实现)接收到变化,该事件及它对应的 API 对象这个组合,被称为增量(Delta),它会被放进 DeltaFIFO 中。

  3. 一旦 DeltaFIFO 中有数据,Informer 会马上取出,然后会做两件事: (1)判断事件的类型(增、删、改),然后根据类型更新 LocalStore,例如是删除数据,则 informer 会删除 LocalStore 中该 API 实例;如果事件类型是增加操作,那么 Informer 会通过 Indexer 把这个增量里的 API 实例保存到LocalStore,并为它创建索引; (2)调用注册在 Informer 上该 API 的事件处理函数,即图中的 Resource Event Handlers,handlers 需要用户自己实现,一般是先对事件进行过滤,然后将事件(实际上就是 API的 name + namespace)写入到队列中。

  4. 用户实现的控制器从队列中取出对象,启动一个协程 来执行自己的调谐逻辑,调谐逻辑一般就是计算集群当前的状态和用户希望期望状态的状态,然后根据计算结果做出一系列操作,最终让这两个状态达到一致,如 deployment 的 replicas 由 1 变为 2,那么控制器就需要调用 kube-apiserver 的 api 创建一个新的 Pod,达到新的预期状态。

以上就是 informer 和控制器的基本原理。

那么我们该怎么去实现一个控制器呢?一般我们会使用 kubebuilder 工具进行开发?那么他的作用是什么呢?

我们回头看下上图下半部分,用户需要实现的部分包括以下几个:

  • 事件处理函数
  • 队列
  • 调谐逻辑(控制器逻辑)

此外,还需要创建

  • CRD yaml
  • 为了能够让我们的控制器有权限向 kube-apiserver 访问需要的资源,需要创建rbac yaml

上面的步骤看起来还是比较麻烦的,所幸 kubebuilder 为我们的开发提供了便利,只要我们创建一个 kubebuilder 工程,它就会给我们封装好事件处理函数、队列的操作,创建 crd yaml,创建 rbac yaml,我们唯一需要做的就是专心实现调谐逻辑即可。此外,kubebuilder 还有构建镜像、发布 crd 和控制器到目标集群等能力,为我们省去了很多麻烦。

下一篇文章我们就使用 kubebuilder 来开发一个实例进行实践。