kubernetes-CRD的实现原理

777 阅读6分钟

CRD(custom resource definition)

简介

在 Kubernetes 中一切都可视为资源,Kubernetes 1.7 之后增加了对 CRD 自定义资源二次开发能力来扩
展 Kubernetes API,通过 CRD 我们可以向 Kubernetes API 中增加新资源类型,而不需要修改 
Kubernetes 源码来创建自定义的 API server,该功能大大提高了 Kubernetes 的扩展能力。

当你创建一个新的CRD时,Kubernetes API服务器将为你指定的每个版本创建一个新的RESTful资源路径,我
们可以根据该api路径来创建一些我们自己定义的类型资源。CRD可以是命名空间的,也可以是集群范围的,由
CRD的作用域(scpoe)字段中所指定的,与现有的内置对象一样,删除名称空间将删除该名称空间中的所有自定义
对象。CRDC本身没有名称空间,所有名称空间都可以使用

官方解释&实例

简单栗子

student.yaml:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  # metadata.name的内容是由"复数名.分组名"构成,如下,students是复数名,bolingcavalry.k8s.io是分组名
  name: students.bolingcavalry.k8s.io
spec:
  # 分组名,在REST API中也会用到的,格式是: /apis/分组名/CRD版本
  group: bolingcavalry.k8s.io
  # list of versions supported by this CustomResourceDefinition
  versions:
    - name: v1
      # 是否有效的开关.
      served: true
      # 只有一个版本能被标注为storage
      storage: true
  # 范围是属于namespace的
  scope: Namespaced
  names:
    # 复数名
    plural: students
    # 单数名
    singular: student
    # 类型名
    kind: Student
    # 简称,就像service的简称是svc
    shortNames:
    - stu

启动kubectl apply -f student.yaml, 可以通过kubectl get crd查看到新创建的students.bolingcavalry.k8s.io资源, 如果想查看对应更详细的信息, 使用命令kubectl get crd stu

stu 为student.yaml中定义的简称;

如果配置了etcd可以查看到新创建的CRD保存到了etcd上.

controller

但如果仅仅是在etcd保存Student对象是没有什么意义的,试想通过deployment创建pod时,如果只在etcd创建pod对象,而不去node节点创建容器,那这个pod对象只是一条数据而已,没有什么实质性作用,其他对象如service、pv也是如此;

作用

监听指定对象的新增、删除、修改等变化,针对这些变化做出相应的响应

controller.jpg

如上图,API对象的变化会通过Informer存入队列(WorkQueue),在Controller中消费队列的数据做出响应,响应相关的具体代码就是我们要做的真正业务逻辑;

相关代码

可自动生成的代码

上图中的informer, client, workqueud代码可以通过k8s中的工具自动生成, 我们只需要编写controller中的逻辑即可(也就是接收到有变化后需要做处的反应)

client目录下的内容都是客户端相关代码,在开发controller时会用到; client目录下的clientset、informers、listers的身份和作用可以和前面的图结合来理解;

自动生成代码栗子

真正处理的逻辑代码

controller编写的代码

如何与k8s通信

首先,控制器需要与 kubernetes apiserver 进行通讯,则需要上面所说的 client, 这个 client 需要有以下的信息:

  • apiserver 的地址以及连接 apiserver 的认证信息,如用户名密码或者 token。
  • kubernetes 的 API resource 的 group 和 version,以及结构体的定义。
  • 一个 serializer 来控制序列化与反序列化 apiserver 的结果。

然后你就可以用这个 client 去 apiserver list/watch 特定的类型的资源。 一般是建议使用 client-go 中提供的 informer 来 watch 资源的变更而不是轮询 apiserver,因为 informer 的方式性能更好。

infomer

基本功能

Informer 是 Client-go 中的一个核心工具包。在 Kubernetes 源码中,如果 Kubernetes 的某个组件,需要 List/Get Kubernetes 中的 Object,在绝大多 数情况下,会直接使用 Informer 实例中的 Lister()方法(该方法包含 了 Get 和 List 方法),而很少直接请求 Kubernetes API。Informer 最基本 的功能就是 List/Get Kubernetes 中的 Object。

高级功能

Informer 还可以监听事件并触发回调函数等,以实现更加复杂的业务逻辑。

设计思路

为了让 Client-go 更快地返回 List/Get 请求的结果、减少对 Kubenetes API 的直接调用,Informer 被设计实现为一个依赖 Kubernetes List/Watch API 、可监听事件并触发回调函数的二级缓存工具包。

更快地返回 List/Get 请求,减少对 Kubenetes API 的直接调

使用 Informer 实例的 Lister() 方法, List/Get Kubernetes 中的 Object 时,Informer 不会去请求 Kubernetes API,而是直接查找缓存在本地内存中的数据(这份数据由 Informer 自己维护)。通过这种方式,Informer 既可以更快地返回结果,又能减少对 Kubernetes API 的直接调用。

依赖 Kubernetes List/Watch API

Informer 只会调用 Kubernetes List 和 Watch 两种类型的 API。Informer 在初始化的时,先调用 Kubernetes List API 获得某种 resource 的全部 Object,缓存在内存中; 然后,调用 Watch API 去 watch 这种 resource,去维护这份缓存; 最后,Informer 就不再调用 Kubernetes 的任何 API。

用 List/Watch 去维护缓存、保持一致性是非常典型的做法,但令人费解的是,Informer 只在初始化时调用一次 List API,之后完全依赖 Watch API 去维护缓存,没有任何 resync 机制。

可监听事件并触发回调函数

Informer 通过 Kubernetes Watch API 监听某种 resource 下的所有事件。而且,Informer 可以添加自定义的回调函数,这个回调函数实例(即 ResourceEventHandler 实例)只需实现 OnAdd(obj interface{}) OnUpdate(oldObj, newObj interface{}) 和 OnDelete(obj interface{}) 三个方法,这三个方法分别对应 informer 监听到创建、更新和删除这三种事件类型。

二级缓存

二级缓存属于 Informer 的底层缓存机制,这两级缓存分别是DeltaFIFO 和 LocalStore

这两级缓存的用途各不相同。DeltaFIFO 用来存储 Watch API 返回的各种事件LocalStore 只会被 Lister 的 List/Get 方法访问

虽然 Informer 和 Kubernetes 之间没有 resync 机制,但 Informer 内部的这两级缓存之间存在 resync 机制。

详细解析

我们以 Pod 为例,详细说明一下 Informer 的关键逻辑:

  1. Informer 在初始化时,Reflector 会先 List API 获得所有的 Pod

  2. Reflect 拿到全部 Pod 后,会将全部 Pod 放到 Store 中

  3. 如果有人调用 Lister 的 List/Get 方法获取 Pod, 那么 Lister 会直接从 Store 中拿数据

4.jpg 5. Informer 初始化完成之后,Reflector 开始 Watch Pod,监听 Pod 相关 的所有事件;如果此时 pod_1 被删除,那么 Reflector 会监听到这个事件

  1. Reflector 将 pod_1 被删除 的这个事件发送到 DeltaFIFO

  2. DeltaFIFO 首先会将这个事件存储在自己的数据结构中(实际上是一个 queue),然后会直接操作 Store 中的数据,删除 Store 中的 pod_1

  3. DeltaFIFO 再 Pop 这个事件到 Controller 中

9.jpg 10. Controller 收到这个事件,会触发 Processor 的回调函数

10.jpg LocalStore 会周期性地把所有的 Pod 信息重新放到 DeltaFIFO 中

参考&引用资料:

k8s自定义controller三部曲

如何用 client-go 拓展 Kubernetes 的 API

Kubernetes Informer 详解