解决k8s调度不均衡问题

前言

在近期的工作中,我们发现 k8s 集群中有些节点资源使用率很高,有些节点资源使用率很低,我们尝试重新部署应用和驱逐 Pod,发现并不能有效解决负载不均衡问题。在学习了 Kubernetes 调度原理之后,重新调整了 Request 配置,引入了调度插件,才最终解决问题。这篇就来跟大家分享 Kubernetes 资源和调度相关知识,以及如何解决k8s调度不均衡问题。

Kubernetes 的资源模型

在 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 的值来进行设置。
这是因为在实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额,这种策略能有效的提高整体资源的利用率。

Kubernetes 的服务质量

服务质量 QoS 的英文全称为 Quality of Service。在 Kubernetes 中,每个 Pod 都有个 QoS 标记,通过这个 Qos 标记来对 Pod 进行服务质量管理,它确定 Pod 的调度和驱逐优先级。在 Kubernetes 中,Pod 的 QoS 服务质量一共有三个级别:

  • Guaranteed:当 Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等的时候,这个 Pod 就属于 Guaranteed 类别 。
  • Burstable:而当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests。那么这个 Pod 就会被划分到 Burstable 类别。
  • BestEffort:而如果一个 Pod 既没有设置 requests,也没有设置 limits,那么它的 QoS 类别就是 BestEffort。

具体地说,当 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 值要相等。

Kubernetes 的调度策略

kube-scheduler 是 Kubernetes 集群的默认调度器,它的主要职责是为一个新创建出来的 Pod,寻找一个最合适的 Node。kube-scheduler 给一个 Pod 做调度选择包含三个步骤:

  • 过滤:调用一组叫作 Predicate 的调度算法,将所有满足 Pod 调度需求的 Node 选出来;
  • 打分:调用一组叫作 Priority 的调度算法,给每一个可调度 Node 进行打分;
  • 绑定:调度器将 Pod 对象的 nodeName 字段的值,修改为得分最高的 Node。

Kubernetes 官方过滤和打分编排源码如下:
https://github.com/kubernetes/kubernetes/blob/281023790fd27eec7bfaa7e26ff1efd45a95fb09/pkg/scheduler/framework/plugins/legacy_registry.go

过滤(Predicate)

过滤阶段,首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的 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)

打分阶段,通过 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 参数来开启调度打分日志。

自定义配置

如果官方默认的过滤和打分策略,无法满足实际业务,我们可以自定义配置:

  • 调度策略:允许你修改默认的过滤 断言(Predicates) 和打分 优先级(Priorities) 。
  • 调度配置:允许你实现不同调度阶段的插件, 包括:QueueSort, Filter, Score, Bind, Reserve, Permit 等等。 你也可以配置 kube-scheduler 运行不同的配置文件。

解决k8s调度不均衡问题

一、按实际用量配置 Pod 的 requeste

从上面的调度策略可以得知,资源相关的打分算法 LeastRequestedPriority 和 MostRequestedPriority 都是基于 request 来进行评分,而不是按 Node 当前资源水位进行调度(在没有安装 Prometheus 等资源监控相关组件之前,kube-scheduler 也无法实时统计 Node 当前的资源情况),所以可以动态采 Pod 过去一段时间的资源使用率,据此来设置 Pod 的Request,才能契合 kube-scheduler 默认打分算法,让 Pod 的调度更均衡。

二、为资源占用较高的 Pod 设置反亲和

对一些资源使用率较高的 Pod ,进行反亲和,防止这些项目同时调度到同一个 Node,导致 Node 负载激增。

三、引入实时资源打分插件 Trimaran

但在实际项目中,并不是所有情况都能较为准确的估算出 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。

  • Kubernetes Metrics Server:是 kubernetes 监控体系中的核心组件之一,它负责从 kubelet 收集资源指标,然后对这些指标监控数据进行聚合(依赖kube-aggregator),并在 Kubernetes Apiserver 中通过 Metrics API( /apis/metrics.k8s.io/) 公开暴露它们;
  • Prometheus Server: 是一款基于时序数据库的开源监控告警系统,非常适合 Kubernetes 集群的监控。基本原理是通过 Http 协议周期性抓取被监控组件的状态,任意组件只要提供对应的 Http 接口就可以接入监控。不需要任何 SDK 或者其他的集成过程。这样做非常适合做虚拟化环境监控系统,比如 VM、Docker、Kubernetes 等。官网地址:https://prometheus.io/
  • SignalFx:是一家基础设施及应用实时云监控服务商,它采用了一个低延迟、可扩展的流式分析引擎,以监视微服务(松散耦合、独立部署的应用组件集合)和协调的容器环境(如Kubernetes和Docker)。官网地址:https://www.signalfx.com/

Trimaran 的架构如下:
image.png
可以看到在 kube-scheduler 打分的过程中,Trimaran 会通过 load-watcher 获取当前 node 的实时资源水位,然后据此打分从而干预调度结果。

Trimaran 打分原理:https://github.com/kubernetes-sigs/scheduler-plugins/tree/master/kep/61-Trimaran-real-load-aware-scheduling

四、引入重平衡工具 descheduler

从 kube-scheduler 的角度来看,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定,但调度是静态的,Pod 一旦被绑定了节点是不会触发重新调度的。虽然打分插件可以有效的解决调度时的资源不均衡问题,但每个 Pod 在长期的运行中所占用的资源也是会有变化的(通常内存会增加)。假如一个应用在启动的时候只占 2G 内存,但运行一段时间之后就会占用 4G 内存,如果这样的应用比较多的话,Kubernetes 集群在运行一段时间后就可能会出现不均衡的状态,所以需要重新平衡集群。
除此之外,也还有一些其他的场景需要重平衡:

  • 集群添加新节点,一些节点不足或过度使用;
  • 某些节点发生故障,其pod已移至其他节点;
  • 原始调度决策不再适用,因为在节点中添加或删除了污点或标签,不再满足 pod/node 亲和性要求。

当然我们可以去手动做一些集群的平衡,比如手动去删掉某些 Pod,触发重新调度就可以了,但是显然这是一个繁琐的过程,也不是解决问题的方式。为了解决实际运行中集群资源无法充分利用或浪费的问题,可以使用 descheduler 组件对集群的 Pod 进行调度优化,descheduler 可以根据一些规则和配置策略来帮助我们重新平衡集群状态,其核心原理是根据其策略配置找到可以被移除的 Pod 并驱逐它们,其本身并不会进行调度被驱逐的 Pod,而是依靠默认的调度器来实现,descheduler 重平衡原理可参见官网。

descheduler 官网地址:https://github.com/kubernetes-sigs/descheduler

参考资料

posted on 2022-06-19 23:11  劼哥stone  阅读(303)  评论(0编辑  收藏  举报

导航