在近期的工作中,我们发现 k8s 集群中有些节点资源使用率很高,有些节点资源使用率很低,我们尝试重新部署应用和驱逐 Pod,发现并不能有效解决负载不均衡问题。在学习了 Kubernetes 调度原理之后,重新调整了 Request 配置,引入了调度插件,才最终解决问题。这篇就来跟大家分享 Kubernetes 资源和调度相关知识,以及如何解决k8s调度不均衡问题。
在 Kubernetes 里,Pod 是最小的原子调度单位。这也就意味着,所有跟调度和资源管理相关的属性都应该是属于 Pod 对象的字段。而这其中最重要的部分,就是 Pod 的 CPU 和内存配置。像 CPU 这样的资源被称作“可压缩资源”(compressible resources)。它的典型特点是,当可压缩资源不足时,Pod 只会“饥饿”,但不会退出。而像内存这样的资源,则被称作“不可压缩资源(incompressible resources)。当不可压缩资源不足时,Pod 就会因为 OOM(Out-Of-Memory)被内核杀掉。Pod 可以由多个 Container 组成,所以 CPU 和内存资源的限额,是要配置在每个 Container 的定义上的。这样,Pod 整体的资源配置,就由这些 Container 的配置值累加得到。Kubernetes 里 Pod 的 CPU 和内存资源,实际上还要分为 limits 和 requests 两种情况:
spec.containers[].resources.limits.cpu
spec.containers[].resources.limits.memory
spec.containers[].resources.requests.cpu
spec.containers[].resources.requests.memory
这两者的区别其实非常简单:在调度的时候,kube-scheduler 只会按照 requests 的值进行调度。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。这是因为在实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额,这种策略能有效的提高整体资源的利用率。
服务质量 QoS 的英文全称为 Quality of Service。在 Kubernetes 中,每个 Pod 都有个 QoS 标记,通过这个 Qos 标记来对 Pod 进行服务质量管理,它确定 Pod 的调度和驱逐优先级。在 Kubernetes 中,Pod 的 QoS 服务质量一共有三个级别:
具体地说,当 Kubernetes 所管理的宿主机上不可压缩资源短缺时,就有可能触发 Eviction 驱逐。目前,Kubernetes 为你设置的 Eviction 的默认阈值如下所示:
memory.available<100Mi
nodefs.available<10%
nodefs.inodesFree<5%
imagefs.available<15%
当宿主机的 Eviction 阈值达到后,就会进入 MemoryPressure 或者 DiskPressure 状态,从而避免新的 Pod 被调度到这台宿主机上,然后 kubelet 会根据 QoS 的级别来挑选 Pod 进行驱逐,具体驱逐优先级是:BestEffort -> Burstable -> Guaranteed。QoS 的级别是通过 Linux 内核 OOM 分数值来实现的,OOM 分数值取值范围在-1000 ~1000之间。在 Kubernetes 中,常用服务的 OOM 的分值如下:
-1000 => sshd等进程
-999 => Kubernetes 管理进程
-998 => Guaranteed Pod
0 => 其他进程 0
2~999 => Burstable Pod
1000 => BestEffort Pod
OOM 分数越高,就代表这个 Pod 的优先级越低,在出现资源竞争的时候,就越早被杀掉,分数为-999和-1000的进程永远不会因为 OOM 而被杀掉。
划重点:如果期望 Pod 尽可能的不被驱逐,就应当把 Pod 里的每一个 Container 的 requests 和 limits 都设置齐全,并且 requests 和 limits 值要相等。
kube-scheduler 是 Kubernetes 集群的默认调度器,它的主要职责是为一个新创建出来的 Pod,寻找一个最合适的 Node。kube-scheduler 给一个 Pod 做调度选择包含三个步骤:
Kubernetes 官方过滤和打分编排源码如下:https://github.com/kubernetes/kubernetes/blob/281023790fd27eec7bfaa7e26ff1efd45a95fb09/pkg/scheduler/framework/plugins/legacy_registry.go
过滤阶段,首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的 Node 将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。调度器会根据限制条件和复杂性依次进行以下过滤检查,检查顺序存储在一个名为 PredicateOrdering() 的函数中,具体如下表格:
算法名称 | 默认 | 顺序 | 详细说明 |
---|---|---|---|
CheckNodeUnschedulablePred | 强制 | 1 | 检查节点是否可调度; |
GeneralPred | 是 | 2 | 是一组联合检查,包含了:HostNamePred、PodFitsResourcesPred、PodFitsHostPortsPred、MatchNodeSelectorPred 4个检查 |
HostNamePred | 否 | 3 | 检查 Pod 指定的 Node 名称是否和 Node 名称相同; |
PodFitsHostPortsPred | 否 | 4 | 检查 Pod 请求的端口(网络协议类型)在节点上是否可用; |
MatchNodeSelectorPred | 否 | 5 | 检查是否匹配 NodeSelector 节点选择器的设置; |
PodFitsResourcesPred | 否 | 6 | 检查节点的空闲资源(例如,CPU 和内存)是否满足 Pod 的要求; |
NoDiskConflictPred | 是 | 7 | 根据 Pod 请求的卷是否在节点上已经挂载,评估 Pod 和节点是否匹配; |
PodToleratesNodeTaintsPred | 强制 | 8 | 检查 Pod 的容忍是否能容忍节点的污点; |
CheckNodeLabelPresencePred | 否 | 9 | 检测 NodeLabel 是否存在; |
CheckServiceAffinityPred | 否 | 10 | 检测服务的亲和; |
MaxEBSVolumeCountPred | 是 | 11 | 已废弃,检测 Volume 数量是否超过云服务商 AWS 的存储服务的配置限制; |
MaxGCEPDVolumeCountPred | 是 | 12 | 已废弃,检测 Volume 数量是否超过云服务商 Google Cloud 的存储服务的配置限制; |
MaxCSIVolumeCountPred | 是 | 13 | Pod 附加 CSI 卷的数量,判断是否超过配置的限制; |
MaxAzureDiskVolumeCountPred | 是 | 14 | 已废弃,检测 Volume 数量是否超过云服务商 Azure 的存储服务的配置限制; |
MaxCinderVolumeCountPred | 否 | 15 | 已废弃,检测 Volume 数量是否超过云服务商 OpenStack 的存储服务的配置限制; |
CheckVolumeBindingPred | 是 | 16 | 基于 Pod 的卷请求,评估 Pod 是否适合节点,这里的卷包括绑定的和未绑定的 PVC 都适用; |
NoVolumeZoneConflictPred | 是 | 17 | 给定该存储的故障区域限制, 评估 Pod 请求的卷在节点上是否可用; |
EvenPodsSpreadPred | 是 | 18 | 检测 Node 是否满足拓扑传播限制; |
MatchInterPodAffinityPred | 是 | 19 | 检测是否匹配 Pod 的亲和与反亲和的设置; |
可以看出,Kubernetes 正在逐步移除某个具体云服务商的服务的相关代码,而使用接口(Interface)来扩展功能。
打分阶段,通过 Priority 策略对可用节点进行评分,最终选出最优节点。具体是用一组打分函数处理每一个可用节点,每一个打分函数会返回一个 0~100 的分数,分数越高表示节点越优, 同时每一个函数也会对应一个权重值。将每个打分函数的计算得分乘以权重,然后再将所有打分函数的得分相加,从而得出节点的最终优先级分值。权重可以让管理员定义优选函数倾向性的能力,其计算优先级的得分公式如下:
finalScoreNode = (weight1 * priorityFunc1) + (weight2 * priorityFunc2) + … + (weightn * priorityFuncn)
全部打分函数如下表格所示:
算法名称 | 默认 | 权重 | 详细说明 |
---|---|---|---|
EqualPriority | 否 | - | 给予所有节点相等的权重; |
MostRequestedPriority | 否 | - | 支持最多请求资源的节点。该策略将 Pod 调度到整体工作负载所需的最少的一组节点上; |
RequestedToCapacityRatioPriority | 否 | - | 使用默认的打分方法模型,创建基于 ResourceAllocationPriority 的 requestedToCapacity; |
SelectorSpreadPriority | 是 | 1 | 属于同一 Service、 StatefulSet 或 ReplicaSet 的 Pod,尽可能地跨 Node 部署(鸡蛋不要只放在一个篮子里,分散风险,提高可用性); |
ServiceSpreadingPriority | 否 | - | 对于给定的 Service,此策略旨在确保该 Service 关联的 Pod 在不同的节点上运行。它偏向把 Pod 调度到没有该服务的节点。整体来看,Service 对于单个节点故障变得更具弹性; |
InterPodAffinityPriority | 是 | 1 | 实现了 Pod 间亲和性与反亲和性的优先级; |
LeastRequestedPriority | 是 | 1 | 偏向最少请求资源的节点。换句话说,节点上的 Pod 越多,使用的资源就越多,此策略给出的排名就越低; |
BalancedResourceAllocation | 是 | 1 | CPU和内存使用率越接近的节点权重越高,该策略不能单独使用,必须和 LeastRequestedPriority 组合使用,尽量选择在部署Pod后各项资源更均衡的机器。 |
NodePreferAvoidPodsPriority | 是 | 10000 | 根据节点的注解 scheduler.alpha.kubernetes.io/preferAvoidPods 对节点进行优先级排序。你可以使用它来暗示两个不同的 Pod 不应在同一节点上运行; |
NodeAffinityPriority | 是 | 1 | 根据节点亲和中 PreferredDuringSchedulingIgnoredDuringExecution 字段对节点进行优先级排序; |
TaintTolerationPriority | 是 | 1 | 根据节点上无法忍受的污点数量,给所有节点进行优先级排序。此策略会根据排序结果调整节点的等级; |
ImageLocalityPriority | 是 | 1 | 如果Node上存在Pod容器部分所需镜像,则根据这些镜像的大小来决定分值,镜像越大,分值就越高; |
EvenPodsSpreadPriority | |||
是 | 2 | 实现了 Pod 拓扑扩展约束的优先级排序; |
我自己遇到的是“多节点调度资源不均衡问题”,所以跟节点资源相关的打分算法是我关注的重点。1、BalancedResourceAllocation(默认开启),它的计算公式如下所示:
score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10
其中,每种资源的 Fraction 的定义是 :Pod 的 request 资源 / 节点上的可用资源。而 variance 算法的作用,则是计算每两种资源 Fraction 之间的“距离”。而最后选择的,则是资源 Fraction 差距最小的节点。所以说,BalancedResourceAllocation 选择的,其实是调度完成后,所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况。2、LeastRequestedPriority(默认开启),它的计算公式如下所示:
score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2
可以看到,这个算法实际上是根据 request 来计算出空闲资源(CPU 和 Memory)最多的宿主机。3、MostRequestedPriority(默认不开启),它的计算公式如下所示:
score = (cpu(10 sum(requested) / capacity) + memory(10 sum(requested) / capacity)) / 2
在 ClusterAutoscalerProvider 中替换 LeastRequestedPriority,给使用多资源的节点更高的优先级。
你可以修改 /etc/kubernetes/manifests/kube-scheduler.yaml 配置,新增 v=10 参数来开启调度打分日志。
如果官方默认的过滤和打分策略,无法满足实际业务,我们可以自定义配置:
从上面的调度策略可以得知,资源相关的打分算法 LeastRequestedPriority 和 MostRequestedPriority 都是基于 request 来进行评分,而不是按 Node 当前资源水位进行调度(在没有安装 Prometheus 等资源监控相关组件之前,kube-scheduler 也无法实时统计 Node 当前的资源情况),所以可以动态采 Pod 过去一段时间的资源使用率,据此来设置 Pod 的Request,才能契合 kube-scheduler 默认打分算法,让 Pod 的调度更均衡。
对一些资源使用率较高的 Pod ,进行反亲和,防止这些项目同时调度到同一个 Node,导致 Node 负载激增。
但在实际项目中,并不是所有情况都能较为准确的估算出 Pod 资源用量,所以依赖 request 配置来保障 Pod 调度的均衡性是不准确的。那有没有一种通过 Node 当前实时资源进行打分调度的方案呢?Kubernetes 官方社区 SIG 小组提供的调度插件 Trimaran 就具备这样的能力。
Trimaran 官网地址:https://github.com/kubernetes-sigs/scheduler-plugins/tree/master/pkg/trimaran
Trimaran 是一个实时负载感知调度插件,它利用 load-watcher 获取程序资源利用率数据。目前,load-watcher支持三种度量工具:Metrics Server、Prometheus 和 SignalFx。
Trimaran 的架构如下:可以看到在 kube-scheduler 打分的过程中,Trimaran 会通过 load-watcher 获取当前 node 的实时资源水位,然后据此打分从而干预调度结果。
Trimaran 打分原理:https://github.com/kubernetes-sigs/scheduler-plugins/tree/master/kep/61-Trimaran-real-load-aware-scheduling
从 kube-scheduler 的角度来看,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定,但调度是静态的,Pod 一旦被绑定了节点是不会触发重新调度的。虽然打分插件可以有效的解决调度时的资源不均衡问题,但每个 Pod 在长期的运行中所占用的资源也是会有变化的(通常内存会增加)。假如一个应用在启动的时候只占 2G 内存,但运行一段时间之后就会占用 4G 内存,如果这样的应用比较多的话,Kubernetes 集群在运行一段时间后就可能会出现不均衡的状态,所以需要重新平衡集群。除此之外,也还有一些其他的场景需要重平衡:
当然我们可以去手动做一些集群的平衡,比如手动去删掉某些 Pod,触发重新调度就可以了,但是显然这是一个繁琐的过程,也不是解决问题的方式。为了解决实际运行中集群资源无法充分利用或浪费的问题,可以使用 descheduler 组件对集群的 Pod 进行调度优化,descheduler 可以根据一些规则和配置策略来帮助我们重新平衡集群状态,其核心原理是根据其策略配置找到可以被移除的 Pod 并驱逐它们,其本身并不会进行调度被驱逐的 Pod,而是依靠默认的调度器来实现,descheduler 重平衡原理可参见官网。
descheduler 官网地址:https://github.com/kubernetes-sigs/descheduler