《深入剖析kubernetes》学习笔记(7)——作业调度和资源管理
40. kubernetes的资源模型和资源管理
-
Pod是kubernetes中最小的调度单元,与调度和资源管理相关的属性都属于Pod对象的字段(实际上定义在Pod的各容器下),例如
apiVersion: v1 kind: Pod metadata: name: frontend spec: containers: - name: db image: mysql env: - name: MYSQL_ROOT_PASSWORD value: "password" resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" - name: wp image: wordpress resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m"
-
资源的分类
- 可压缩资源,例如CPU,当其不足时,Pod只会“饥饿”等待,而不会退出
- 不可压缩资源,例如内存,当其不足时,Pod会被内核kill
-
yaml中资源的单位
- CPU:一般写作
cpu:“500m”
,表示500milicpu,意味着0.5cpu,也可以直接写成cpu:0.5
,但这种写法不通用,仍建议使用m
为单位 - 内存:默认单位时bytes,例如
memory:500Mi
,意味着内存为500*1024*1024bytes;memory:100M
意味着内存为500*1000*1000bytes
- CPU:一般写作
-
定义资源限制时,还分limits和requests两种
- 在调度的时候,kube-scheduler 只会按照 requests 的值进行计算
- 在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置
- 类似Borg 论文中对“动态资源边界”的思想(刚提交作业时,其所需资源一般远小于他的资源限额,所以刚提交时会主动减小其资源限额,以提升效率,当资源用量提升到一定阈值时再“快速恢复”)
-
kubernetes的QoS模型,给Pod划分为几个QoS级别:
- Guaranteed: Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等(若仅设置了limits没有设置requests,kubernetes会自动设置与limits取值相同的requests)
- Burstable:Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests
- BestEffort:Pod 既没有设置 requests,也没有设置 limits
-
当宿主机资源紧张的时候,kubelet 会对 Pod 进行 Eviction(即资源回收)
-
比如,可用内存(memory.available)、可用的宿主机磁盘空间(nodefs.available),以及容器运行时镜像存储空间(imagefs.available)等等
-
Eviction 在 Kubernetes 里分为 Soft 和 Hard 两种模式。
- Soft Eviction 允许设置一段“优雅时间”,达到阈值持续一段时间才会触发Eviction
- Hard Eviction 模式下,Eviction 过程就会在阈值达到之后立刻开始。
-
Eviction时,kubelet根据Pod的QoS级别来决定回收哪些Pod:
- 首先选择 BestEffort 类别的 Pod
- 其次是Burstable 类别中资源使用量超过requestes的Pod
- 最后是Guaranteed类别中资源使用量超过limits的,或者宿主机内存紧张的Pod
- 建议将DaemonSet的Pod都设置为Guaranteed级别,不然一旦被回收又会立刻重建,令资源回收失去意义
-
cpuset的设置
- 通过cpuset的设置,可将容器绑定到某个CPU核心上,无需共享CPU,减小OS在CPU之间切换的开销,提高容器中应用性能
- 设置方式:保证Pod是Guaranteed类别,且CPU的requests和limits是相同的整数
41.kubernetes的默认调度器
- 调度器职责在于为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。
- 调度器的核心在于两个相互独立的控制循环:
- Informer Path
- 启动一系列Informer,监听Etcd中与Pod、Node、Service 等与调度相关的 API 对象的变化
- 将待调度的Pod加入调度队列(默认是优先级队列)
- 更新调度器缓存(scheduler cache)中的信息
- Scheduling Path
- 是实际负责调度的主循环
- 从调度队列中取出一个Pod,调用Predicates算法“过滤”得到一组Node(“过滤”所需的Node信息都是从调度器缓存中获取的),再调用Priority算法对这组Node进行打分,得分最高的作为调度的结果
- 执行“乐观绑定”(也叫做Assume),真正的绑定操作需要访问APIServer,修改Pod对象中nodeName字段的值,而在本步骤中直接修改调度器缓存中的Pod和Node的信息
- 创建一个Goroutine来异步地向APIServer发起更新Pod的请求,完成真正的Bind操作
- Informer Path
- Admit操作
- 由于“乐观绑定”的操作,所以当新Pod完成调度要在某个节点上启动时,该节点kubelet还要执行Admit操作再次验证Pod是否确实能够在本节点上运行。
- 实际就是再执行了一遍GeneralPredicates的基本调度算法
42.默认调度器策略
Predicates算法过滤Node的策略
- 最基础的调度策略GeneralPredicates
- PodFitsResources检查Pod的requests字段,计算宿主机的CPU和内存资源是否足够
- PodFitsHost检查宿主机名字是否和Pod的sepc.nodeName一致(应只在admit操作中生效?)
- PodFitsHostPort检查Pod申请的宿主机端口spec.nodePort是否已被占用
- PodMatchNodeSelector检查Pod的nodeSelector或者nodeAffinity指定的节点是否与当前考察节点匹配
- 该组策略检查了Pod能否运行在一个Node上的基本条件,在Pod启动之后前,kubelet还会执行Admit操作,即再执行一遍GeneralPredicates
- 与Volume相关的过滤策略
- NoDiskConflict检查多个Pod声明挂载的持久化Volume是否冲突
- MaxPDVolumeCountPredicate检查某类型的持久化Volume是否超过一定数目
- VolumeZonePredicate检查持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配
- VolumeBindingPredicate检查该Pod对应的PV的spec.nodeAffinity字段是否和该节点匹配【本地持久化卷“延迟绑定”发生的地方,若Pod的PVC还没有和PV绑定,调度器负责遍历所有待绑定的PV,当出现可用PV、且宿主机满足nodeAffinity要求一致时,该规则才返回“成功”】
- 与宿主机相关的过滤策略
- 主要检查待调度的Pod是否满足Node自身的要求,例如
- PodToleratesNodeTaints检查Pod的Toleration字段是否和Node的Taint字段匹配
- NodeMemoryPressurePredicate检查当前节点内存是否充足
- 与Pod相关的过度策略
- 与GeneralPredicates的规则大多数重合
- PodAffinityPredicate检查待调度的Pod和当前节点已有Pod之间的亲密和反亲密关系
- 当开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个 Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod 的宿主机列表
Priority算法
-
LeastRequestedPriority:选择空闲资源(CPU 和 Memory)最多的宿主机
score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2
-
BalancedResourceAllocation:选择资源占用最均衡的宿主机
score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10
-
ImageLocalityPriority:如果Pod所需镜像很大,则已有其所需镜像的node的得分会更高
- 为了避免引起调度堆叠,当大镜像分布的节点很少时,则适当调低这些节点的权重
-
还有NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority等
43.调度优先级和抢占机制
-
优先级(Priority)和抢占(Preemption)解决的是调度失败时怎么办的问题
-
基本思路
- 当某个高优先级的Pod调度失败,则“挤走“某个Node上的一些低优先级Pod,从而保证高优先级Pod的成功调度
-
PriorityClass的定义
apiVersion: scheduling.k8s.io/v1beta1 kind: PriorityClass metadata: name: high-priority value: 1000000 globalDefault: false description: "This priority class should be used for high priority service pods only."
- Kubernetes 规定,优先级是一个 32 bit 的整数,且不超过10亿,超出的部分分配给了系统Pod
-
具体实现
- 调度器维护了两个调度队列
- activeQ,存放下一个调度周期需要调度的Pod
- unschedulableQ,存放调度失败的Pod。当其中的Pod更新后,就将其移动到activeQ中
- 如何寻找Victims(应该被”挤走“的Pod)?
- 第一步,检查调度失败原因,确认抢占是否可以帮抢占者找到新节点。(比如,若因没有和nodeAffinity匹配的节点而调度失败,则不会触发抢占)
- 第二步,复制一份缓存的节点信息,用该副本模拟抢占操作。
- 模拟抢占操作的过程
- 遍历缓存副本中的所有节点,在每个节点上,从最低优先级开始逐一删除Pod,一旦抢占者可以运行,则记录该Node和需要删除的Pod列表
- 从所有可能的抢占结果中选择一个,找到最优结果,以尽量减小对现有系统的影响(被删除的Pod数量最小,或者优先级最低等等)
- 实际抢占过程
- 第一步,清理牺牲者Pod所携带的nominatedNodeName字段
- 第二步,将抢占者Pod的nominatedNodeName字段设置为被抢占的Node名字,抢占者Pod重新进入activeQ,进入下一个调度周期【需要注意,调度器不能保证该Pod一定会被调度到nominatedNodeName指定的Node上】
- 第三步,开启一个Goroutine,同步删除牺牲者
- 当activeQ中存在有比当前待调度的Pod优先级更高的Pod携带了nominatedNodeName字段时,则对当前待调度Pod,调度器会对所有Node执行两遍Predicates算法
- 第一遍, 调度器会假设上述“潜在的抢占者”已经运行在这个节点上,然后执行 Predicates 算法;
- 原因在于,考虑InterPodAntiAffinity的规则,若抢占者已经存在待考察Node上了,当前Pod还能否成功调度到该Node上
- 第二遍, 调度器会正常执行 Predicates 算法,即:不考虑任何“潜在的抢占者”。
- 第一遍, 调度器会假设上述“潜在的抢占者”已经运行在这个节点上,然后执行 Predicates 算法;
- 调度器维护了两个调度队列
-
当整个集群发生可能会影响调度结果的变化(比如,添加或者更新 Node,添加和更新 PV、Service 等)时,调度器会执行一个被称为 MoveAllToActiveQueue 的操作,把所调度失败的 Pod 从 unscheduelableQ 移动到 activeQ 里面
-
当一个已经调度成功的 Pod 被更新时,调度器则会将 unschedulableQ 里所有跟这个 Pod 有 Affinity/Anti-affinity 关系的 Pod,移动到 activeQ 里面
44.GPU管理和device plugin
- 基本使用诉求:只要在 Pod 的 YAML 里面,声明某容器需要的 GPU 个数,那么 Kubernetes 为我创建的容器里就应该出现对应的 GPU 设备,以及它对应的驱动目录
- Kubernetes 在 Pod 的 API 对象里,没有为 GPU 专门设置一个资源类型字段,而是使用了一种叫作 Extended Resource(ER)的特殊字段来负责传递 GPU 的信息,例如
apiVersion: v1
kind: Pod
metadata:
name: cuda-vector-add
spec:
restartPolicy: OnFailure
containers:
- name: cuda-vector-add
image: "k8s.gcr.io/cuda-vector-add:v0.1"
resources:
limits:
nvidia.com/gpu: 1
- 宿主机需要向kubernetes的APIServer汇报自身的GPU资源情况,这是通过device plugin机制实现的
- Device Plugin示意图
- Device Plugin工作流程
- 每一种硬件设备,都有对应的Device Plugin管理,通过gRPC的方法与kubelet连接
- ListAndWatch:定期向kubelet汇报该Node上的GPU列表
- Allocate:Pod调度到本机后,kubelet从自己持有的GPU中为该容器分配其所需的GPU,即通过Allocate()方法向Device Plugin传递待分配的GPU ID列表
- Device Plugin根据Allocate()方法提供的GPU ID列表,查找设备的路径和驱动目录,返回给kubelet
- kubelet将设备的信息追加在CRI请求中,发送给Docker,则Docker创建的容器就会有这个设备,并挂载了其所需的驱动目录
- Device Plugin机制的缺点:
- Allocate 和 ListAndWatch API 无法添加可扩展性的参数,所以对硬件设备的管理只能停留在“设备个数”这一层面,无法完成更加复杂的操作