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 的工作流程如下:
- 扫描:按配置的 Profile 和插件,扫描所有节点和 Pod
- 评估:对每个 Pod 判断是否满足驱逐条件(由
DefaultEvictor过滤,再由各策略插件识别) - 驱逐:对命中条件的 Pod 发起 Eviction 请求(走
PodDisruptionBudget保护) - 重调度:被驱逐的 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 和插件生效,是全局开关:
| 字段 | 类型 | 说明 |
|---|---|---|
nodeSelector | string | 全局源节点过滤:限定 descheduler 只观察哪些节点,所有插件均在此范围内分析 |
maxNoOfPodsToEvictPerNode | uint | 单次运行中每个节点最多驱逐的 Pod 数,0 为不限 |
maxNoOfPodsToEvictPerNamespace | uint | 单次运行中每个命名空间最多驱逐的 Pod 数,0 为不限 |
maxNoOfPodsToEvictTotal | uint | 单次运行全局最多驱逐的 Pod 数,0 为不限 |
两个位置都有 nodeSelector,容易混淆但语义不同:
| 位置 | 作用阶段 | 真实语义 |
|---|---|---|
顶层 nodeSelector | 节点分类阶段 | 限定 descheduler 视野——所有插件( 包括利用率分析)只在这个范围内识别 underutilized / overutilized |
DefaultEvictor.nodeSelector | Pod 过滤阶段 | 候选 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 才会被驱逐。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
nodeSelector | string | "" | 限制只驱逐运行在匹配节点上的 Pod,支持 key=value,key2=value2(AND 语义) |
nodeFit | bool | false | 驱逐前检查集群中是否存在可接收该 Pod 的节点,防止 Pod 驱逐后长期 Pending |
ignorePvcPods | bool | false | 跳过挂载了 PVC 的 Pod(PVC Pod 迁移需重新挂载存储卷,风险较高) |
evictLocalStoragePods | bool | false | 允许驱逐使用本地存储(emptyDir/hostPath)的 Pod |
evictSystemCriticalPods | bool | false | 允许驱逐 system-cluster-critical/system-node-critical 优先级的 Pod |
evictFailedBarePods | bool | false | 允许驱逐没有 owner reference 且处于 Failed 状态的裸 Pod |
minReplicas | uint | 0 | 只驱逐副本数大于此值的工作负载的 Pod,防止单副本服务被驱逐到零 |
priorityThreshold | object | nil | 只驱逐优先级低于此值的 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 的对比
| 对比项 | LowNodeUtilization | HighNodeUtilization |
|---|---|---|
| 驱逐来源 | 高负载节点(≥ targetThresholds) | 低负载节点(< thresholds) |
| 目标效果 | 负载均衡,防止节点过热 | 节点整合,腾空轻载节点 |
| 收敛速度 | 多轮逐渐趋于均衡 | 直接腾空,效果立竿见影 |
| 适用场景 | 集群整体负载不均衡 | 部分节点轻载需要收拢资源 |
| 阈值前提 | thresholds < targetThresholds | thresholds 必须能切出两类节点 |
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 的重启次数