(一)、Kubernetes的资源模型与资源管理
资源模型的设计
Pod 是最小的原子调度单位。这也就意味着,所有跟调度和资源管理相关的属性都应该是属于 Pod 对象的字段。而这其中最重要的部分,就是 Pod 的 CPU 和内存配置。
在 Kubernetes 中,像 CPU 这样的资源被称作“可压缩资源”(compressible resources)。它的典型特点是,当可压缩资源不足时,Pod 只会“饥饿”,但不会退出。而像内存这样的资源,则被称作“不可压缩资源(incompressible resources)。当不可压缩资源不足时,Pod 就会因为 OOM(Out-Of-Memory)被内核杀掉。
由于 Pod 可以由多个 Container 组成,所以 CPU 和内存资源的限额,是要配置在每个 Container 的定义上的。这样,Pod 整体的资源配置,就由这些 Container 的配置值累加得到。
单位
Kubernetes 里为 CPU 设置的单位是“CPU 的个数”。比如,cpu=1 指的就是,这个 Pod 的 CPU 限额是 1 个 CPU。当然,具体“1 个 CPU”在宿主机上如何解释,是 1 个 CPU 核心,还是 1 个 vCPU,还是 1 个 CPU 的超线程(Hyperthread),完全取决于宿主机的 CPU 实现方式。Kubernetes 只负责保证 Pod 能够使用到“1 个 CPU”的计算能力。此外,Kubernetes 允许将 CPU 限额设置为分数(k8s通用写法),比如CPU limits 的值为 500m,指的就是 500 millicpu,也就是 0.5 个 CPU 的意思。这样,这个 Pod 就会被分配到 1 个 CPU 一半的计算能力。
对于内存资源来说,它的单位自然就是 bytes。Kubernetes 支持使用 Ei、Pi、Ti、Gi、Mi、Ki(或者 E、P、T、G、M、K)的方式来作为 bytes 的值。比如,Memory requests 的值就是 64MiB 即表示2 的 26 次方 bytes 。这里要注意区分 MiB(mebibyte)和 MB(megabyte)的区别(1Mi=1024*1024;1M=1000*1000)。
limits和requests
思想:容器化作业在提交时所设置的资源边界,并不一定是调度系统所必须严格遵守的,这是因为在实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额。因此,用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。在调度的时候,kube-scheduler 只会按照 requests 的值进行计算。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。
// 字段描述
spec.containers[].resources.limits.cpu
spec.containers[].resources.limits.memory
spec.containers[].resources.requests.cpu
spec.containers[].resources.requests.memory
以上面的示例为例:当指定了 requests.cpu=250m 之后,相当于将 Cgroups 的 cpu.shares 的值设置为 (250/1000)*1024。而当你没有设置 requests.cpu 的时候,cpu.shares 默认则是 1024。这样,Kubernetes 就通过 cpu.shares 完成了对 CPU 时间的按比例分配。而如果指定了 limits.cpu=500m 之后,则相当于将 Cgroups 的 cpu.cfs_quota_us 的值设置为 (500/1000)*100ms,而 cpu.cfs_period_us 的值始终是 100ms。这样,Kubernetes 就为你设置了这个容器只能用到 CPU 的 50%。而对于内存来说,当指定了 limits.memory=128Mi 之后,相当于将 Cgroups 的 memory.limit_in_bytes 设置为 128 * 1024 * 1024。需要注意的是,在调度的时候,调度器只会使用 requests.memory=64Mi 来进行判断。
k8s里的Qos模型
QoS(Quality of Service,服务质量)指一个网络能够利用各种基础技术,为指定的网络通信提供更好的服务能力,是网络的一种安全机制, 是用来解决网络延迟和阻塞等问题的一种技术。<百度百科>
Qos模型分类
- Guaranteed(默认):Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等。或者只设定了limits,没有设定requests。
- Burstable:Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests。
- BestEffort:Pod 既没有设置 requests,也没有设置 limits
Eviction
QoS 划分的主要应用场景,是当宿主机资源紧张的时候,kubelet 对 Pod 进行 Eviction(即资源回收)时需要用到的。当 Kubernetes 所管理的宿主机上不可压缩资源短缺时,就有可能触发 Eviction。比如,可用内存(memory.available)、可用的宿主机磁盘空间(nodefs.available),以及容器运行时镜像存储空间(imagefs.available)等等。
// Kubernetes设置的 Eviction 的默认阈值
memory.available<100Mi
nodefs.available<10%
nodefs.inodesFree<5%
imagefs.available<15%
// 可用kubelet命令配置
kubelet --eviction-hard=imagefs.available<10%,memory.available<500Mi,nodefs.available<5%,nodefs.inodesFree<5% --eviction-soft=imagefs.available<30%,nodefs.available<10% --eviction-soft-grace-period=imagefs.available=2m,nodefs.available=2m --eviction-max-pod-grace-period=600
Eviction 在 Kubernetes 里其实分为 Soft 和 Hard 两种模式。其中,Soft Eviction 允许为 Eviction 过程设置一段“优雅时间”。比如imagefs.available=2m,就意味着当 imagefs 不足的阈值达到 2 分钟之后,kubelet 才会开始 Eviction 的过程。而 Hard Eviction 模式下,Eviction 过程就会在阈值达到之后立刻开始。
Kubernetes 计算 Eviction 阈值的原理:将 Cgroups (limits属性)设置的值和 cAdvisor 监控的数据相比较。当宿主机的 Eviction 阈值达到后,就会进入 MemoryPressure 或者 DiskPressure 状态,从而避免新的 Pod 被调度到这台宿主机上。而当 Eviction 发生的时候,kubelet 具体会挑选哪些 Pod 进行删除操作,就需要参考这些 Pod 的 QoS 类别了。
- 首当其冲的,自然是 BestEffort类别的 Pod。
- 其次,是属于 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。
- 最后,才是 Guaranteed 类别。并且,Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。
对于同 QoS 类别的 Pod 来说,Kubernetes 还会根据 Pod 的优先级来进行进一步地排序和选择。
cpuset的设置
在使用容器的时候,可以通过设置 cpuset 把容器绑定到某个 CPU 的核上(独占),而不是像 cpushare 那样共享 CPU 的计算能力。这种情况下,由于操作系统在 CPU 之间进行上下文切换的次数大大减少,容器里应用的性能会得到大幅提升。事实上,cpuset 方式,是生产环境里部署在线应用类型的 Pod 时,非常常用的一种方式。
cpuset在k8s中的实现如下:
- 首先, Pod 必须是 Guaranteed 的 QoS 类型;
- 然后,只需要将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值即可。
// 示例
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
memory: "200Mi"
cpu: "2"
requests:
memory: "200Mi"
cpu: "2"
如上例所示,这个Pod 会被绑定在 2 个独占的 CPU 核上。当然,具体是哪两个 CPU 核,是由 kubelet 分配的。
小结
- DaemonSet 的 Pod 都设置为 Guaranteed, 避免进入“回收->创建->回收->创建”的“死循环”。(如果ds不设置成guaranteed,那么宿主机资源不够时,为了回收资源和优先级的因素,他会被优先清理,但是ds资源类型比较特殊,仍然会在宿主机重新创建pod,这样循环反复,不停地做无用功。)
(二)、Kubernetes默认调度器
在 Kubernetes 项目中,默认调度器的主要职责,就是为一个新创建出来的 Pod,寻找一个最合适的节点(Node)。
调度流程
- 从集群所有的节点中,根据调度算法挑选出所有可以运行该 Pod 的节点;
- 从第一步的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果。
在具体的调度流程中,默认调度器会首先调用一组叫作 Predicate 的调度算法,来检查每个 Node。然后,再调用一组叫作 Priority 的调度算法,来给上一步得到的结果里的每个 Node 打分。最终的调度结果,就是得分最高的那个 Node。调度器对一个 Pod 调度成功,实际上就是将它的 spec.nodeName 字段填上调度结果的节点名字。
工作原理
Kubernetes 的调度器的核心,实际上就是两个相互独立的控制循环。
第一个循环是Informer Path,主要是启动一系列Informer用来监听(Watch)Etcd中的Pod,Node, Service等与调度相关的API对象的变化。比如,当一个待调度 Pod(即:它的 nodeName 字段是空的)被创建出来之后,调度器就会通过 Pod Informer 的 Handler,将这个待调度 Pod 添加进调度队列。在默认情况下,Kubernetes 的调度队列是一个 PriorityQueue(优先级队列),并且当某些集群信息发生变化的时候,调度器还会对调度队列里的内容进行一些特殊操作。此外,Kubernetes 的默认调度器还要负责对调度器缓存(即:scheduler cache)进行更新。
事实上,Kubernetes 调度部分进行性能优化的一个最根本原则,就是尽最大可能将集群信息 Cache 化,以便从根本上提高 Predicate 和 Priority 调度算法的执行效率。
第二个控制循环是Scheduling Path,主要逻辑是不断从调度队列里出队一个Pod,然后用Predicates算法进行过滤,得到一组可以运行这个Pod的宿主机列表,然后再用Priority打分,得分高的作为调度的结果。调度算法执行完成后,调度器就需要将 Pod 对象的 nodeName 字段的值,修改为上述 Node 的名字(这个步骤在 Kubernetes 里面被称作 Bind)。
特点:
- 算法执行前,Cache 化:默认调度器对 Scheduler Cache 进行更新,同时从 Scheduler Cache 里直接获得 Predicates 算法需要的 Node 信息。
- 算法执行后,乐观绑定:默认调度器在 Bind 阶段,只会更新 Scheduler Cache 里的 Pod 和 Node 的信息(Assume);之后,调度器创建一个 Goroutine 来异步地向 APIServer 发起更新 Pod 的请求,来真正完成 Bind 操作。
- 无锁化:只有对调度队列和 Scheduler Cache 进行操作时,才加锁。算法执行不加锁(在 Scheduling Path 上,调度器会启动多个 Goroutine 以节点为粒度并发执行 Predicates 算法,从而提高这一阶段的执行效率。而与之类似的,Priorities 算法也会以 MapReduce 的方式并行计算然后再进行汇总。而在这些所有需要并发的路径上,调度器会避免设置任何全局的竞争资源,从而免去了使用锁进行同步带来的巨大的性能损耗。)。
可扩展机制
默认调度器的可扩展机制,在 Kubernetes 里面叫作 Scheduler Framework。这个设计的主要目的,就是在调度器生命周期的各个关键点上,为用户暴露出可以进行扩展和实现的接口,从而实现由用户自定义调度器的能力。
上图中,每一个绿色的箭头都是一个可以插入自定义逻辑的接口。比如,上面的 Queue 部分,就意味着可以在这一部分提供一个自己的调度队列的实现,从而控制每个 Pod 开始被调度(出队)的时机。而 Predicates 部分,则意味着可以提供自己的过滤算法实现,根据自己的需求,来决定选择哪些机器。需要注意的是,上述这些可插拔式逻辑,都是标准的 Go 语言插件机制(Go plugin 机制),也就是说,需要在编译的时候选择把哪些插件编译进去。
小结
- 调度的主要目标就是完成节点的筛选,并更新pod 的spec.nodeName
- 几个重点:
- 采用两层循环的作用 降低了系统的复杂度 提高系统的灵活性,通过优先级队列降低模块间的耦合性, 通过缓存提高节点的筛选速度。
- 基于乐观绑定策略(主要绑定在缓存里面) 而不是立即调用远程api-server ,后续会启动一个goroutin 来异步响应api-server 如果异步响应失败的话会在后续缓存同步的过程中pod信息得到矫正
- 调度请求到达节点以后 还会通过kubelet 做最终的验证比如端口占用 资源是否可用等常规校验。
(三)、Kubernetes默认调度器调度策略解析
Predicates
Predicates 在调度过程中的作用,可以理解为 Filter,即:它按照调度策略,从当前集群的所有节点中,“过滤”出一系列符合条件的节点。这些节点,都是可以运行待调度 Pod 的宿主机。
三种默认调度策略
- GeneralPredicates:最基础的调度策略
例如:
PodFitsResources 计算 宿主机的 CPU 和内存资源等是否够用;
PodFitsHost 检查 宿主机的名字是否跟 Pod 的 spec.nodeName 一致;
PodFitsHostPorts 检查 Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突;
PodMatchNodeSelector 检查 Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配。
- 与 Volume 相关的过滤规则:负责的是跟容器持久化 Volume 相关的调度策略
例如:
NoDiskConflict 检查的条件,是多个 Pod 声明挂载的持久化 Volume 是否有冲突;
MaxPDVolumeCountPredicate 检查的条件,是一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目,如果是的话,那么声明使用该类型持久化 Volume 的 Pod 就不能再调度到这个节点了;
VolumeZonePredicate,则是检查持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配;
VolumeBindingPredicate ,它负责检查的,是该 Pod 对应的 PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配。
- 宿主机相关的过滤规则:主要考察待调度 Pod 是否满足 Node 本身的某些条件
例如:
PodToleratesNodeTaints,负责检查的就是我们前面经常用到的 Node 的“污点”机制。只有当 Pod 的 Toleration 字段与 Node 的 Taint 字段能够匹配的时候,这个 Pod 才能被调度到该节点上
NodeMemoryPressurePredicate,检查的是当前节点的内存是不是已经不够充足,如果是的话,那么待调度 Pod 就不能被调度到该节点上
- Pod 相关的过滤规则:跟 GeneralPredicates 大多数是重合的,比较特殊的,是 PodAffinityPredicate
PodAffinityPredicate规则的作用,是检查待调度 Pod 与 Node 上的已有 Pod 之间的亲和(podAffinity)和反亲和(podAntiAffinity)关系。
在具体执行的时候, 当开始调度一个 Pod 时,Kubernetes 调度器会同时启动 16 个 Goroutine,来并发地为集群里的所有 Node 计算 Predicates,最后返回可以运行这个 Pod 的宿主机列表。
Priorities
在 Predicates 阶段完成了节点的“过滤”之后,Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。
Priorities 里最常用到的一个打分规则,是 LeastRequestedPriority。它的计算方法,实际上就是在选择空闲资源(CPU 和 Memory)最多的宿主机。可以简单地总结为如下所示的公式:
// LeastRequestedPriority打分规则计算公式
score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2
BalancedResourceAllocation:选择调度完成后,所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况。
// BalancedResourceAllocation打分规则计算公式
score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10
其中,每种资源的 Fraction 的定义是 :Pod 请求的资源 / 节点上的可用资源。而 variance 算法的作用,则是计算每两种资源 Fraction 之间的“距离”。而最后选择的,则是资源 Fraction 差距最小的节点。
此外,还有 NodeAffinityPriority、TaintTolerationPriority 和 InterPodAffinityPriority 这三种 Priority。它们与前面的 PodMatchNodeSelector、PodToleratesNodeTaints 和 PodAffinityPredicate 这三个 Predicate 的含义和计算方法是类似的。但是作为 Priority,一个 Node 满足上述规则的字段数目越多,它的得分就会越高。
ImageLocalityPriority:如果待调度 Pod 需要使用的镜像很大,并且已经存在于某些 Node 上,那么这些 Node 的得分就会比较高。
小结
- 在实际的执行过程中,调度器里关于集群和 Pod 的信息都已经缓存化,所以这些算法的执行过程还是比较快的。
- 如何配置这些过滤和打分策略?
-- 可以通过为 kube-scheduler 指定一个配置文件或者创建一个 ConfigMap ,来配置哪些规则需要开启、哪些规则需要关闭。并且,你可以通过为 Priorities 设置权重,来控制调度器的调度行为。