跳到主要内容

Descheduler 二次调度

概述

Descheduler 是 Kubernetes SIGs 维护的组件,用于弥补 kube-scheduler 只做一次调度的局限。它定期分析当前集群中 Pod 的分布情况,对不满足策略的 Pod 执行驱逐,使 Pod 重新进入调度流程,由 kube-scheduler 重新分配到更合适的节点。

kube-scheduler 只在 Pod 首次创建时运行,之后不会重新评估已运行 Pod 的位置。这意味着随着时间推移,集群状态会逐渐偏离最优:新节点加入后旧 Pod 不会自动迁移过来;节点上的 Taint/Affinity 发生变化后违规 Pod 不会被驱逐;业务高峰期过后节点负载持续不均衡,轻载节点上的 Pod 无法自动收拢到少量节点上。

提示

Descheduler 只负责驱逐,不负责调度。驱逐后 Pod 由 kube-scheduler 按当前集群状态重新调度,因此必须确保集群中存在满足 Pod 要求的可调度节点,否则 Pod 会长期处于 Pending 状态。

工作原理

Descheduler 的工作流程如下:

  1. 扫描:按配置的 Profile 和插件,扫描所有节点和 Pod
  2. 评估:对每个 Pod 判断是否满足驱逐条件(由 DefaultEvictor 过滤,再由各策略插件识别)
  3. 驱逐:对命中条件的 Pod 发起 Eviction 请求(走 PodDisruptionBudget 保护)
  4. 重调度:被驱逐的 Pod 由 kube-scheduler 重新调度到合适节点

Descheduler 自身不记录 Pod 历史轨迹,每次运行都是独立的全量扫描。策略插件与 kube-scheduler 使用一致的资源模型(基于 Pod request 而非实际使用量),两者判断口径相同。

部署方式

Descheduler 支持三种运行模式:

模式适用场景特点
Job一次性手动触发执行一批驱逐后退出
CronJob定时周期执行(推荐)按 cron 表达式触发,执行完退出
Deployment持续后台运行固定间隔循环执行,适合需要实时响应的场景

生产环境推荐使用 CronJob 模式,在业务低峰期窗口执行,完成一批驱逐后退出,避免高频驱逐影响业务。

Helm 安装

helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
helm repo update

# 以 CronJob 方式部署(默认每 2 分钟触发一次,按需修改 schedule)
helm install descheduler descheduler/descheduler \
--namespace kube-system \
--set kind=CronJob \
--set schedule="*/30 2-3 * * *" \
--version 0.33.0

YAML 部署

Descheduler 所需的 RBAC 资源、CronJob 工作负载以及策略 ConfigMap 可通过原生 YAML 或 Kustomize 管理。可规划目录结构如下:

descheduler/
├── base/
│ ├── rbac/ # ServiceAccount、ClusterRole、ClusterRoleBinding
│ └── cronjob/ # CronJob 模板
└── overlays/
└── <cluster>/
└── <profile>/
├── kustomization.yaml
├── policy.yaml # 策略配置
└── patch.yaml # 覆盖 schedule、资源限制等

策略通过 ConfigMap 挂载到容器内:

apiVersion: batch/v1
kind: CronJob
metadata:
name: descheduler
namespace: kube-system
spec:
schedule: "*/30 2-3 * * *"
timeZone: "Asia/Shanghai" # K8s 1.25+ 支持,按指定时区解释 schedule
concurrencyPolicy: Forbid # 上一次未完成时跳过本次触发
jobTemplate:
spec:
template:
spec:
serviceAccountName: descheduler
restartPolicy: Never
containers:
- name: descheduler
image: registry.k8s.io/descheduler/descheduler:v0.33.0
args:
- --policy-config-file=/policy-dir/policy.yaml
- --v=3
volumeMounts:
- name: policy-volume
mountPath: /policy-dir
volumes:
- name: policy-volume
configMap:
name: descheduler-policy

策略配置

Descheduler 的策略通过 DeschedulerPolicy 定义,支持多个 Profile,每个 Profile 包含若干插件。插件分为两类:

  • Evictor 插件:控制哪些 Pod 可以被驱逐(全局过滤器)
  • Strategy 插件:实现具体的驱逐逻辑,分为 balance(负载均衡类)和 deschedule(合规清理类)两组
apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
# 顶层字段(详见下方"顶层策略字段")
nodeSelector: "node-role.kubernetes.io/worker="
maxNoOfPodsToEvictPerNode: 10
maxNoOfPodsToEvictTotal: 50
profiles:
- name: my-profile
pluginConfig:
- name: DefaultEvictor # Evictor 插件
args:
nodeFit: true
- name: LowNodeUtilization # Strategy 插件
args:
thresholds:
cpu: 20
memory: 20
targetThresholds:
cpu: 50
memory: 50
plugins:
balance:
enabled:
- LowNodeUtilization

顶层策略字段

DeschedulerPolicy 顶层字段对所有 Profile 和插件生效,是全局开关:

字段类型说明
nodeSelectorstring全局源节点过滤:限定 descheduler 只观察哪些节点,所有插件均在此范围内分析
maxNoOfPodsToEvictPerNodeuint单次运行中每个节点最多驱逐的 Pod 数,0 为不限
maxNoOfPodsToEvictPerNamespaceuint单次运行中每个命名空间最多驱逐的 Pod 数,0 为不限
maxNoOfPodsToEvictTotaluint单次运行全局最多驱逐的 Pod 数,0 为不限
顶层 nodeSelector vs DefaultEvictor.nodeSelector

两个位置都有 nodeSelector,容易混淆但语义不同:

位置作用阶段真实语义
顶层 nodeSelector节点分类阶段限定 descheduler 视野——所有插件(包括利用率分析)只在这个范围内识别 underutilized / overutilized
DefaultEvictor.nodeSelectorPod 过滤阶段候选 Pod 当前节点必须匹配此 selector 才允许驱逐;nodeFit:true 时同时影响 nodeFit 候选节点

实际生产中常见组合:顶层 nodeSelector 做大范围圈定,DefaultEvictor.nodeSelector 做精细控制(如只在某个池子的 GPU 节点驱逐)。两者都设时取交集。

限流参数迁移

在早期版本中 --max-pods-to-evict-* 是 CLI flag,从 v1alpha2 起统一迁移到策略 YAML 顶层字段(驼峰命名)

DefaultEvictor — 全局驱逐过滤器

DefaultEvictor 是所有 Profile 默认启用的 Evictor 插件,在任何 Strategy 插件执行驱逐前,先对候选 Pod 进行过滤。只有通过 DefaultEvictor 所有检查的 Pod 才会被驱逐。

参数类型默认值说明
nodeSelectorstring""限制只驱逐运行在匹配节点上的 Pod,支持 key=value,key2=value2(AND 语义)
nodeFitboolfalse驱逐前检查集群中是否存在可接收该 Pod 的节点,防止 Pod 驱逐后长期 Pending
ignorePvcPodsboolfalse跳过挂载了 PVC 的 Pod(PVC Pod 迁移需重新挂载存储卷,风险较高)
evictLocalStoragePodsboolfalse允许驱逐使用本地存储(emptyDir/hostPath)的 Pod
evictSystemCriticalPodsboolfalse允许驱逐 system-cluster-critical/system-node-critical 优先级的 Pod
evictFailedBarePodsboolfalse允许驱逐没有 owner reference 且处于 Failed 状态的裸 Pod
minReplicasuint0只驱逐副本数大于此值的工作负载的 Pod,防止单副本服务被驱逐到零
priorityThresholdobjectnil只驱逐优先级低于此值的 Pod

nodeFit: true 是生产环境的重要保护参数。它在驱逐前模拟调度,确认集群中至少有一个节点满足该 Pod 的所有调度约束(资源、亲和性、Taint/Toleration 等),若找不到合适节点则跳过驱逐。

nodeSelector 只控制从哪些节点上的 Pod 可以被驱逐,不控制驱逐后 Pod 会被调度到哪里。Pod 被驱逐后,由 kube-scheduler 依据当前全集群状态重新调度。

LowNodeUtilization — 均衡高负载节点

LowNodeUtilization 的目标是让节点负载趋于均衡:从高负载节点驱逐 Pod,使其迁移到低负载节点,防止部分节点过热。

配置需要两个阈值:

  • thresholds低负载阈值,低于此值的节点是 Pod 的接收目标(不从这些节点驱逐)
  • targetThresholds高负载阈值,高于此值的节点是驱逐来源
0% thresholds targetThresholds 100%
|──────────────|──────────────────|─────────────────|
低负载节点(接收目标) 缓冲区(不处理) 高负载节点(驱逐来源)
- name: LowNodeUtilization
args:
thresholds:
cpu: 20 # CPU request 低于 20% 的节点为低负载(接收目标)
memory: 20
pods: 20
targetThresholds:
cpu: 50 # CPU request 高于 50% 的节点为高负载(驱逐来源)
memory: 50
pods: 50

节点分类的 AND / OR 不对称语义

三个资源维度(cpu / memory / pods)的判定逻辑是不对称的:

节点分类判定逻辑含义
低负载(接收目标)CPU < threshold AND Mem < threshold AND Pods < threshold三维全部低于阈值才算
高负载(驱逐来源)CPU ≥ targetThresholds OR Mem ≥ targetThresholds OR Pods ≥ targetThresholds任一维度达标即视为高负载

想"忽略某个维度"必须把对应阈值设到极值(如 cpu:99 几乎让 CPU 维度失效),不能用低值绕过。

必须同时存在两类节点
  • thresholds 必须严格小于 targetThresholds,否则策略拒绝运行
  • 如果集群中没有低于 thresholds 的节点(无接收目标)→ 跳过,日志提示"No node is underutilized"
  • 如果集群中没有高于 targetThresholds 的节点(无驱逐来源)→ 跳过,日志提示无高负载节点

两类节点必须同时存在才会真正触发驱逐。

适用场景:业务高峰过后,部分节点仍承载大量 Pod,整体负载不均衡,希望通过驱逐实现再平衡。

HighNodeUtilization — 腾空低负载节点

HighNodeUtilization 的目标是节点资源整合(Bin Packing):从低负载节点驱逐 Pod,将分散在多个轻载节点上的 Pod 收拢到少量高利用率节点,使轻载节点完全腾空。

只有一个阈值 thresholds,将节点分为两个区间:

0% thresholds 100%
|─────────────────|────────────────────────|
underutilized(驱逐源,腾空) overutilized(名义整合目标,维持)
- name: HighNodeUtilization
args:
thresholds:
cpu: 50 # CPU request 低于 50% 的节点为低负载(驱逐来源)
memory: 50
pods: 50

节点分类的 AND / OR 不对称语义

节点分类判定逻辑含义
underutilized(驱逐源)CPU < threshold AND Mem < threshold AND Pods < threshold三维全部低于阈值才算轻载
overutilized(整合目标)CPU ≥ threshold OR Mem ≥ threshold OR Pods ≥ threshold任一维度达标即视为有负载
关键前提:必须同时存在两类节点

插件需要 underutilized + overutilized 节点同时存在才会启动驱逐:

集群状态插件行为日志
两类都有✓ 正常驱逐Number of underutilized nodes: N + 驱逐记录
全部 underutilized(如 thresholds=100/100/100)✗ 跳过All nodes are underutilized, nothing to do here
全部 overutilized(thresholds 太低)✗ 跳过No node is underutilized, nothing to do here

易踩坑: 直觉把 thresholds 设到 100 想"驱逐所有节点" → 实际上所有节点都 < 100% → 全部被归为 underutilized → 没有 overutilized 整合目标 → 0 驱逐。

关于"整合目标"的实际语义

"整合目标"(overutilized 节点)只是插件内部的分类概念,并不决定 Pod 驱逐后会调度到哪里。Pod 被驱逐后由 kube-scheduler 按 Pod 自身亲和性 / 容忍度 / 资源情况重新调度,最终落点可能是 overutilized 节点,也可能是别的节点(如果有更合适的)。

整合目标存在的唯一作用是让插件满足启动前提(必须有 overutilized 节点)。要真正引导 Pod 去特定节点,必须在 Pod 侧加 nodeAffinity 或在节点侧加 Taint。

与 LowNodeUtilization 的对比

对比项LowNodeUtilizationHighNodeUtilization
驱逐来源高负载节点(≥ targetThresholds)低负载节点(< thresholds)
目标效果负载均衡,防止节点过热节点整合,腾空轻载节点
收敛速度多轮逐渐趋于均衡直接腾空,效果立竿见影
适用场景集群整体负载不均衡部分节点轻载需要收拢资源
阈值前提thresholds < targetThresholdsthresholds 必须能切出两类节点

HighNodeUtilization 利用率口径:默认基于 Pod 的 resource request 计算节点利用率,与 kube-scheduler 的资源评估口径一致。官方文档说明:

Pod resource requests and limits, not actual usage, determine node resource consumption, aligning with the kube-scheduler's approach.

若需改为基于实际使用率计算,需配置 metricsUtilization.source: KubernetesMetrics(需部署 metrics-server)。

RemovePodsViolatingNodeAffinity — 清理违反亲和性的 Pod

当节点标签变化导致已运行 Pod 的 requiredDuringSchedulingIgnoredDuringExecution 亲和性规则不再满足时,驱逐这些 Pod 使其重新调度到符合亲和性的节点。

- name: RemovePodsViolatingNodeAffinity
args:
nodeAffinityType:
- requiredDuringSchedulingIgnoredDuringExecution

典型场景:节点迁移、标签重构后,旧节点标签被删除,运行在该节点上的 Pod 亲和性规则不再满足。

RemovePodsViolatingNodeTaints — 清理违反 Taint 的 Pod

当节点新增了 Taint,或运行中 Pod 失去了对应的 Toleration,这些 Pod 按照 NoSchedule 效果不应再运行在该节点上。Descheduler 可以主动驱逐这些违规 Pod:

- name: RemovePodsViolatingNodeTaints
args:
excludeTaints:
- "node.kubernetes.io/not-ready" # 不处理节点 not-ready Taint

典型场景:节点后期打上了隔离 Taint,需要将其上的 Pod 迁移到其他节点。

RemovePodsViolatingTopologySpreadConstraint — 修复拓扑分布

当 Pod 的 topologySpreadConstraints 中设置了 whenUnsatisfiable: DoNotSchedule,但由于节点变化或 Pod 重启导致实际分布违反了约束,Descheduler 可以驱逐超出约束的 Pod,使拓扑分布重新收敛。

- name: RemovePodsViolatingTopologySpreadConstraint
args:
includeSoftConstraints: false # 是否同时处理 DoNotSchedule(false)还是也处理 ScheduleAnyway

RemovePodsViolatingInterPodAntiAffinity — 清理反亲和冲突

当节点上同时运行了互相存在 requiredDuringSchedulingIgnoredDuringExecution 反亲和约束的 Pod 时(通常由滚动更新的瞬间窗口引起),驱逐其中较新的 Pod,消除反亲和冲突。

- name: RemovePodsViolatingInterPodAntiAffinity
args: { }

PodLifeTime — 清理长期运行的 Pod

驱逐存活时间超过阈值的 Pod,强制触发重建。常用于定期轮换长期运行的 Pod,避免配置漂移或内存泄漏积累。

- name: PodLifeTime
args:
maxPodLifeTimeSeconds: 86400 # 存活超过 24 小时则驱逐
podStatusPhases:
- Pending # 只驱逐处于这些 Phase 的 Pod

RemovePodsHavingTooManyRestarts — 清理频繁重启的 Pod

驱逐重启次数超过阈值的 Pod,触发重新调度,可能将其调度到状态更健康的节点。

- name: RemovePodsHavingTooManyRestarts
args:
podRestartThreshold: 100
includingInitContainers: true # 是否统计 init container 的重启次数

RemoveFailedPods — 清理 Failed 状态的 Pod

驱逐处于 Failed 状态的 Pod,通常搭配 evictFailedBarePods: true 一起使用,清理已失败的裸 Pod(无 owner 的 Pod)。

- name: RemoveFailedPods
args:
reasons:
- NodeAffinity
- Evicted
includingInitContainers: true
minPodLifetimeSeconds: 3600 # 至少存活 1 小时后才清理,避免误清理启动中的 Pod

RemoveDuplicates — 清理同节点重复副本

如果同一个 ReplicaSet/Deployment/StatefulSet 的多个 Pod 运行在同一节点上,驱逐多余的 Pod,使其分散到不同节点,提升可用性。

- name: RemoveDuplicates
args:
excludeOwnerKinds:
- "DaemonSet" # 跳过 DaemonSet 的重复 Pod

生产实践

节点整合(腾空轻载节点)

背景:集群存在多个低利用率节点,Pod 分散在各节点上,希望将这些 Pod 收拢到较少的节点,使轻载节点完全腾空。

方案:使用 HighNodeUtilization 驱逐轻载节点上的 Pod,Pod 重新调度到有空闲资源的其他节点,轻载节点逐步清空。通过 nodeSelector 限制只处理特定节点池,避免影响不应被整理的节点。

apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
# 顶层 nodeSelector:限定 descheduler 视野,所有插件均在此范围内分析
# 同时作为源节点过滤——只有这些节点上的 Pod 会被纳入驱逐候选
nodeSelector: "node.kubernetes.io/pool=order-service"
# 限流:单次运行的驱逐数量上限,控制每轮冲击范围
maxNoOfPodsToEvictPerNode: 5
maxNoOfPodsToEvictTotal: 30
profiles:
- name: consolidate-nodes
pluginConfig:
- name: DefaultEvictor
args:
# 驱逐前确认集群中有可调度落点,防止 Pod 被驱逐后长期 Pending
nodeFit: true
# PVC Pod 驱逐后需重新挂载存储卷,风险高,不驱逐
ignorePvcPods: true
# 允许驱逐本地存储 Pod(emptyDir/hostPath),确保节点能被彻底腾空
evictLocalStoragePods: true
- name: HighNodeUtilization
args:
# 阈值要让节点池能自然分裂成 underutilized + overutilized 两类
# 阈值需基于实际监控数据设定,不能简单调到 100/100/100
# (那样所有节点都被归为 underutilized,插件会跳过)
thresholds:
cpu: 50
memory: 50
pods: 50
plugins:
balance:
enabled:
- HighNodeUtilization

CronJob 调度(低峰期多次执行,每次腾空一批轻载节点):

# 低峰期每 30 分钟执行一次,多轮逐步整理
schedule: "*/30 2-3 * * *"
timeZone: "Asia/Shanghai" # 跨时区运维务必显式声明
concurrencyPolicy: Forbid

驱逐效果示意:

02:00
node-a: 30% request → 低于 thresholds → 驱逐全部 Pod → 0%(节点已腾空)
node-b: 45% request → 低于 thresholds → 驱逐全部 Pod → 0%(节点已腾空)
node-c: 70% request → 高于 thresholds → 本轮不处理,作为 Pod 接收目标
node-d: 80% request → 高于 thresholds → 本轮不处理,作为 Pod 接收目标

02:30
再次运行,如有新的轻载节点继续处理

修复滚动更新后的反亲和冲突

滚动更新期间,Deployment 先创建新 Pod 再删除旧 Pod,若新旧 Pod 同时运行时触发了反亲和 required 规则,更新完成后冲突 Pod 仍留在原节点。配合 RemovePodsViolatingInterPodAntiAffinity 清理这些残留冲突。

profiles:
- name: fix-anti-affinity
pluginConfig:
- name: DefaultEvictor
args:
nodeFit: true
- name: RemovePodsViolatingInterPodAntiAffinity
args: { }
plugins:
deschedule:
enabled:
- RemovePodsViolatingInterPodAntiAffinity

节点标签变更后的合规清理

运维操作修改了节点标签后,部分 Pod 的 requiredDuringSchedulingIgnoredDuringExecution nodeAffinity 不再满足,需要驱逐这些 Pod 使其重新调度到满足亲和性的节点。

profiles:
- name: fix-node-affinity
pluginConfig:
- name: DefaultEvictor
args:
nodeFit: true
minReplicas: 2 # 只驱逐副本数 > 2 的工作负载的 Pod,保护单副本服务
- name: RemovePodsViolatingNodeAffinity
args:
nodeAffinityType:
- requiredDuringSchedulingIgnoredDuringExecution
plugins:
deschedule:
enabled:
- RemovePodsViolatingNodeAffinity

注意事项

PodDisruptionBudget 保护

Descheduler 的所有驱逐都通过 Kubernetes Eviction API 发起,PDB 对驱逐请求生效。如果驱逐会违反 PDB 的 minAvailablemaxUnavailable 约束,该 Pod 的驱逐请求会被拒绝。

生产环境中,建议为所有核心服务配置 PDB:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 1 # 或 maxUnavailable: 1
selector:
matchLabels:
app: my-app

驱逐频率控制

Descheduler 单次运行会驱逐所有命中策略的 Pod,可能对业务造成冲击。控制手段:

  1. PDB:从业务侧限制可驱逐数量(推荐)
  2. CronJob 模式:执行完一批驱逐后退出,避免持续高频驱逐
  3. 低峰期窗口schedule 配置在业务低峰期执行

nodeFit 与调度模拟

nodeFit: true 在驱逐前执行调度模拟,但模拟结果是瞬时的:判断时有可用节点,驱逐后可能因并发驱逐消耗资源导致 Pod 仍处于 Pending。大批量驱逐时建议配合 PDB 分批控制速率。

不适用场景

  • StatefulSet 有状态应用:驱逐可能导致存储卷重新挂载延迟,谨慎使用
  • PVC 绑定的 PodignorePvcPods: true 可跳过,但若必须迁移需评估存储层支持情况
  • DaemonSet Pod:DaemonSet Pod 在被驱逐后会由 DaemonSet Controller 立刻在原节点重建,驱逐无意义,Descheduler 默认不驱逐 DaemonSet Pod

版本兼容性

Descheduler 版本与 Kubernetes 版本存在对应关系,部署前参考官方兼容性矩阵。策略 API 版本为 descheduler/v1alpha2,旧版本(v1alpha1)的配置格式与当前版本差异较大,升级时需同步迁移策略配置。

参考资料