



























上一篇调度专题【左扬精讲】拆解了 Scheduling Framework 的六大内置插件,并预告了本篇主线:当所有 Filter 阶段都失败、Score 也找不到合适节点时,kube-scheduler 怎么把已经在跑的低优先级 Pod "挪走",给待调度 Pod 腾位置?这个机制叫做抢占(Preemption)。
本篇的核心问题是:当一个高优先级 Pod 调度不上时,调度器会进入 PostFilter 扩展点,调用 DefaultPreemption 插件(以下简称 DP)。DP 并不是简单地把节点上的"低优先级" Pod 全部杀光——它会先在候选节点里找最少必要的 victim,且必须考虑 PodDisruptionBudget(以下简称 PDB)的保护性约束。本篇以 k8s v1.36.1 源码为蓝本,彻底讲透这两层逻辑。
读完本篇,你应该能回答三个问题:DefaultPreemption 怎么被触发?它怎么选节点?怎么在候选节点上选 victim,而 PodDisruptionBudget 又如何介入?
Kubernetes Scheduler Preemption DefaultPreemption PodDisruptionBudget Go k8s v1.36.1
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- Preemption 的 6 步主干流程:Evaluator.Preempt(pkg/scheduler/framework/preemption/preemption.go:103)的0 取最新 Pod → 1 资格检查 → 2 找候选节点 → 3 Extender 过滤 → 4 选最优候选 → 5 执行驱逐
- DefaultPreemption 插件定义与触发点:PostFilterPlugin / PreEnqueuePlugin 双接口(pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go:89-90)
- victim 选择算法:SelectVictimsOnNode(default_preemption.go:251)的"先全删后回填"贪心 + filterPodsWithPDBViolation(default_preemption.go:414)的 PDB 分组
- PDB 如何约束 victim 选择:PDB 不直接阻止抢占,而是把候选分成"违反 PDB 的"和"未违反 PDB 的"两组,调度器会优先保护后者
次重点(了解即可)
- 异步抢占 EnableAsyncPreemption:v1.36.1 起,通过 PreEnqueue 阻止 Pod 在抢占未完成前重入
- 工作负载感知抢占 EnableWorkloadAwarePreemption:PodGroupPostFilter(default_preemption.go:468)接管 PodGroup 整体抢占
- MinCandidateNodesPercentage / MinCandidateNodesAbsolute:FindCandidates 的"抽样"策略(default_preemption.go:218-227)
文章目录
k8s 是一个多租户共享集群的系统。当一个高优先级 Pod 调度不上时,调度器有三种选择:
Preemption 是 k8s 在QoS 和资源利用率之间取平衡的核心机制:日常让低优先级 Pod 跑满资源(binpack 提利用率),关键时刻"以小换大"(高优先级驱逐低优先级)。
设计精髓
抢占不是"无脑驱逐",而是一套带约束的贪心:① 抢占者(preemptor)必须 priority 高于被驱逐者(victim) ② victim 集合最小化(少杀多得) ③ 必须尊重 PDB(不能为了驱逐而破坏业务 SLO)。这三条约束保证了抢占"有序、可控、可解释"。
调度框架里有两个扩展点与抢占直接相关:PostFilter 和 PreEnqueue。DefaultPreemption 同时实现这两个接口(default_preemption.go:89-90):
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 89-95, k8s v1.36.1)
// DefaultPreemption 是 PostFilter 扩展点实现:当所有 Filter 失败后被调用
// 同时是 PreEnqueue 扩展点:阻止一个 Pod 在抢占未完成前被重新入队
var _ fwk.PostFilterPlugin = &DefaultPreemption{}
var _ fwk.PreEnqueuePlugin = &DefaultPreemption{}
| 扩展点 | 方法 | 何时被调用 |
|---|---|---|
| PostFilter | PostFilter(ctx, state, pod, m) | 调度周期内,所有 Filter 插件都失败时,调度器调用 PostFilter 扩展点上的所有插件。DefaultPreemption 是唯一内置实现 |
| PreEnqueue | PreEnqueue(ctx, pod) | Pod 进入 activeQ 之前,用来门控。当 EnableAsyncPreemption 开启时,正在异步抢占的 Pod 会被 PreEnqueue 拒绝 |
抢占的总入口在 pkg/scheduler/framework/preemption/preemption.go:103 的 Evaluator.Preempt。DefaultPreemption 插件的 PostFilter(default_preemption.go:135)只是薄薄一层,把活儿转交给 pl.Evaluator.Preempt。
注意
PostFilter 是可选扩展点:只有 Unschedulable 或 UnschedulableAndUnresolvable 状态触发。DefaultPreemption 内部把 PostFilter 写得相当克制:5 行 if 处理 PodGroup 特例(default_preemption.go:140-155),其余全部委托给 Evaluator。
Evaluator.Preempt(preemption.go:103-170)是抢占的"大脑"。它严格按 6 步执行:
┌──────────────────────────────────────────────────────────────────────┐
│ Evaluator.Preempt(ctx, state, pod, m) preemption.go:103 │
│ │
│ 0) 取最新 Pod ──── PodLister 拿 preemptor 的最新对象(行 110-115) │
│ │ │
│ ▼ │
│ 1) 资格检查 ──── PodEligibleToPreemptOthers()(行 117-122) │
│ │ ├─ preemptionPolicy=Never → 拒绝 │
│ │ └─ nominatedNode 上有 terminating victim → 拒绝 │
│ ▼ │
│ 2) 找候选节点 ──── findCandidates()(行 124-148) │
│ │ ├─ 随机 offset + N 个候选 │
│ │ ├─ 调 SelectVictimsOnNode 模拟抢占 │
│ │ └─ 过滤出"能装下 preemptor"的节点 │
│ ▼ │
│ 3) Extender ──── callExtenders()(行 150-154) │
│ │ 把候选节点给外部 Extender 再过滤一次 │
│ ▼ │
│ 4) 选最优候选 ──── SelectCandidate()(行 156-162) │
│ │ 按"违反 PDB 最少"的策略挑节点 │
│ ▼ │
│ 5) 执行驱逐 ──── executor.actuatePodPreemption()(行 164-167) │
│ 调 apiserver Evict 接口删除 victim Pod │
└──────────────────────────────────────────────────────────────────────┘
每一步都有可观测的返回值和日志:
| 步骤 | 关键函数 | 源码行号 | 失败/空结果语义 |
|---|---|---|---|
| 0 取最新 Pod | PodLister.Get | preemption.go:110-115 | apiserver 错误,AsStatus 透传 |
| 1 资格检查 | PodEligibleToPreemptOthers | default_preemption.go:363 | 返回 Unschedulable,Pod 进 unschedulableQ 等待 |
| 2 找候选节点 | findCandidates | preemption.go:174 | 返回 Unschedulable + Preemption is not helpful |
| 3 Extender | callExtenders | preemption.go:151 | 透传 extender 返回的错误 |
| 4 选最优候选 | SelectCandidate | preemption.go:157 | 返回 no candidate node for preemption |
| 5 执行驱逐 | actuatePodPreemption | executor.go | 驱逐失败,Unschedulable |
小贴士 — 关于"非必要不抢"
当 findCandidates 返回空集合时,调度器不会杀任何 Pod,而是直接给 preemptor 打上 Unschedulable 状态(preemption.go:147)。这种"preemption is not helpful"的诊断对应一个经典的运维难题:明明 priority 已经最高,为什么还在 Pending?答案往往是没有"可被驱逐"的目标——所有 victim 的 priority 都不低于 preemptor。
SelectVictimsOnNode(default_preemption.go:251-353)是 victim 选择的核心算法。它有 4 个关键步骤:
第一阶段对节点上所有 Pod 做资格检查:
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 277-293, k8s v1.36.1)
// As the first step, remove all pods eligible for preemption from the node and
// check if the given pod can be scheduled without them present.
for _, pi := range nodeInfo.GetPods() {
if pl.isPreemptionAllowed(nodeInfo, pi, pod) {
potentialVictims = append(potentialVictims, pi)
}
}
for _, pi := range potentialVictims {
if err := removePod(pi); err != nil { // 模拟删除
return nil, 0, fwk.AsStatus(err)
}
}
关键函数 isPreemptionAllowed(default_preemption.go:395-398)做了两件事:
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 395-398, k8s v1.36.1)
func (pl *DefaultPreemption) isPreemptionAllowed(nodeInfo fwk.NodeInfo, victim fwk.PodInfo, preemptor *v1.Pod) bool {
// The victim must have lower priority than the preemptor
return corev1helpers.PodPriority(victim.GetPod()) < corev1helpers.PodPriority(preemptor) &&
pl.IsEligiblePod(nodeInfo, victim, preemptor)
}
第二阶段:把所有 potentialVictims 全部模拟删除后,跑一次 Filter 看看 preemptor 能否装下:
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 295-303, k8s v1.36.1)
if status := pl.fh.RunFilterPluginsWithNominatedPods(ctx, state, pod, nodeInfo); !status.IsSuccess() {
return nil, 0, status
}
如果删除全部 victim 后还装不下,直接放弃这个节点(selectVictimsOnNode 返回 0 候选)。这是性能优化:避免无意义的 PDB 分析。
注意
这一步在 v1.36.1 中有个已知边界:default_preemption.go:297-300 的注释明确说"如果 preemptor 失败是因为对某个 victim 有 inter-pod affinity,不支持这种场景"。换句话说,不要给高优先级 Pod 配 requiredDuringSchedulingIgnoredDuringExecution 指向一个低优先级 Pod —— 这会让抢占在大多数情况下"找不到候选节点"。
第三阶段:把 potentialVictims 按PDB 是否会被违反分成两组:
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 311-314, k8s v1.36.1)
// Try to reprieve as many pods as possible. We first try to reprieve the PDB
// violating victims and then other non-violating ones. In both cases, we start
// from the highest importance victims.
violatingVictims, nonViolatingVictims := filterPodsWithPDBViolation(potentialVictims, pdbs)
filterPodsWithPDBViolation(default_preemption.go:414-465)的逻辑:
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 414-465, k8s v1.36.1)
func filterPodsWithPDBViolation(podInfos []fwk.PodInfo, pdbs []*policy.PodDisruptionBudget) (violatingPodInfos, nonViolatingPodInfos []fwk.PodInfo) {
pdbsAllowed := make([]int32, len(pdbs))
for i, pdb := range pdbs {
pdbsAllowed[i] = pdb.Status.DisruptionsAllowed
}
for _, podInfo := range podInfos {
pod := podInfo.GetPod()
pdbForPodIsViolated := false
if len(pod.Labels) != 0 {
for i, pdb := range pdbs {
if pdb.Namespace != pod.Namespace { continue }
selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector)
if err != nil || selector.Empty() || !selector.Matches(labels.Set(pod.Labels)) { continue }
// 关键:已经被记录在 DisruptedPods 里的 Pod 不算"违反"
if _, exist := pdb.Status.DisruptedPods[pod.Name]; exist { continue }
pdbsAllowed[i]--
if pdbsAllowed[i] < 0 { pdbForPodIsViolated = true }
}
}
if pdbForPodIsViolated { violatingPodInfos = append(violatingPodInfos, podInfo) } else { nonViolatingPodInfos = append(nonViolatingPodInfos, podInfo) }
}
return violatingPodInfos, nonViolatingPodInfos
}
这是 PDB 介入的关键算法:逐 Pod 模拟驱逐,每模拟驱逐一个属于 PDB 保护集的 Pod,pdbsAllowed[i] 就减 1。当 pdbsAllowed[i] < 0 时,意味着如果驱逐这个 Pod,PDB 就会被违反。
小贴士 — 关于 DisruptedPods 字段
DisruptedPods(default_preemption.go:446)是 PDB status 的一个 map,记录当前正在被驱逐的 Pod。当驱逐流程进行时,apiserver 端 eviction controller 会把目标 Pod 写入这个 map。已经在 DisruptedPods 里的 Pod 不会被重复计数,避免对同一 Pod 重复扣减 DisruptionsAllowed。
第四阶段:把 victim 按重要度降序排,依次尝试回填(reprieve)到节点,看 preemptor 还能不能装下:
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 315-329, k8s v1.36.1)
reprievePod := func(pi fwk.PodInfo) (bool, error) {
if err := addPod(pi); err != nil { return false, err }
status := pl.fh.RunFilterPluginsWithNominatedPods(ctx, state, pod, nodeInfo)
fits := status.IsSuccess()
if !fits {
if err := removePod(pi); err != nil { return false, err }
victims = append(victims, pi)
logger.V(5).Info("Pod is a potential preemption victim on node", "pod", klog.KObj(pi.GetPod()), "node", klog.KObj(nodeInfo.Node()))
}
return fits, nil
}
// 先尝试回填"违反 PDB"的那批
for _, p := range violatingVictims { ... }
// 再尝试回填"未违反 PDB"的那批
for _, p := range nonViolatingVictims { ... }
关键设计:
回填顺序按 MoreImportantPod(default_preemption.go:86)降序(先处理 priority 高、runtime 长的 Pod),保证"重要 Pod 优先存活"。
设计精髓
这个"全删 → 按序回填"的贪心算法,本质上是"最少驱逐集合"的近似解:每次回填一个不破坏可调度性的 Pod,直到"再回填一个就不行了"。由于每一步都跑 RunFilterPluginsWithNominatedPods,算法复杂度是 O(n·F)(n=potentialVictims 数量,F=Filter 插件耗时),但相比"穷举所有子集"的指数复杂度,这已经是工业界可接受的近似最优。
PDB 是 k8s 给运维/业务方的一把"保护伞":保证"任意时刻同时中断的 Pod 数量"不超过阈值。它只关心自愿中断(eviction),被抢占的 Pod 也算自愿中断。
| 字段 | 含义 | 示例 |
|---|---|---|
| spec.selector | 匹配哪些 Pod 受保护 | app=nginx |
| spec.minAvailable | 最少保持可用数量(或百分比) | 2(至少有 2 个 Pod 跑着) |
| status.disruptionsAllowed | 当前可中断配额(由 controller 实时算) | 1(还能再中断 1 个) |
PDB 在 victim 选择中是软约束,不是硬约束。它的介入体现在三处:
┌────────────────────────────────────────────┐
│ PDB 介入 victim 选择的 3 个时间点 │
├────────────────────────────────────────────┤
│ │
① 收集阶段 ──┼─► isPreemptionAllowed 仍然只看 priority │
│ (PDB 不参与:先收集所有可能被抢的 Pod) │
│ │
② 分组阶段 ──┼─► filterPodsWithPDBViolation 按"模拟驱逐 │
│ 是否破坏 PDB"把 Pod 切成两组 │
│ │
③ 回填阶段 ──┼─► 先回填 violatingVictims(已破坏,救不回)│
│ 再回填 nonViolatingVictims(努力救) │
│ 重要度降序,先救高 priority 再说 │
└────────────────────────────────────────────┘
在步骤 4 "选最优候选"中,调度器会优先选"违反 PDB 数最少"的节点(preemption.go:157 的 SelectCandidate)。这就让 PDB 不只是"在节点内约束 victim",还成了"跨节点比较候选"的排序键。
注意
PDB 的"保护"不是"禁止"。当一个高优先级 Pod 必须抢占且所有候选节点都会违反 PDB 时,调度器仍然会选那个违反最少的节点、仍然会驱逐它认为必要的 Pod —— 只是优先保护那些"在 PDB 保护下能存活"的 Pod。不要把 PDB 当作"绝对不能动"的硬墙 —— 它只是"尽量少动"。
// pkg/scheduler/framework/plugins/defaultpreemption/default_preemption.go (行 304-353, k8s v1.36.1)
var victims []fwk.PodInfo
numViolatingVictim := 0
// Sort potentialVictims by descending importance, which ensures reprieve of
// higher importance pods first.
sort.Slice(potentialVictims, func(i, j int) bool {
return pl.MoreImportantPod(potentialVictims[i].GetPod(), potentialVictims[j].GetPod())
})
// Try to reprieve as many pods as possible. We first try to reprieve the PDB
// violating victims and then other non-violating ones. In both cases, we start
// from the highest importance victims.
violatingVictims, nonViolatingVictims := filterPodsWithPDBViolation(potentialVictims, pdbs)
reprievePod := func(pi fwk.PodInfo) (bool, error) {
if err := addPod(pi); err != nil { return false, err }
status := pl.fh.RunFilterPluginsWithNominatedPods(ctx, state, pod, nodeInfo)
fits := status.IsSuccess()
if !fits {
if err := removePod(pi); err != nil { return false, err }
victims = append(victims, pi)
logger.V(5).Info("Pod is a potential preemption victim on node", "pod", klog.KObj(pi.GetPod()), "node", klog.KObj(nodeInfo.Node()))
}
return fits, nil
}
for _, p := range violatingVictims {
if fits, err := reprievePod(p); err != nil { return nil, 0, fwk.AsStatus(err) } else if !fits { numViolatingVictim++ }
}
for _, p := range nonViolatingVictims {
if _, err := reprievePod(p); err != nil { return nil, 0, fwk.AsStatus(err) }
}
代码结构很清晰:
设计精髓
注意第 345 行的二次排序:v1.36.1 在合并 violating 与 nonViolating 两组后,会再排一次降序,避免"violating 排前、nonViolating 排后"导致实际驱逐顺序不符合"重要度优先"。这种"先分组、再合并、最后重排"是经典的归并排序思想,工程实现值得借鉴。
把 6 步流程和 victim 选择摆到一张表里,看清每一步调用什么:
| Preempt 步骤 | 关键函数 | 行号 | victim 视角 |
|---|---|---|---|
| 0 取最新 Pod | PodLister.Get | preemption.go:110 | - |
| 1 资格检查 | PodEligibleToPreemptOthers | default_preemption.go:363 | 检查 nominatedNode 上有 terminating victim |
| 2 找候选节点 | findCandidates | preemption.go:174 | 对每个候选节点调 SelectVictimsOnNode |
| 3 Extender 过滤 | callExtenders | preemption.go:151 | 外部 hook,可拒绝候选节点 |
| 4 选最优候选 | SelectCandidate | preemption.go:157 | 按"违反 PDB 数"升序选 |
| 5 执行驱逐 | actuatePodPreemption | executor.go | 按 victims 顺序调 apiserver Evict |
Victim 选择只发生在第 2 步,但它的结果(numViolatingVictim)会被第 4 步用作排序键。也就是说:
小贴士 — 关于异步抢占
v1.36.1 起,EnableAsyncPreemption 特性门控(默认开启)开启时,actuatePodPreemption 不再同步等 apiserver Evict 返回,而是异步触发驱逐。Pod 进入 preempting 集合(executor.go:82-86),PreEnqueue 会拒绝该 Pod 重新入队(default_preemption.go:182-185),直到 victim 真正死亡、Pod 调度成功。
下面 3 个坑都是生产真实碰到过的,按发生概率从高到低排列。
症状:业务给核心 Pod 配了 priorityClassName: system-cluster-critical,但仍卡 Pending 状态,describe 显示 Preemption is not helpful for scheduling。
原因:所有目标节点上的 Pod 都已经用了 system-cluster-critical 或更高 priority,isPreemptionAllowed(default_preemption.go:395-398)全部返回 false,potentialVictims 为空,findCandidates 找不到任何候选节点。
修复:
症状:某业务 Deployment 配了 minAvailable: 100%,所有节点上的同标签 Pod 都受 PDB 保护。当高优先级 Pod 调度时,filterPodsWithPDBViolation 几乎全部归类为 violatingVictims,SelectCandidate 选不到"违反最少"的节点。
原因:minAvailable: 100% 意味着 DisruptionsAllowed = 0,任何被 PDB 匹配的 Pod 都不能被驱逐,调度器只能"放弃"。
修复:
症状:集群里同时有"高优先级"和"中优先级"两个业务,"高优先级"驱逐"中优先级",但"中优先级"被驱逐后找不到位置,又去驱逐"高优先级"的某个 Pod → 集群抖动。
原因:PodEligibleToPreemptOthers(default_preemption.go:363-387)只在"自己的 nominatedNode 上有 terminating victim"时才拒绝抢占,不检查"自己是不是别人 nominatedNode 上的 terminating victim"。
修复:
设计精髓
抢占循环的本质是"没有静态优先级":调度器只看"当前快照"做决策,不考虑"驱逐 A 会引发 A 抢 B"的连锁反应。这种贪心 + 本地视角在大多数情况下 OK,但当业务"互相抢"时就会暴露。生产经验:把 priority 当成静态等级,每个等级预留足够资源,宁可留 buffer 也不靠动态抢占。
从 v1.32 引入 EnableAsyncPreemption(v1.36.1 默认开启)后,驱逐是异步的。调度器在 pkg/scheduler/framework/preemption/executor.go 的 actuatePodPreemption 中不等待 apiserver Evict 返回,而是把 Pod UID 放入 preempting 集合(executor.go:82-86),由 PreEnqueue 阻止该 Pod 重入(default_preemption.go:182-185)。但"找候选节点"和"选 victim"仍然是同步的。
不会被。isPreemptionAllowed(default_preemption.go:395)要求 victim.priority 严格小于 preemptor.priority。在生产中,不要给两个业务设相同 priority 后让它们互相抢——优先用 priorityClassName 区分(PriorityClass 是集群级资源)。
PDB 不参与"是否抢占"的判定,只参与"抢哪些 Pod"的判定。判定流程是:① 抢占者 priority 够不够(isPreemptionAllowed)→ ② 哪些 Pod 可以被抢(PDB 分组)→ ③ 实际驱逐谁(贪心回填)。
被抢占的 Pod 走 apiserver Evict API,apiserver 会给 victim 加 PodReasonPreemptionByScheduler 条件(default_preemption.go:407-410),并强制使用 30s 的优雅期(即便 victim 自己的 terminationGracePeriodSeconds 更长)。这是 k8s 的硬约束,不可配置。
用 kubectl describe events 配合 --all-namespaces:
# 1) 抓所有 Preemption 事件
kubectl get events --all-namespaces --field-selector reason=Preempted
# 2) 看 victim 上的 DisruptionTarget condition
kubectl get pod <victim-pod> -o jsonpath='{.status.conditions[?(@.type=="DisruptionTarget")]}'
# 3) kube-scheduler 日志 (--v=4 看到候选节点筛选, --v=6 看到 victim 选择)
kubectl logs -n kube-system kube-scheduler-<node> | grep -i preempt
调度器是单线程处理一个 Pod 的抢占流程,但多个 Pod 的调度周期并行。可能出现"A 抢 B、C 抢 D,但 B 和 D 在同一节点"的情况,此时 PreEnqueue 的 IsPodRunningPreemption 检查(default_preemption.go:182-185)会阻止一个 Pod 在 victim 未死亡前再次入队。极端情况下会出现"饿死",但生产中很少见。
不一定。SelectVictimsOnNode(default_preemption.go:251)的"全删 → 贪心回填"算法是近似最优,不是数学最优。例如:理论上只杀 1 个大 Pod 就够,但贪心会先按重要度排序,重要度高的会被保留,实际可能多杀几个小的。这是性能与最优性的折衷。
Pod spec.preemptionPolicy 字段(v1.36.1 中可选值:PreemptLowerPriority 默认、Never):设为 Never 后,PodEligibleToPreemptOthers(default_preemption.go:364-366)直接返回 false,Pod 不参与任何抢占。这个字段主要给"critical workload"(如集群插件)使用——保证它们不主动抢别人。
不会。Preemption 只调 apiserver Evict API,不删除 PV / PVC / Service 等关联资源。如果业务 Pod 的 PVC 是 Retain 策略,PV 会在 Pod 删除后保留,需要手动清理。这是个常见的"灰度测试"陷阱:抢占了 Pod 之后,PV 残留导致新 Pod FailedMount。
不是。抢占流程在 preemption.go:165-169 设置 nominatedNodeName,但 preemptor 仍要等 victim 真正死亡(Pod 消失、kubelet 释放资源)后,再次进入调度周期才能 Bind。Pod 状态会显示 nominatedNodeName 字段。
绝对值更可控。生产中强烈建议用绝对值(如 minAvailable: 3),原因有三:① 百分比在扩容瞬间会"陡变"(如 Deployment 从 2 副本扩到 3 副本,minAvailable: 50% 从 1 变成 2) ② 百分比下"可驱逐配额"会随副本数变化 ③ 监控和告警配置更简单。
不会单独限制。抢占候选节点的全集是 SnapshotSharedLister.NodeInfos().List()(preemption.go:125),即所有节点。但如果 preemptor 自身有 NodeSelector 或 NodeAffinity,调度器会在 Filter 阶段就把不匹配的节点淘汰,到 PostFilter 阶段候选集里也只剩下匹配的节点。
两种方式:
会。抢占流程走标准的 Pod 删除路径:apiserver 把 Pod deletionTimestamp 设上 → kubelet 收到删除事件 → 跑 PreStop 钩子 → 给容器发 SIGTERM。整个过程最长 30s 优雅期(见 Q4),超时后 SIGKILL。
v1.36.1 支持 3 个抢占相关特性:
| 特性 | 特性门控 | 入口 | 适用场景 |
|---|---|---|---|
| 异步抢占 | EnableAsyncPreemption | default_preemption.go:166 | 大集群吞吐 |
| PodGroup 抢占 | EnableWorkloadAwarePreemption | default_preemption.go:468 | Volcano / Kueue |
| TAS 抢占 | EnableTopologyAwareWorkloadScheduling | default_preemption.go:140 | 拓扑感知调度 |
由 MinCandidateNodesPercentage 和 MinCandidateNodesAbsolute 决定(default_preemption.go:218-227)。默认:10% 或 100 个,取大者。100 节点以下会全量评估;1000 节点集群只评估 100 个。这是个性能与精度的折衷。
会。当抢占后仍然调度不上(所有候选都被拒绝),Preemptor 会清空 nominatedNodeName(preemption.go:147),回到 unschedulableQ 等待下次重试。重试间隔由 scheduler.conf 里的 backoff 机制控制。
不是。抢占流程只调 apiserver Evict,资源真正释放要等:① kubelet 收到删除事件 ② 容器进程退出(最多 30s 优雅期) ③ kubelet 释放 cgroup / 网络 / 端口等。整个流程通常 5~30s。在这段时间内,新 Pod 不能立刻拿到资源。
由 policy/v1 PDB controller 实时计算:
DisruptionsAllowed = max(0, min(
minAvailable - (currentHealthy - DisruptedPods.size),
totalReplicas - currentHealthy
))
简单说:当前健康 Pod 数减去 minAvailable 还能被驱逐几个。详细算法见 pkg/controller/disruption/disruption.go。注意 DisruptedPods 已经包含了"正在被驱逐"的 Pod,调度器在 filterPodsWithPDBViolation(default_preemption.go:446)会跳过它们。
按下面 5 步法排查:
小贴士 — 80% 的"抢占不生效"是 priority 没配对
在 k8s 里,priority 是唯一决定"谁能抢谁"的字段。PDB、NodeSelector、ResourceQuota 都不会"禁止"抢占,只会影响"抢哪些"和"在哪儿抢"。
本篇把 DefaultPreemption 怎么选 victim、PDB 怎么约束讲透了,但还有几条主线尚未展开:
调度专题的目标读者是资深运维开发:能读 Go 源码、有集群运维经验、对 k8s 整体架构已有认知。下一篇我会从 CycleState 切入,把框架运行时的"骨架"补全。
本文参考与源码链接:
• default_preemption.go · DefaultPreemption 插件
• preemption.go · Evaluator 6 步流程
• executor.go · 抢占执行器(含异步抢占)
• types.go · Candidate 与 Victims 结构
• schedule_one.go · PostFilter 调用入口
• disruption.go · PDB DisruptionsAllowed 算法
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。