调度均衡:Descheduler 与资源碎片整理

系统讲解 Kubernetes 的调度再平衡:为什么调度是一次性决策、Descheduler 的架构与策略清单、资源碎片与装箱问题、低利用率节点与重复副本策略、亲和性与拓扑再平衡、与 Cluster Autoscaler 的协同,以及驱逐预算、安全阈值与效果度量的生产实践。

kube-scheduler 只在 Pod 创建时做一次决策,之后无论集群如何变化都不会重新调度:节点扩了又缩、副本被驱逐后挤在少数节点、某个节点长期只有 5% 利用率,调度器都不会主动纠正。Descheduler 就是补上这一环——它周期性扫描集群,识别不合理的分布,通过驱逐触发调度器重新安置 Pod。本文覆盖策略清单、碎片整理、与 Cluster Autoscaler 的协同,以及驱逐预算与安全阈值。


目录


1. 为什么需要再平衡

1.1 调度是一次性决策

kube-scheduler 的职责是「为新 Pod 找节点」,它不会重新评估已经 Running 的 Pod。这带来几类必然出现的失衡:

场景一:节点池缩容后,剩余节点负载不均
场景二:批量驱逐(如节点维护)后,Pod 全部挤到少数节点
场景三:长期运行后,新老 Pod 混布导致资源碎片
场景四:拓扑约束与实时负载不匹配(某区 Pod 过密)
场景五:节点利用率极低但无法缩容(有不可驱逐的 Pod)

1.2 失衡的代价

失衡类型代价
节点利用率不均无法缩容,成本浪费
副本集中在少数节点单节点故障影响面放大
跨区分布不均跨区流量增加
碎片化大规格 Pod 无法调度

1.3 为什么不能靠「重启 Pod」

手工删除 Pod 依赖调度器重新决策,短期有效但不可持续,而且没有任何安全阀:可能一次驱逐太多、可能把有状态 Pod 打散、可能忽略 PDB。Descheduler 把这些约束固化成策略与阈值。


2. Descheduler 架构与策略

2.1 运行模式

Descheduler 以 CronJob 或 Deployment 形式运行(推荐 CronJob,每 5~30 分钟一次),单次运行完成「扫描 → 筛选 → 驱逐」三步:

1. 列举所有 Node 与 Pod
2. 对每个策略生成候选驱逐列表
3. 过滤:PDB、优先级、本地存储、镜像亲和、守护进程
4. 执行 Eviction API(走 PDB 与准入)
5. 记录指标与事件,退出

关键点:Descheduler 不调度,它只驱逐。被驱逐的 Pod 由 kube-scheduler 重新安置,因此结果仍受调度约束保护。

2.2 策略清单

策略作用
RemoveDuplicates同一 ReplicaSet 的多副本挤在同一节点
LowNodeUtilization节点利用率过低或过高
HighNodeUtilization提高装箱率,让节点更满
RemovePodsViolatingInterPodAntiAffinity违反 Pod 反亲和
RemovePodsViolatingNodeAffinity违反节点亲和
RemovePodsViolatingTopologySpreadConstraint违反拓扑打散
RemovePodsHavingTooManyRestarts重启次数过多
PodLifeTime存活时间过长(滚动重启)
RemoveFailedPods清理失败 Pod

2.3 最小配置

apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
  - name: default
    pluginConfig:
      - name: DefaultEvictor
        args:
          evictSystemCriticalPods: false
          ignorePvcPods: true
          nodeFit: true
      - name: LowNodeUtilization
        args:
          thresholds: {cpu: 20, memory: 20, pods: 20}
          targetThresholds: {cpu: 50, memory: 50, pods: 50}
    plugins:
      balance:
        enabled: [LowNodeUtilization]
      deschedule:
        enabled: [RemovePodsHavingTooManyRestarts]

nodeFit: true 是最重要的安全开关:它要求被驱逐的 Pod 在集群中确实存在能容纳它的节点,否则不驱逐。没有它,Descheduler 会把 Pod 赶出去然后无家可归。


3. 资源碎片与装箱问题

3.1 碎片的成因

碎片指「总量足够但无处安放大 Pod」的状态。例如三台 8C16G 的节点各剩 2C4G,集群总空闲 6C12G,但一个需要 4C8G 的 Pod 无处可去。

节点 A:剩余 2C4G      节点 B:剩余 2C4G      节点 C:剩余 2C4G
新 Pod 需要 4C8G -> 无法调度(尽管总空闲 6C12G)

3.2 碎片整理思路

思路一:提高装箱率 —— 用 HighNodeUtilization 把小 Pod 挤到一起,空出整台节点
思路二:保留缓冲节点 —— 预留一台低负载节点给大 Pod,靠优先级与亲和保护
思路三:规格分层 —— 按 Pod 规格划分节点池,避免大小混布
思路四:主动腾空 —— 配合缩容,把节点腾空后由 CA 回收

3.3 装箱率的度量

装箱率 = sum(已请求资源) / sum(可分配资源)
碎片率 = 1 - (可安放的最大 Pod 规格 / 单节点最大剩余)

注意用 requests 而非实际用量:调度器按 requests 决策,用实际用量会得出过于乐观的结论。

3.4 HighNodeUtilization 的配置

该策略的 thresholds(如 cpu: 20)只对低于阈值的节点生效,把其上的 Pod 驱逐出去,让节点变空从而可被缩容。它与 LowNodeUtilization 是互斥的使用场景:前者追求「更满」,后者追求「更均匀」,不要同时启用。


4. 低利用率节点策略

4.1 LowNodeUtilization 的工作原理

它定义两个阈值区间:

underutilized(低于 thresholds)  -> 视为「欠载节点」,有 Pod 可被搬走
overutilized(高于 targetThresholds)-> 视为「过载节点」,需要减负
中间区间                          -> 视为合理,不动

Descheduler 会从欠载节点上挑 Pod 驱逐,期望它们落到中间区间的节点,从而拉平整体分布。

4.2 阈值怎么定

场景thresholdstargetThresholds
追求均衡20 / 20 / 2050 / 50 / 50
保守(少动)10 / 10 / 1070 / 70 / 70
激进(省成本)30 / 30 / 3060 / 60 / 60

阈值不能设得太近,否则每次运行都会驱逐大量 Pod,形成「驱逐-调度-再驱逐」的震荡。

4.3 避免震荡的三道防线

第一道:thresholds 与 targetThresholds 之间留足间隔(至少 30 个百分点)
第二道:Descheduler 运行间隔足够长(>= 5 分钟),给调度器收敛时间
第三道:用 nodeFit + PDB + 优先级过滤,限制单次驱逐规模

4.4 排除不应被搬动的 Pod

- name: DefaultEvictor
  args:
    evictSystemCriticalPods: false
    ignorePvcPods: true
    nodeFit: true
    priorityThreshold:
      value: 10000

priorityThreshold 用于保护高优先级 Pod(如 system-cluster-critical),生产上务必设置,否则可能把关键组件驱逐。


5. 亲和性与拓扑再平衡

5.1 为什么亲和约束会失效

调度时的亲和约束是「当时」满足的,节点标签变化、副本扩缩、手工调度都会让约束被违反。典型场景:

场景一:Pod 反亲和要求副本分散,但驱逐后全部落到同一节点
场景二:节点被打了新标签(如 zone 变更),旧的节点亲和不再匹配
场景三:扩缩容后拓扑打散约束被打破

5.2 相关策略配置

- name: RemovePodsViolatingTopologySpreadConstraint
  args:
    constraints: [DoNotSchedule, ScheduleAnyway]
    includeSoftConstraints: true
- name: RemovePodsViolatingInterPodAntiAffinity
  args: {}
- name: RemovePodsViolatingNodeAffinity
  args:
    nodeAffinityType: [requiredDuringSchedulingIgnoredDuringExecution]

includeSoftConstraints: true 要谨慎:软约束本来就允许偏斜,按它驱逐可能造成不必要的抖动,建议只在硬约束上启用。

5.3 与拓扑感知路由的配合

拓扑再平衡的目标不只是「副本均匀」,还包括「流量就近」。因此再平衡后应复查:

kubectl get pods -l app=api -o custom-columns=\
NODE:.spec.nodeName --no-headers | sort | uniq -c

若各节点副本数与各节点承载的流量不成比例,说明还需要在路由层(trafficDistribution)做进一步调整。

5.4 RemoveDuplicates 的边界

RemoveDuplicates 只针对同一 ReplicaSet / StatefulSet 的副本。它不会动不同工作负载的 Pod,也不会动 DaemonSet。配置:

- name: RemoveDuplicates
  args:
    excludeOwnerKinds: ["DaemonSet"]

6. 与 Cluster Autoscaler 协同

6.1 两者的分工

Cluster Autoscaler(CA):节点层,按调度失败扩容、按低利用率缩容
Descheduler:Pod 层,整理分布,让节点负载均衡

天然的组合:Descheduler 把低负载节点上的 Pod 挤走 → 节点变空 → CA 识别为空节点并缩容 → 成本下降。没有 Descheduler,CA 常常无法缩容,因为每个节点都有一两个「钉子户」Pod。

6.2 缩容失败的两类阻塞

阻塞一:节点上有 kube-system 之外的系统 Pod(如日志采集 DaemonSet 之外的组件)
阻塞二:Pod 有 PDB 且 minAvailable 阻止驱逐
阻塞三:Pod 使用 emptyDir 或本地存储(CA 默认不移除)
阻塞四:Pod 不受控制器管理(裸 Pod,CA 默认不移除)

配置 CA 放宽限制(谨慎):--skip-nodes-with-local-storage=false、--skip-nodes-with-system-pods=false、--scale-down-unneeded-time=10m。

6.3 协同的时序设计

Descheduler 运行间隔:10 分钟
CA 缩容延迟:10~15 分钟(unneeded time)
顺序:先让 Descheduler 腾空节点,再让 CA 在延迟窗口后缩容

不要让 Descheduler 运行过频,否则 CA 刚判定「不需要」就被新 Pod 打断,永远缩不掉。

6.4 常见冲突

冲突现象对策
Descheduler 过于激进节点反复腾空又填满拉长间隔、放宽阈值
CA 缩容阈值太松刚缩容又扩容提高 scale-down 延迟
Pod 有本地存储节点无法缩容评估是否可用网络存储
裸 Pod 存在阻塞缩容纳入控制器管理

7. 驱逐预算与安全阈值

7.1 必须尊重的四道保护

第一道:PodDisruptionBudget —— Eviction API 会自动遵守
第二道:优先级与 Preemption —— 高优先级 Pod 不应被搬动
第三道:nodeFit —— 确保有地方可去
第四道:maxNoOfPodsToEvictPerNode / maxNoOfPodsToEvictPerNamespace —— 限制单次规模

7.2 限制单次驱逐规模

在 DefaultEvictor 中设置 maxNoOfPodsToEvictPerNode: 5、maxNoOfPodsToEvictPerNamespace: 20、maxNoOfPodsToEvictPerRun: 50。这三个参数是防止一次运行把集群搅乱的关键,尤其在策略刚上线时。

7.3 PDB 的正确姿势

PDB 是 Descheduler 的唯一硬性刹车。若某个工作负载没有 PDB,Eviction 会直接成功,可能瞬间失去全部副本。因此上线 Descheduler 前应先审计:所有多副本工作负载是否都有 PDB。一个典型的 minAvailable: 2 就能保证驱逐过程中至少保留两个副本。

7.4 保护有状态工作负载

在 DefaultEvictor 中设 ignorePvcPods: true 会跳过所有挂载 PVC 的 Pod,配合 evictLocalStoragePods: false 与 evictDaemonSetPods: false,是保护数据库、消息队列等有状态服务的简单有效手段。


8. 效果度量与排错

8.1 关键指标

descheduler_pods_evicted_total{strategy,result}   驱逐数量与结果
descheduler_build_info                            版本信息
kube_pod_container_resource_requests              按节点聚合的请求量

用 Prometheus 计算节点间利用率的标准差,作为「均衡度」的核心指标:

均衡度 = stddev(node_cpu_request_ratio)  —— 越小越均衡

8.2 常用排查命令

kubectl -n kube-system logs job/descheduler-xxxxx --tail=100
kubectl get events --field-selector reason=Evicted -A | tail -20
kubectl get pods -A -o wide | awk '{print $8}' | sort | uniq -c | sort -rn

8.3 常见问题对照

现象原因对策
驱逐数为 0阈值不匹配实际负载用实际 requests 数据校准阈值
驱逐后立刻被驱逐回来震荡拉长间隔、加大阈值间隔
Pod 一直 PendingnodeFit 未生效开启 nodeFit
关键组件被驱逐未设优先级阈值配置 priorityThreshold
节点腾不空DaemonSet 与系统 Pod 占位属正常,评估 CA 参数

8.4 上线前的演练

1. 用 --dry-run 运行一次,查看候选驱逐列表
2. 在 staging 环境以最长间隔、最小阈值运行一周
3. 观察 P99 延迟与错误率是否受影响
4. 逐条启用策略,每次只加一个
5. 生产环境先设 maxNoOfPodsToEvictPerRun=5 观察

9. 生产最佳实践

9.1 落地 Checklist

□ 所有多副本工作负载配置 PDB
□ DefaultEvictor 开启 nodeFit 与 priorityThreshold
□ 设置 maxNoOfPodsToEvictPerNode 与 PerRun 上限
□ ignorePvcPods=true 保护有状态服务
□ 阈值间隔至少 30 个百分点,避免震荡
□ 运行间隔 >= 10 分钟,给调度器与 CA 收敛时间
□ 与 Cluster Autoscaler 的缩容延迟对齐
□ 监控驱逐数量与节点利用率标准差
□ 策略纳入 Git,逐条灰度启用
□ 上线前用 dry-run 与 staging 验证

9.2 常见坑与对策

坑现象对策
未开 nodeFitPod 被驱逐后 Pending开启 nodeFit
阈值过近反复驱逐震荡间隔至少 30 个百分点
无 PDB 保护一次性失去全部副本先审计并补齐 PDB
运行过频CA 无法缩容间隔拉到 10 分钟以上
同时启用两种装箱策略行为互相抵消只选 Low 或 High 之一
忽略本地存储 Pod节点无法缩容评估迁移到网络存储
直接在生产启用全策略大范围抖动逐条灰度 + 限制驱逐上限

小结

Descheduler 解决的是调度器的时间盲区:调度只在创建时决策一次,而集群状态一直在变。它的定位非常清晰——只驱逐、不调度,且必须尊重 PDB 与 nodeFit。落地的核心是三件事:用阈值间隔与运行间隔避免震荡、用 PDB 与优先级阈值保证安全、用与 Cluster Autoscaler 的时序配合把均衡真正转化为成本节省。切记不要把它当作「一键优化」,而应视为一个需要持续调参的收敛器:先在 staging 用 dry-run 观察候选列表,再以小规模上限灰度到生产,用节点利用率标准差证明它确实让集群更均匀。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「云原生」更多文章

  1. Cluster API 与声明式集群生命周期管理
  2. 运行时安全:Falco/Tetragon 与 eBPF 检测实战
  3. 拓扑感知路由:topologySpread、本地流量与流量亲和