与容器相关的那些事——Kubernetes资源抢占和持久化

Updated on with 0 views and 0 comments

1 优先级和抢占机制

优先级和抢占机制,解决的是Pod调度失败时该怎么办的问题。

正常情况下,当一个Pod调度失败后,它就会被暂时“搁置”起来,直到Pod被更新,或者集群状态发生变化,调度器才会对这个Pod进行重新调度。

1.1 PriorityClass

前一篇文章提到过 Kubernetes调度器 维护着一个优先级队列,当Pod拥有了优先级之后,高优先级的Pod就可能会比低优先级的Pod提前出队。

优先级的定义需要用到 PriorityClass,具体的用法见下图:
ea46d17031884f8f83278f85a44af024.png

说明:

  • 优先级是一个 32 bit 的整数,最大值不超过10亿,并且值越大代表优先级越高
  • 超出10亿的部分,被Kubernetes预留给系统Pod使用的,这样可以保证系统Pod不被用户抢占
  • 如果 globalDefault 被设置为true,那就意味着这个PriorityClass的值会成为系统的默认值
  • 如果 globalDefault 被设置为false,那么对于没有声明使用该PriorityClass的Pod来说,其优先级是0
  • 如果Pod声明使用某个PriorityClass,那么当这个Pod被提交给Kubernetes之后,Kubernetes的 PriorityAdmissionController 就会自动为这个Pod设置 spec.priority 字段
  • Kubernetes v1.19 开始引入非抢占式PriorityClass,即设置 preemptionPolicy 为Never

1.2 抢占机制的原理

Kubernetes调度器为了实现抢占算法用到了两个队列:

  • activeQ:凡是在activeQ里的Pod,都是下一个调度周期需要调度的对象
  • unschedulableQ:专门用来存放调度失败的Pod

69241fff0d6442ca87fb6f3acfa7c659.png

当某个Pod调度失败后,会被放进unschedulableQ队列,这次失败事件会触发抢占流程,可以分为两大步骤:

  • 寻找牺牲者
    • 首先,调度器会检查这次失败事件的原因,因为有很多 Predicates(不清楚是啥的出门右转看 Kubernetes调度器)的失败是不能通过抢占来解决的
    • 如果确定抢占可以解决这次调度失败问题,那么调度器就会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程
    • 在模拟过程中,调度器会检查缓存副本里的每一个Node,然后从该Node上最低优先级的Pod开始,逐一“删除”这些Pod
    • 当遍历完所有的节点之后,调度器会在上述模拟产生的所有抢占结果里做一个选择,原则是尽量减少抢占对整个系统的影响
  • 开始抢占
    • 调度器会把抢占者Pod的 nominatedNodeName 字段设置为被抢占Pod所在的Node的名字,并不会立刻被调度到被抢占的Node上,说白了,就是把抢占者交给下一个调度周期再处理
    • 在下一个调度周期时,调度器会考虑潜在的抢占者,即activeQ队列里存在相同nominatedNodeName值的Pod,而Pod之间可能会受到 InterPodAntiAffinity 规则约束
    • 简言之,调度器并不保证抢占者一定会运行在当初选定的被抢占的Node上

2 持久化存储体系

容器化一个应用比较麻烦的是对其“状态”的管理,接下来,我们一起看看最常见的存储状态。

2.1 PV、PVC、StorageClass

6fad0d601a3646f595d4ed385fb48f2e.png

说明:

  • PVC 描述的是Pod想要使用的持久化存储的属性,比如存储的大小、读写权限等
  • PV 描述的是一个具体的Volume的属性,比如Volume的类型、挂载目录、远程存储服务器地址等
  • StorageClass 的作用是充当PV的模板,并且只有StorageClass相同的PV和PVC才可以绑定在一起;StorageClass的另一个重要作用是指定PV的Provisioner(存储插件),如果存储插件支持 Dynamic Provisioning ,那么Kubernetes就可以自动创建需要的PV

2.2 本地持久化存储DEMO

为了进行以下DEMO,可以使用 RAM Disk 来模拟本地磁盘:

mkdir -p /mnt/disks/vol
mount -t tmpfs -o size=100m vol /mnt/disks/vol
  • 定义 local StorageClass
    4f770c642fe048f5b49aaeeceab5bb0d.png
  • 定义 local PV,注意 local PV 目前尚不支持 Dynamic Provisioning,所以没办法在用户创建PVC的时候,就自动创建出对应的PV
    5065d439659840d4bb96f3d9ed50af94.png
  • 创建 local PVC,可以看到没有立即绑定 local PV
    24a5e24061004ac6ae7e58383754e306.png
  • Pod声明PVC,才会开始绑定 local PV,即在调度的时候就考虑Volume分布
    0223e35996044d3880a646ccf751039e.png

看完上面的过程,可以发现 local PV 并非通过 hostPathnodeAffinity 来实现的。

2.3 持久化Volume的过程

持久化Volume分为两阶段,是靠独立于kubelet主控制循环(Kubelet Sync Loop)之外的两个控制循环来实现的:

  • 第一阶段是 Attach为宿主机挂载远程磁盘
    • Attach阶段,Kubernetes提供的可用参数是nodeName,即宿主机的名字
    • 如果Volume类型是远程文件存储,比如NFS,则可以跳过这一阶段
    • Attach操作是由 Volume Controller 负责维护的,这个控制循环叫作 AttachDetachController运行在Master节点上的
  • 第二阶段是 Mount将磁盘设备格式化并挂载到Volume宿主机目录
    • Mount阶段,Kubernetes提供的可用参数是dir,即Volume的宿主机目录
    • Mount操作必须发生在Pod对应的宿主机上,这个控制循环叫作 VolumeManagerReconciler,是kubelet组件的一部分,是一个独立于kubelet主循环的Goroutine

经过上面两阶段处理,就得到了一个可以持久化的Volume宿主机目录。接下来,kubelet只要把这个Volume目录通过CRI里的Mounts参数传递给Docker,然后就可以为Pod里的容器挂载这个持久化的Volume了,相当于执行了如下所示的命令:

docker run -v <宿主机挂载目录>:<容器内的目标目录> xxx镜像 ...

2.4 CSI插件体系

仅仅依靠Attach阶段和Mount阶段是无法实现 Dynamic Provisioning 这类相对复杂的功能,这个时候就需要有 Container Storage Interface(CSI)这样更完善、更编程友好的插件方式。

简单来说,CSI插件体系的核心设计思想,就是把Provision阶段以及Kubernetes的一部分存储管理功能从主干代码里剥离出来,做成几个单独的组件,这些组件会通过 Watch API 监听Kubernetes里与存储相关的事件变化,比如PVC的创建,来执行具体的存储管理动作。

eb9db8d555f841d79ee31b3743854757.png

上图最右侧的部分,就是需要我们编写代码来实现的CSI插件。一个CSI插件只有一个二进制文件,它会以 gRPC 的方式对外提供三个服务:

  • CSI Identity 服务,负责对外暴露这个插件本身的信息
  • CSI Controller 服务,属于Kubernetes里 Volume Controller 的逻辑,是属于Master节点的一部分,其定义了对 CSI Volume(对应Kubernetes里的PV)的管理接口,比如:
    • 创建和删除 CSI Volume,对应Provision阶段
    • 对 CSI Volume 进行Attach/Dettach,在CSI里这个操作被叫作 Publish/Unpublish,对应Attach阶段
    • 对 CSI Volume 进行Snapshot
  • CSI Node 服务,定义了在宿主机上对 CSI Volume 执行的操作

除了上面三个服务,这套存储插件体系多了三个独立的 External Components,对应的正是从Kubernetes项目里面剥离出来的那部分存储管理功能:

  • Driver Registrar 组件,负责将插件注册到kubelet里面,在具体实现上,需要请求CSI插件的Identity服务来获取插件信息

    新版本由 Node Driver Registrar 替代

  • External Provisioner 组件,负责的是Provision阶段,它监听了APIServer里的PVC对象
  • External Attacher 组件,负责的是Attach阶段,它监听了APIServer里 VolumeAttachment 对象的变化

注意,Volume的Mount阶段并不属于 External Components 的职责

综上所述,CSI插件体系包含Provision、Attach和Mount三个阶段。其中,Provision相当于创建磁盘,至于Attach和Mount与原来的区别如下:

  • 当kubelet的 VolumeManagerReconciler 控制循环需要进行Mount操作时,它实际上会直接向 CSI Node 服务发起调用 NodePublishVolume 方法的请求
  • AttachDetachController 需要进行Attach操作时,它实际上会创建一个VolumeAttachment对象,从而触发 External Attacher 调用 CSI Controller 服务的 ControllerPublishVolume 方法

2.5 CSI插件部署

  • 通过 DaemonSet 在每个Node上都启动一个CSI插件,为kubelet提供一对一的 CSI Node 服务
    • Driver Registrar 这个外部组件以 sidecar 的方式运行
    • 需要开启双向挂载传播,即将mountPropagation设置Bidirectional,将容器的挂载操作“传播”给宿主机
  • 通过 StatefulSet 在任意一个Node上再启动一个CSI插件,为 External Components 提供 CSI Controller 服务
    • External Provisioner 和 External Attacher 这两个外部组件,需要以 sidecar 的方式,与提供 CSI Controller 服务的CSI插件运行在同一个Pod中,方便调用
    • 将StatefulSet的replicas设置为1,可以确保Pod被删除重建的时候,有且只有一个CSI插件的Pod运行在集群中

2.6 CSI插件运行流程

41b0d21f182c47b19a8e5f6361f77ca1.png

CSI插件相对来说比较复杂,可能需要多看几遍才能消化理解!


标题:与容器相关的那些事——Kubernetes资源抢占和持久化
作者:yanghao
地址:http://solo.fancydigital.com.cn/articles/2022/04/29/1651165018402.html