资源分配不均匀问题
简述
资源相关的打分算法 LeastRequestedPriority 和 MostRequestedPriority 都是基于 request 来进行评分,而不是按 Node 当前资源水位进行调度(在没有安装 Prometheus/Metrics 等资源监控相关组件之前,kube-scheduler 也无法实时统计 Node 当前的资源情况)。
简单来说,k8s在进行调度时,计算的就是requests的值,不管你limits设置多少,k8s都不关心。所以当这个值没有达到资源瓶颈时,理论上,该节点就会一直有pod调度上去。
综上所述,在实际场景就可能会遇到以下几种情况
- 经常在 K8s 集群种部署负载的时候不设置 CPU
requests
(这样“看上去”就可以在每个节点上容纳更多 Pod )。在业务比较繁忙的时候,节点的 CPU 全负荷运行。业务延迟明显增加,有时甚至机器会莫名其妙地进入 CPU 软死锁等“假死”状态。 - 在 K8s 集群中,集群负载并不是完全均匀地在节点间分配的,通常内存不均匀分配的情况较为突出,集群中某些节点的内存使用率明显高于其他节点。
- cpu 负载较低的Node cpu,内存,磁盘都很充足,按理说肯定会通过过滤筛选,而且分值会很高,应该会最先调度,但实际情况却没有。所以这个时候就会出现调度不均衡的问题。
- 如果在业务高峰时间遇到上述问题,可能就会导致节点集群节点资源打满,并且出现机器已经 hang 住甚至无法远程 ssh 登陆。那么通常留给集群管理员的只剩下重启集群这一个选项(资源分配不均匀的情况在节点资源各不相同的时候更为明显)。
解决办法?
预留部分系统资源,保证集群稳定性
kubelet 具有以下默认硬驱逐条件:
memory.available<100Mi
nodefs.available<10%
imagefs.available<15%
nodefs.inodesFree<5%
(Linux 节点)
参考:https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/
可以按照官方文档编辑 Kubelet 配置文件所述,预留一部分系统资源,从而保证当可用计算资源较少时 kubelet 所在节点的稳定性。 这在处理如内存和硬盘之类的不可压缩资源时尤为重要。
当为 kubelet 配置驱逐策略时, 应该确保调度程序不会在 Pod 触发驱逐时对其进行调度,因为这类 Pod 会立即引起内存压力。
考虑以下场景:
- 节点内存容量:
10Gi
- 操作员希望为系统守护进程(内核、
kubelet
等)保留 10% 的内存容量 - 操作员希望在节点内存利用率达到 95% 以上时驱逐 Pod,以减少系统 OOM 的概率。
为此,kubelet 启动设置如下:
plaintext1
2--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi在此配置中,
--system-reserved
标志为系统预留了1.5Gi
的内存, 即总内存的 10% + 驱逐条件量
。如果 Pod 使用的内存超过其请求值或者系统使用的内存超过
1Gi
, 则节点可以达到驱逐条件,这使得memory.available
信号低于500Mi
并触发条件。eviction-hard=memory.available ~=
10 * 1024 - 10 * 1024 * 95%
system-reserved=memory ~=
10 * 1024 - 10 * 1024 * 95% + 10 * 1024 * 0.1
- 节点内存容量:
限制/分配服务资源
采取服务资源使用情况
动态采 Pod 过去一段时间的资源使用率,据此来设置 Pod 的Request,才能契合 kube-scheduler 默认打分算法,让 Pod 的调度更均衡。
为每一个pod设置requests和limits
- 不同 QoS 的 Pod 具有不同的 OOM 分数,当出现资源不足时,集群会优先 Kill 掉
Best-Effort
类型的 Pod ,其次是Burstable
类型的 Pod ,最后是Guaranteed
类型的 Pod 。 - 因此,如果资源充足,可将 QoS pods 类型均设置为
Guaranteed
。用计算资源换业务性能和稳定性,减少排查问题时间和成本。当然如果想更好的提高资源利用率,可以设置核心业务服务为Guaranteed
,而其他服务根据重要程度可分别设置为Burstable
或Best-Effort
。
- 不同 QoS 的 Pod 具有不同的 OOM 分数,当出现资源不足时,集群会优先 Kill 掉
为资源占用较高的 Pod 设置反亲和
- 对一些资源使用率较高的 Pod ,做反亲和配置,防止这些项目同时调度到同一个 Node,导致 Node 负载激增。
实践 - 限制/分配服务资源
分析
对于一些关键的业务容器,通常其流量和负载相比于其他 pod 都是比较高的,对于这类容器的requests
和limits
需要具体问题具体分析。
分析的维度是多个方面的,例如该业务容器是 CPU 密集型的,还是 IO 密集型的。是单点的还是高可用的,这个服务的上游和下游是谁等等。
另一方面,在生产环境中这类业务容器的负载从一个比较长的时间维度看的话,往往是具有周期性的。
因此,业务容器的历史监控数据可以在参数设置方面提供重要的参考价值。
横向涵盖 CPU ,内存,网络,存储等。一般,requests
值可以设定为历史数据的均值,而limits
要大于历史数据的均值,当然最终数值还需要结合具体情况做一些小的调整。
估算集群内pod预期资源指标
背景:例如我们的服务基本都是在10-11点/13-15点进入流量高峰期
计划:
- 取这个时间段每个服务的资源占用
- 计算其最大cpu/memory,以及平均cpu/memory
- 指标数量的跨度尽量大些,以下案例为每5分钟一次,时间跨度大于两周以上
- 利用
crontab
,每5分钟1次,取其指标值。将所有文件放在D:\\limit-resource
1 | */5 10-11 * * * kubectl top pods -n {NAMESPACE}|grep -v NAME|sort -nrk 3 > /root/limit-resource/resource-$(date +\%Y\%m\%d\%H\%M\%S).txt |
- 计算
非核心服务:设置
limits
设置为历史数据最高点的均值,设置requests
为limits
的 0.6 - 0.9 倍核心服务:设置
requests
和limits
均设置为历史数据最高点的均值
1 | /** |
问题
在配置完resources后,可能会产生一个新问题:集群预留资源和实际使用情况完全不成正比,造成了资源浪费。如下图

出现这个问题的原因有几个:
- 有些服务在启动时可能会吃掉400m cpu,但运行起来可能高峰也就200m。简单说就是设置小了跑不起来,设置大了浪费。
- 可能有些cpu密集型服务在某个高峰点可以吃掉1000m cpu,但它不会一直消耗这么多(可能一周有这么几回吃到1000m),这就导致计算得到的数值偏大。
新方案 - 延申
在实际项目中,并不是所有情况都能较为准确的估算出 Pod 资源用量,所以依赖 request 配置来保障 Pod 调度的均衡性不是太过准确。
延申1 - 实时资源打分插件 Trimaran
简述
Trimaran 官网地址:https://github.com/kubernetes-sigs/scheduler-plugins/tree/master/pkg/trimaran
Trimaran 是一个实时负载感知调度插件,它利用 load-watcher 获取程序资源利用率数据,可以通过 Node 当前实时资源进行打分调度。目前,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
准备集群
k8s version:v1.24.16 + k3s1,安装略
scheduler-plugins/trimaran/load-watcher
scheduler-plugins提供了两种使用方式:
安装scheduler-plugins binary,这里安装方案也提供了两种,参考:https://github.com/kubernetes-sigs/scheduler-plugins/blob/release-1.24/doc/install.md
作为第二套scheduler,与默认的kube-scheduler共存于集群(使用helm chart安装)
如果网络环境不允许连接外网,可参考如下方式,换第三方源
bash1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26# 替换 k8s.m.daocloud.io 镜像源
[root@k3s-node1 charts]# crictl pull k8s.m.daocloud.io/scheduler-plugins/kube-scheduler:v0.24.9
[root@k3s-node1 charts]# crictl pull k8s.m.daocloud.io/scheduler-plugins/controller:v0.24.9
# retag
[root@k3s-node1 charts]# ctr images tag k8s.m.daocloud.io/scheduler-plugins/kube-scheduler:v0.24.9 registry.k8s.io/scheduler-plugins/kube-scheduler:v0.24.9
[root@k3s-node1 charts]# ctr images tag k8s.m.daocloud.io/scheduler-plugins/controller:v0.24.9 registry.k8s.io/scheduler-plugins/controller:v0.24.9
# 检查
[root@k3s-node1 charts]# crictl images
IMAGE TAG IMAGE ID SIZE
docker.io/rancher/klipper-helm v0.8.0-build20230510 6f42df210d7fa 95MB
docker.io/rancher/klipper-lb v0.4.4 af74bd845c4a8 4.92MB
docker.io/rancher/local-path-provisioner v0.0.24 b29384aeb4b13 14.9MB
docker.io/rancher/mirrored-coredns-coredns 1.10.1 ead0a4a53df89 16.2MB
docker.io/rancher/mirrored-library-traefik 2.9.10 d1e26b5f8193d 39.6MB
docker.io/rancher/mirrored-metrics-server v0.6.3 817bbe3f2e517 29.9MB
docker.io/rancher/mirrored-pause 3.6 6270bb605e12e 301kB
docker.io/wangxiaowu950330/load-watcher 0.2.3 53b2340fed4ce 21.9MB
docker.io/wangxiaowu950330/trimaran 1.24 3fc0109c9b9cb 377MB
k8s.m.daocloud.io/scheduler-plugins/controller v0.24.9 b7e8f1d464e7c 15.7MB
registry.k8s.io/scheduler-plugins/controller v0.24.9 b7e8f1d464e7c 15.7MB
k8s.m.daocloud.io/scheduler-plugins/kube-scheduler v0.24.9 b8fa20c9c006d 19MB
registry.k8s.io/scheduler-plugins/kube-scheduler v0.24.9 b8fa20c9c006d 19MB
[root@k3s-node1 charts]# kubectl get pods -n scheduler-plugins -w
NAME READY STATUS RESTARTS AGE
scheduler-plugins-controller-859bfc6f78-ljkq6 1/1 Running 0 46s
scheduler-plugins-scheduler-6555ff78d7-p4bz4 1/1 Running 0 38s替换掉默认的kube-scheduler
我们也可以选择将load-watcher作为独立服务部署,然后自行构建kube-scheduler/trimaran/load-watcher组件,参考:https://github.com/kubernetes-sigs/scheduler-plugins/blob/release-1.24/pkg/trimaran/README.md。最终架构如图:
这里选择第二种使用方式
构建load-watcher镜像(wangxiaowu950330/load-watcher)
参考:https://github.com/paypal/load-watcher
- 下载源码
1 | git clone https://github.com/paypal/load-watcher.git \ |
- 执行
1 | docker build -t load-watcher:<version> . |
构建kube-scheduler镜像(wangxiaowu950330/trimaran)
- 下载源码
1 | git clone https://github.com/kubernetes-sigs/scheduler-plugins \ |
- Makefile.tm
1 | COMMONENVVAR=GOOS=$(shell uname -s | tr A-Z a-z) GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) |
- Dockerfile
1 | FROM golang:1.17.3 |
- 执行
1 | docker build -t trimaran . |
部署
serviceaccount
1 | # 这里以admin sa进行测试部署 |
trimaran-cm.yaml
1 | apiVersion: v1 |
load-watcher.yaml
1 | apiVersion: apps/v1 |
trimaran-scheduler.yaml
1 | apiVersion: apps/v1 |
测试
创建 指定 scheduler 为trimaran的pod
1 | kubectl apply -f - <<EOF |
验证
1 | - 执行资源的创建/删除 |

延申2 - 重平衡工具 descheduler
从 kube-scheduler 的角度来看,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定,但调度是静态的,Pod 一旦被绑定了节点是不会触发重新调度的。虽然打分插件可以有效的解决调度时的资源不均衡问题,但每个 Pod 在长期的运行中所占用的资源也是会有变化的(通常内存会增加)。假如一个应用在启动的时候只占 2G 内存,但运行一段时间之后就会占用 4G 内存,如果这样的应用比较多的话,Kubernetes 集群在运行一段时间后就可能会出现不均衡的状态,所以需要重新平衡集群。
除此之外,也还有一些其他的场景需要重平衡:
- 集群添加新节点,一些节点不足或过度使用;
- 某些节点发生故障,其pod已移至其他节点;
- 原始调度决策不再适用,因为在节点中添加或删除了污点或标签,不再满足 pod/node 亲和性要求。
当然我们可以去手动做一些集群的平衡,比如手动去删掉某些 Pod,触发重新调度就可以了,但是显然这是一个繁琐的过程,也不是解决问题的方式。为了解决实际运行中集群资源无法充分利用或浪费的问题,可以使用 descheduler 组件对集群的 Pod 进行调度优化,descheduler 可以根据一些规则和配置策略来帮助我们重新平衡集群状态,其核心原理是根据其策略配置找到可以被移除的 Pod 并驱逐它们,其本身并不会进行调度被驱逐的 Pod,而是依靠默认的调度器来实现,descheduler 重平衡原理可参见官网。
策略介绍
策略 | 描述 |
---|---|
RemoveDuplicates | 将节点上同类型的Pod进行迁移,确保只有一个Pod与同一节点上运行的ReplicaSet、Replication Controller、StatefulSet或者Job关联。 |
LowNodeUtilization | 将 requests 比率较高节点上的Pod进行迁移。 |
HighNodeUtilization | 将 requests 比率较低节点上的Pod进行迁移。 |
RemovePodsViolatingInterPodAntiAffinity | 将不满足反亲和性的Pod进行迁移。 |
RemovePodsViolatingNodeAffinity | 将不满足节点节点亲和性策略的Pod进行迁移。 |
RemovePodsViolatingNodeTaints | 将不满足节点污点策略的Pod进行迁移。 |
RemovePodsViolatingTopologySpreadConstraint | 将不满足拓扑分布约束的Pod进行迁移。 |
RemovePodsHavingTooManyRestarts | 将重启次数过多的Pod进行迁移。 |
PodLifeTime | 将运行时间较长的Pod进行迁移。 |
RemoveFailedPods | 将运行失败的Pod进行迁移。 |
主要记录下两个较为重要的策略
LowNodeUtilization
1 | # 如果使用率均在targetThresholds和thresholds之间,则被认为已得到适当利用,并且不被考虑驱逐 |
HighNodeUtilization
1 | apiVersion: "descheduler/v1alpha1" |
驱逐 pod - 遵循机制
当 Descheduler 调度器决定于驱逐 pod 时,它将遵循下面的机制:
- Critical pods (with annotations scheduler.alpha.kubernetes.io/critical-pod) are never evicted:关键型 pod(annotations带有scheduler.alpha.kubernetes.io/critical-pod属性的pod)永远不会被驱逐。
- Pods (static or mirrored pods or stand alone pods) not part of an RC, RS, Deployment or Jobs are never evicted because these pods won’t be recreated: 不属于RC,RS,部署或作业的Pod(静态或镜像pod或独立pod)永远不会被驱逐,因为这些pod不会被重新创建。
- Pods associated with DaemonSets are never evicted: 与 DaemonSets 关联的 Pod 永远不会被驱逐。
- Pods with local storage are never evicted: 具有本地存储的 Pod 永远不会被驱逐。
- BestEffort pods are evicted before Burstable and Guaranteed pods QoS: 等级为 BestEffort 的 pod 将会在等级为 Burstable 和 Guaranteed 的 pod 之前被驱逐。
部署
为了避免被自己驱逐,Descheduler 将会以 关键型 pod 运行,因此它只能被创建建到 kube-system
namespace 内。 关于 Critical pod 的介绍参考:Guaranteed Scheduling For Critical Add-On Pods
资源文件
如果镜像源无法访问,可修改为
lank8s.cn/descheduler/descheduler:xxx
- 使用deployment进行部署
- 使用cronjob进行部署
- 使用Job进行部署
测试验证
启动之后,可以来验证下descheduler是否启动成功
1 | # kubectl get pod -n kube-system | grep descheduler |
再来验证下pod是否分布均匀

可以看到,目前node02这个节点的pod数是20个,相比较其他节点,还是差了几个,那么我们只对pod数量做重平衡的话,可以注释掉关于cpu和内存的配置项(默认是)
1 | # cat kubernetes/base/configmap.yaml |
修改完成后,重启下即可
1 | kubectl apply -f kubernetes/base/configmap.yaml |
然后,看下Descheduler的调度日志
1 | # kubectl logs -n kube-system descheduler-job-9rc9h |
通过这个日志,可以看到Node “k8s-node01” is over utilized
,然后就是有提示evicting pods from node “k8s-node01”
,这就说明,Descheduler已经在重新调度了,最终调度结果如下:

PDB
由于使用 descheduler
会将 Pod 驱逐进行重调度,但是如果一个服务的所有副本都被驱逐的话,则可能导致该服务不可用。如果服务本身存在单点故障,驱逐的时候肯定就会造成服务不可用了,这种情况我们强烈建议使用反亲和性和多副本来避免单点故障,但是如果服务本身就被打散在多个节点上,这些 Pod 都被驱逐的话,这个时候也会造成服务不可用了,这种情况下我们可以通过配置 PDB(PodDisruptionBudget)
对象来避免所有副本同时被删除,比如我们可以设置在驱逐的时候某应用最多只有一个副本不可用,则创建如下所示的资源清单即可:
1 | apiVersion: policy/v1 |
关于 PDB 的更多详细信息可以查看官方文档:https://kubernetes.io/docs/tasks/run-application/configure-pdb/。
所以如果我们使用 descheduler
来重新平衡集群状态,那么强烈建议给应用创建一个对应的 PodDisruptionBudget
对象进行保护。
问题
Descheduler中以 Remove 开头的几个策略其实在大部分场景是不需要的,集群内的资源完全可以通过配置反亲和性策略将同一 workload 下所有 Pod 打散。
LowNodeUtilization 和 HighNodeUtilization 这两种策略都是根据取决于pod的请求和限制,而非node实际使用情况。但在大部分生产环境下都更希望使用 node 真实使用率来进行 Pod 迁移。
但是看社区已在做 TargetLoadPacking 插件的支持,相信不久后就可以仅利用Descheduler实现真实资源使用情况检测,链接: