Kubernetes Resource Controller

Kubernetes 如何管理资源

什么是 kubernetes?

这次的主题不是介绍 k8s,所以只做简单的概括,之前对 k8s 不熟悉的话,可以提问我们现场沟通😂。

docker 等容器化技术解决了应用标准化部署的痛点。接下来的问题就是如何把容器分发到不同的机器上执行,这也被称为编排 orchestrating。

k8s 就是一个编排系统,简而言之主要就是做了这件事:

  1. k8s 负责管理所有的宿主机资源
  2. 给用户提供简单的接口,用户仅需要声明要部署的应用元信息
  3. k8s 管理应用的全生命周期
    1. 负责安排应用所需的资源,部署运行应用
    2. 在一些情况下(水平扩容、节点故障)时,自动迁移应用,保持运行
    3. 用户更新、删除应用时,负责更新、删除所有相关的资源

本次主题我们只讨论最简单的资源:delployment -> replicaset -> pod

Overview

k8s 的资源管理逻辑流程可以概括为三大件:

  • apiserver: 负责存储资源,并且提供查询、修改资源的接口
  • controller: 负责操作(修改)本级和下级资源。

    比如 deploymentController 就负责管理 deployment 资源 和下一级的 rs 资源。

  • kubelet: 安装在 node 节点上,负责部署资源

一些理念:

  • 所有组件都只和 apiserver 通讯,通过 apiserver 对资源声明进行增删改查
  • controller 不断监听本级资源,然后修改下级资源的声明
  • scheduler 负责将现存的 pod 声明分配到 node 上
  • kubelet 查询当前 node 所分配到的资源声明,然后在机器上执行清理和部署。

理想中,每一层控制器(controller)只管理本级和子两层资源。

但因为每一个资源都是被上层定义和创建的,所以实际上每一层资源都对下层资源的定义有完全的了解。 所以由上至下的操作是可以跨级的。

A -> B -> C -> D (此时 A 可以预知 D 的资源格式)

但是跨级操作需要非常小心,最好不要跨级,跨级的话只读而不要修改,因为会干扰下层的其他控制器。

术语

  • UID: 每一个被创建(提交给 apiserver)的资源都有一个全局唯一的 UUID。
  • metadata: 每一个资源都有的元数据,包括 label、owner、uid 等
  • label: 每个资源都要定义的标签
  • selector: 父资源通过 labelSelector 查询归其管理的子资源。 不允许指定空 selector(全匹配)。
  • owner: 子资源维护一个 owner UID 的列表 OwnerReferences, 指向其父级资源。列表中第一个有效的指针会被视为生效的父资源。 selector 实际上只是一个 adoption 的机制, 真实起作用的父子级关系是靠 owner 来维持的, 而且 owner 优先级高于 selector。
  • replicas: 副本数,pod 数
  • 父/子资源的相关:
    • orphan: 没有 owner 的资源(需要被 adopt 或 GC)
    • adopt: 将 orphan 纳入某个资源的管理(成为其 owner)
    • match: 父子资源的 label/selector 匹配
    • release: 子资源的 label 不再匹配父资源的 selector,将其释放
  • RS 相关:
    • saturated: 饱和,意指某个资源的 replicas 已符合要求
    • surge: rs 的 replicas 不能超过 spec.replicas + surge
    • proportion: 每轮 rolling 时,rs 的变化量(小于 maxSurge)
    • fraction: scale 时 rs 期望的变化量(可能大于 maxSurge)

Controller

控制器的核心代码可以概括为:

for {
    for {
        key, empty := queue.Get()
        if empty {
            break
        }

        defer queue.Done(key)
        syncHandler(key)
    }

    time.sleep(time.Second)
}
  1. 监听变化的资源
  2. 获取资源声明和当前状态
  3. 执行操作,修改其当前状态使之满足声明

查询关联子资源

k8s 中,资源间可以有上下级(父子)关系。

理论上 每一个 controller 都负责创建当前资源和子资源,父资源通过 labelSelector 查询应该匹配的子资源。

一个 deployment 的定义:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

但其实 k8s 内部还为每一个创建的资源分配了一个 UID(UUID)。每一个资源还有一个 OwnerReferences 属性,保存了父资源的 UID。

所以 k8s 内部的上下级关系其实是通过 owner 指针来维系的。owner 的优先级,要高于 label/selector。

拿 deployment 来举例,每次 syncHandler 处理一个 deployments 时:

  1. 遍历 namespace 下所有的 rs
  2. 如果 rs owner 不为空,且不匹配,skip
  3. 如果 rs owner 不为空,且匹配,但是 label 不匹配,release
  4. 如果 rs owner 为空,且 label 匹配,adopt

如果 owner 不匹配,即使 label 匹配,也不会被视为是父子关系

Garbage Collector

前文提到了 k8s 的资源有父子关联,那么这个关联有什么用呢?

实际上这个关联就是用来做级联删除(cascading deletion)的。

删除一个资源有三种方式(propagationPolicy/DeletionPropagation):

  • Foreground(default): Children are deleted before the parent (post-order)
  • Background: Parent is deleted before the children (pre-order)
  • Orphan: ignore owner references

(可以通过 kubelet delete --cascade=??? 使用)

Deletion

k8s 中,资源的 metadata 中有几个对删除比较重要的属性:

  • ownerRerences: 指向父资源的 UID
  • deletionTimestamp: 如果不为空,表明该资源正在被删除中
  • finalizers: 一个字符串数组,列举删除前必须执行的操作
  • blockOwnerDeletion: 布尔,当前资源是否会阻塞父资源的删除流程

无论是什么删除策略,都需要先把所有的 finalizer 逐一执行完,每完成一个,就从 finalizers 中移除一个。在 finalizers 为空后,才能正式的删除资源。

Foreground cascading deletion

  1. 设置资源的 deletionTimestamp,表明该资源的状态为正在删除中("deletion in progress")。
  2. 设置资源的 metadata.finalizers"foregroundDeletion"
  3. 删除所有 ownerReference.blockOwnerDeletion=true 的子资源
  4. 删除当前资源

每一个子资源的 owner 列表的元素里,都有一个属性 ownerReference.blockOwnerDeletion,这是一个 bool,表明当前资源是否会阻塞父资源的删除流程。删除父资源前,应该把所有标记为阻塞的子资源都删光。

在当前资源被删除以前,该资源都通过 apiserver 持续可见。

Orphan deletion

触发 FinalizerOrphanDependents,将所有子资源的 owner 清空,也就是令其成为 orphan。然后再删除当前资源。

Background cascading deletion

立刻删除当前资源,然后在后台任务中删除子资源。

foreground 和 orphan 删除策略是通过 finalizer 实现的

const (
    FinalizerOrphanDependents = "orphan"
    FinalizerDeleteDependents = "foregroundDeletion"
)

因为这两个策略有一些删除前必须要做的事情。而 background 则就是走标准删除流程:删自己 -> 删依赖。

Informer

前文中提到的 controller、GC 等都依赖对资源事件的监听,而且动辄就会有大量的查询甚至遍历。如果每次都直接去请求 apiserver 的话,会造成较大的压力和资源的浪费。

Informer 也经历了两代演进,从最早各管各的 Informer,到后来统一监听,各自 filter 的 sharedIndexer。

所有的 controller 都在一个 controller-manager 进程内,所以完全可以共享同一个 informer, 不同的 controller 注册不同的 filter(kind、labelSelector),来订阅自己需要的消息。

简而言之,现在的 sharedIndexer,就是一个统一的消息订阅器,而且内部还维护了一个资源存储,对外提供可过滤的消息分发和资源查询功能。

各个 controller 的订阅和查询绝大部分都在 sharedIndexer 的内存内完成,提高资源利用率和效率。

一般 controller 的消息来源就通过两种方式:

  1. lister: controller 注册监听特定类型的资源事件,事件格式是字符串,<namespace>/<name>
  2. handler: controller 通过 informer 的 AddEventHandler 方法注册 Add/Update/Delete 事件的处理函数。

这里有个值得注意的地方是,资源事件的格式是字符串,形如 <namespace>/<name>,这其中没有包含版本信息。

那么某个版本的 controller 拿到这个信息后,并不知道查询出来的资源是否匹配自己的版本,也许会查出一个很旧版本的资源。

所以 controller 对于资源必须是向后兼容的,新版本的 controller 必须要能够处理旧版资源。 这样的话,只需要保证运行的是最新版的 controller 就行了。

参考资料

如何阅读源码

核心代码:https://github.com/kubernetes/kubernetes,所有的 controller 代码都在 pkg/controller/ 中。

所有的 clientset、informer 都被抽象出来在 https://github.com/kubernetes/client-go 库中,供各个组件复用。

学习用示例项目:https://github.com/kubernetes/sample-controller