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