





















上一篇调度专题【左扬精讲】拆解了 Scheduling Framework 的6 大内置插件,覆盖了"Filter → Score → Bind"这条主链路。本篇把视角从"调度动作"切到"调度入口"——Pod 进入调度器后究竟被怎么排队、重试、触发回填。一条 Pod 的命运,70% 发生在 SchedulingQueue 这个"调度前厅"。
本篇重点回答四个问题:三段队列(activeQ / backoffQ / unschedulablePods)各自负责什么、Pod 如何在三段之间流转?v1.36 引入的 QueueingHint 是怎么取代"盲目重试"的?哪些事件会触发从 unschedulablePods 回到 activeQ?以及 v1.36.1 GA 后这套机制还有哪些踩坑点?
Kubernetes Scheduler SchedulingQueue QueueingHint 三段队列 Go k8s v1.36.1
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- 三段队列的数据结构与位置:PriorityQueue(pkg/scheduler/backend/queue/scheduling_queue.go:172),activeQ + backoffQ + unschedulablePods,三段各自的数据结构是 heap / heap / map
- QueueingHint 入口:isPodWorthRequeuing()(scheduling_queue.go:488)+ EventsToRegister()(taint_toleration.go:70)
- v1.36 GA 的特性门控:SchedulerQueueingHints(pkg/features/kube_features.go:1948,v1.34 LockToDefault),SchedulerPopFromBackoffQ(kube_features.go:1944,v1.33 Beta)
- PluginToEvents 反向索引:buildEventMap()(scheduling_queue.go:423)+ pluginToEventsMap(scheduling_queue.go:202)
次重点(了解即可)
- PendingPlugins 与 UnschedulablePlugins 的区别(v1.33 引入的"待重试"扩展点)
- PreEnqueue 与 QueueingHint 的关系:PreEnqueue 拦截"入队",QueueingHint 拦截"重试"
- OpportunisticBatching 的 PodSigner(scheduling_queue.go:94)如何利用调度签名加速快路径
文章目录
很多人以为 kube-scheduler 是"看到 Pod 就立刻调度",实际上从 Pod 被 Informer 监听到被 Bind 中间要经过三段队列的多次往返。这个设计的核心动机有四:
设计精髓
三段队列不是三个相互独立的 queue,而是一个 PriorityQueue 结构(scheduling_queue.go:172)内的三块内存。三者共享一把 sync.RWMutex(scheduling_queue.go:181),按"lock → activeQ.lock → backoffQ.lock → nominator.nLock"顺序加锁。这种单结构多段的设计避免了跨 queue 的协调,并发安全也只在一处维护。
SchedulingQueue 是调度器暴露给外界的唯一队列接口(scheduling_queue.go:99-153)。任何"Pod 入队 / 出队 / 移动"的逻辑都必须经过它。
| 方法分类 | 方法 | 作用 |
|---|---|---|
| 入队 | Add(ctx, pod) | 新增 Pod,走完 PreEnqueue 后入 activeQ |
| 激活 | Activate(logger, pods) | 从 unschedulable / backoff 强制搬到 activeQ |
| 失败回填 | AddUnschedulableIfNotPresent | 调度失败时入 unschedulablePods,记录失败插件 |
| 出队 | Pop(logger) | 从 activeQ 取下一个待调度的 Pod(阻塞) |
| 完成回调 | Done(uid) | Pop 后必须调 Done,否则 InFlight 计数不释放 |
| 事件广播 | MoveAllToActiveOrBackoffQueue | 接 Node/PVC 等事件时批量重排 |
| 就绪状态 | AssignedPodAdded/Updated | 已调度的 Pod 落到 Node 上后的回调 |
小贴士 — 关于 InFlight
Pop 返回 Pod 后,调度器不会立刻把它从队列里"删除",而是先标记为 InFlight。当调度失败回填到 unschedulablePods 时,要靠 InFlight 标记避免"重复入队"。每次 Pop 后必须调 Done,否则 InFlight 计数不释放,Pod 被永久"锁住"。
PriorityQueue 内部实际持有 4 个协作子结构(scheduling_queue.go:186-202):
// pkg/scheduler/backend/queue/scheduling_queue.go (行 172-225, k8s v1.36.1)
// PriorityQueue 是 v1.36.1 唯一的 SchedulingQueue 实现,
// 它同时持有 activeQ、backoffQ、unschedulablePods、nominator 4 个子结构。
type PriorityQueue struct {
*nominator // 被提名 Pod 的管理(与抢占相关)
lock sync.RWMutex // 顶层锁
activeQ activeQueuer // 第 1 段:堆(heap)
backoffQ backoffQueuer // 第 2 段:堆(heap)
unschedulablePods *unschedulablePods // 第 3 段:map + 双向链表
preEnqueuePluginMap map[string]map[string]fwk.PreEnqueuePlugin
queueingHintMap QueueingHintMapPerProfile // v1.28+ QueueingHint 表
pluginToEventsMap map[string][]fwk.ClusterEvent // 反向索引
podSigners map[string]PodSigner // v1.32+ 调度签名
apiDispatcher fwk.APIDispatcher // v1.32+ 异步 API
...
}
三段队列的数据结构与定位对比表:
| 队列段 | 数据结构 | 存在哪里 | 何时进出 | 作用 |
|---|---|---|---|---|
| activeQ | heap(按优先级) | active_queue.go | Add → Pop → Done | 调度器主循环唯一的取 Pod 来源 |
| backoffQ | heap(按到期时间) | backoff_queue.go | 失败 → backoff → 时间到 → activeQ | 调度失败 Pod 的指数退避重试 |
| unschedulablePods | map + 双向链表 | unschedulable_pods.go | 失败 → unschedulable → QueueingHint → activeQ | "硬不可调度" Pod 的长时停留区 |
下图展示了 Pod 的典型生命周期(含 QueueingHint 介入):
┌──────────────────────────────────────────────────────────────────────┐
│ SchedulingQueue · Pod 生命周期 (v1.36.1) │
│ │
│ Pod 创建 (Informer) │
│ │ │
│ ▼ │
│ ┌──────────────┐ PreEnqueue 通过 │
│ │ PreEnqueue │ ──────────────────────────┐ │
│ │ (gating check)│ gating 失败 → unschedulablePods │
│ └──────┬───────┘ │ │
│ │ 通过 ▼ │
│ ▼ ┌──────────────────┐ │
│ ┌──────────────┐ │ unschedulablePods │ │
│ │ activeQ │ ◄─────────────────┤ (map + LRU) │ │
│ │ (heap) │ │ 记录: failed plugins│ │
│ └──────┬───────┘ │ · UnschedulablePlugins │ │
│ │ │ · PendingPlugins (v1.33+)│ │
│ │ Pop() └────────┬─────────┘ │
│ ▼ │ │
│ ┌──────────────┐ Filter/Score 成功 ─► Bind │ │
│ │ ScheduleOne()│ ─────────────► Done │ │
│ └──────┬───────┘ │ │
│ │ Filter/Score 失败 │ │
│ ▼ │ │
│ ┌──────────────┐ backoff 完成 │ │
│ │ backoffQ │ ──────────────────┐ │ │
│ │ (heap) │ 时间未到则等待 │ │ │
│ └──────┬───────┘ │ │ │
│ │ ▼ ▼ │
│ └─────────────────────────► activeQ ◄─── ClusterEvent 触发 │
│ ▲ QueueingHint 检查 │
│ │ (v1.28+ GA in v1.34) │
│ └──── Plugin 返回 Queue │
└──────────────────────────────────────────────────────────────────────┘
设计精髓
unschedulablePods 用的不是 heap 而是 map + 双向链表(unschedulable_pods.go):map 给 O(1) 按 UID 查找,链表给 LRU 淘汰。关键差异:activeQ / backoffQ 关心"取出顺序",所以用 heap;unschedulablePods 关心"按 UID 命中",所以用 map。当一个 Pod 在 unschedulablePods 待超过 podMaxInUnschedulablePodsDuration(默认 5 分钟,scheduling_queue.go:66)后,会被强行搬到 backoffQ / activeQ —— 这是"兜底重试"机制,防止 QueueingHint 漏报。
backoffQ 的退避时间每次失败翻倍,直到上限:
// pkg/scheduler/backend/queue/backoff_queue.go (行 80-110, k8s v1.36.1)
// 退避时间算法(伪代码):
// attempt := 失败次数
// backoff := podInitialBackoffDuration (默认 1s)
// for i := 0; i < attempt; i++ {
// backoff = backoff * 2
// if backoff > podMaxBackoffDuration (默认 10s) {
// backoff = podMaxBackoffDuration
// break
// }
// }
//
// 第 1 次失败:等 1s
// 第 2 次失败:等 2s
// 第 3 次失败:等 4s
// 第 4 次失败:等 8s
// 第 5+ 次失败:等 10s(封顶)
backoffQ 内部用一个 最小堆(按 Pod 的 readyTime 排序),每次 Pop 拿"最早到期"的 Pod。Pop 时如果当前时间 clock.Now() < pInfo.readyTime,会阻塞等待直到到期(backoff_queue.go:130-148)。
activeQ 的堆排序键由 QueueSort 插件提供(默认 PrioritySort,按 PodSpec.Priority 倒序)。堆的 Less 函数在 scheduling_queue.go:417 的 convertLessFn 包装。
注意
v1.33 起,SchedulerPopFromBackoffQ 特性门控(pkg/features/kube_features.go:1944)开启后,调度器主循环会同时从 activeQ 和 backoffQ 取 Pod(scheduling_queue.go:407:backoffQPopper = backoffQ)。这避免了"backoff 中 Pod 等不到调度器空闲"的死等。v1.36.1 该特性已 Beta 默认开启。
QueueingHint 是 kube-scheduler 在 v1.28 引入、v1.34 GA、v1.36.1 完全 LockToDefault 的重试优化机制。它的核心思想:
传统方式(v1.27 及以前):Node 资源变化时,调度器会把所有 unschedulable Pod 都重排一次 —— 即便它们失败的插件跟 Node 资源毫无关系。这是一种"宁可错杀"的保守策略。
QueueingHint 方式(v1.34+):每个插件声明自己关心哪些事件,并提供一个 hint 函数,告诉调度器"这个事件是否能让某个失败的 Pod 重新可调度"。调度器只重排那些 hint 返回 Queue 的 Pod。
QueueingHint 由 3 层 Go 类型组成(staging/src/k8s.io/kube-scheduler/framework/types.go:186-218):
// staging/src/k8s.io/kube-scheduler/framework/types.go (行 186-220, k8s v1.36.1)
// ClusterEventWithHint 把"事件"和"提示函数"绑在一起
type ClusterEventWithHint struct {
Event ClusterEvent
// QueueingHintFn:插件告诉调度器"这个事件对某个 Pod 是否重要"
QueueingHintFn QueueingHintFn
}
// QueueingHintFn:核心判断函数
// - pod: 当前重试的 Pod
// - oldObj/newObj: 事件的旧/新对象
// - 返回: Queue(重试)/ QueueSkip(跳过)
// - 报错: caller 视为 Queue(防止 Pod 卡死)
type QueueingHintFn func(logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (QueueingHint, error)
type QueueingHint int
const (
QueueSkip QueueingHint = iota // 事件对 Pod 无意义,跳过重排
Queue // 事件可能让 Pod 可调度,重排
)
插件通过实现 EnqueueExtensions 接口(一个可选扩展点)的 EventsToRegister() 方法来注册 hint:
// pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go (行 68-91, k8s v1.36.1)
func (pl *TaintToleration) EventsToRegister(_ context.Context) ([]fwk.ClusterEventWithHint, error) {
if pl.enableSchedulingQueueHint {
// v1.34+ 路径:纯 QueueingHint,无 preCheck
return []fwk.ClusterEventWithHint{
{Event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add | fwk.UpdateNodeTaint},
QueueingHintFn: pl.isSchedulableAfterNodeChange},
{Event: fwk.ClusterEvent{Resource: fwk.Pod, ActionType: fwk.UpdatePodToleration},
QueueingHintFn: pl.isSchedulableAfterPodTolerationChange},
}, nil
}
// v1.27 兼容路径:保留 UpdateNodeLabel 是为了"绕过" preCheck 的丢事件 bug
return []fwk.ClusterEventWithHint{
{Event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.Add | fwk.UpdateNodeTaint | fwk.UpdateNodeLabel},
QueueingHintFn: pl.isSchedulableAfterNodeChange},
}, nil
}
// hint 函数本体
func (pl *TaintToleration) isSchedulableAfterNodeChange(
logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
originalNode, modifiedNode, err := util.As[*v1.Node](oldObj, newObj)
if err != nil { return fwk.Queue, err } // 出错时默认 Queue,防卡死
// 老节点有 Pod 不容忍的污点 且 新节点没有 污点 → Pod 现在可以调度了
_, wasUntolerated := v1helper.FindMatchingUntoleratedTaint(...originalNode...)
_, isUntolerated := v1helper.FindMatchingUntoleratedTaint(...modifiedNode...)
if wasUntolerated && !isUntolerated {
return fwk.Queue, nil
}
return fwk.QueueSkip, nil
}
当 apiserver 发送一个 Node 更新事件时,调度器内部会按以下 5 步走:
┌──────────────────────────────────────────────────────────────────────┐
│ 步骤 1: 事件广播 (eventhandlers.go) │
│ addAllEventHandlers() 中注册的 Node 回调被触发 │
│ 收集 unionedGVKs 后,调用 sched.MoveAllToActiveOrBackoffQueue() │
│ │ │
│ ▼ │
│ 步骤 2: 批量搬运 (scheduling_queue.go:460 isEventOfInterest) │
│ 检查 pluginToEventsMap,看是否有插件关心这个事件 │
│ · 无插件关心 → logger.V(6) "event isn't interested" + 跳过 │
│ · 有插件关心 → 遍历 unschedulablePods │
│ │ │
│ ▼ │
│ 步骤 3: hint 询问 (scheduling_queue.go:488 isPodWorthRequeuing) │
│ 对每个 unschedulable Pod 调用其 failed plugins 的 hint 函数 │
│ 只询问 UnschedulablePlugins ∪ PendingPlugins(避免给无关插件浪费) │
│ │ │
│ ▼ │
│ 步骤 4: 三态决策 (queueingStrategy) │
│ queueSkip → 留在 unschedulablePods │
│ queueAfterBackoff → 搬到 backoffQ(带退避) │
│ queueImmediately → 直接搬到 activeQ(Pending plugin 返回 Queue 时) │
│ │ │
│ ▼ │
│ 步骤 5: 入队执行 (moveToActiveOrBackoffQueue) │
│ 移动 Pod 到对应队列,触发下一轮调度 │
└──────────────────────────────────────────────────────────────────────┘
// pkg/scheduler/backend/queue/scheduling_queue.go (行 488-575, k8s v1.36.1)
func (p *PriorityQueue) isPodWorthRequeuing(
logger klog.Logger, pInfo *framework.QueuedPodInfo,
event fwk.ClusterEvent, oldObj, newObj interface{},
) queueingStrategy {
rejectorPlugins := pInfo.UnschedulablePlugins.Union(pInfo.PendingPlugins)
if rejectorPlugins.Len() == 0 {
// Pod 没记录任何失败插件 → 直接走 backoff(保守)
return queueAfterBackoff
}
// wildcard 事件:搬全部(特例)
if framework.ClusterEventIsWildCard(event) {
...
return queueAfterBackoff
}
hintMap, ok := p.queueingHintMap[pInfo.Pod.Spec.SchedulerName]
if !ok {
return queueAfterBackoff // 兜底:没注册 hint → 走 backoff
}
queueStrategy := queueSkip // 初始假设不重排
for eventToMatch, hintfns := range hintMap {
if !framework.MatchClusterEvents(eventToMatch, event) {
continue // 这个 hint 不关心这个事件
}
for _, hintfn := range hintfns {
if !rejectorPlugins.Has(hintfn.PluginName) {
continue // 这个 Pod 没被这个插件拒绝过,跳过
}
hint, err := hintfn.QueueingHintFn(logger, pod, oldObj, newObj)
if err != nil {
hint = fwk.Queue // 出错兜底:当作 Queue
}
if hint == fwk.QueueSkip { continue }
// 关键:Pending 插件返回 Queue 意味着"立刻"(跳过 backoff)
if pInfo.PendingPlugins.Has(hintfn.PluginName) {
return queueImmediately
}
// unschedulable 插件返回 Queue → backoff 后重试
if pInfo.PendingPlugins.Len() == 0 {
return queueAfterBackoff
}
queueStrategy = queueAfterBackoff
}
}
return queueStrategy
}
设计精髓
QueueingHint 的两层兜底设计堪称教科书:
这两个兜底让 QueueingHint 即使在边界场景下也不会把调度器卡死,只是退化为旧的"盲目重试"行为。
调度器在初始化时为每个 profile构建一张反向表 pluginToEventsMap(scheduling_queue.go:202),键是插件名,值是它关心的事件列表。事件触发时,调度器只需检查 isEventOfInterest()(scheduling_queue.go:460-478)就能秒判"这个事件有没有插件关心",比遍历整个 hintMap 快得多。
// pkg/scheduler/backend/queue/scheduling_queue.go (行 423-435, k8s v1.36.1)
func buildEventMap(qHintMap QueueingHintMapPerProfile) map[string][]fwk.ClusterEvent {
eventMap := make(map[string][]fwk.ClusterEvent)
for _, hintMap := range qHintMap {
for event, qHints := range hintMap {
for _, qHint := range qHints {
// plugin name → event list(反向:事件驱动方向)
eventMap[qHint.PluginName] = append(eventMap[qHint.PluginName], event)
}
}
}
return eventMap
}
很多新人混淆这三个扩展点,它们长得像、都在调度框架外层,但职责完全不同:
| 维度 | QueueingHint | PreEnqueue | Permit |
|---|---|---|---|
| 触发时机 | 事件触发 Pod 从 unschedulablePods 回到 activeQ 时 | Pod 进入 activeQ 前 | 调度成功后、Bind 前 |
| 检查内容 | "这个事件 对 这个 Pod 有意义吗?" | "Pod 当前是否满足某些前置条件?" | "是否可以放行到 Bind 阶段?" |
| 失败后果 | Pod 留在 unschedulablePods | Pod 进 unschedulablePods | Pod 被 Reject |
| 典型代表 | TaintToleration、NodeResourcesFit | SchedulingGates、GangScheduling | Coscheduling(Volcano) |
| 实现接口 | EnqueueExtensions | PreEnqueuePlugin | PermitPlugin |
小贴士 — 关于 Pending 插件
v1.33 引入的 PendingPlugins 字段(framework/types.go):插件可以在 Filter / Permit 阶段不拒绝 Pod 但推迟调度(比如 Gang 的"等兄弟"),把 Pod 标记成 Pending 状态。Pending 插件返回 Queue = 立刻调度,Unschedulable 插件返回 Queue = 等 backoff(scheduling_queue.go:554-557、scheduling_queue.go:562-565)。
v1.32 起,OpportunisticBatching(pkg/features/kube_features.go:720,v1.32 Alpha、v1.33 Beta 默认)引入了 Pod 调度签名(Pod Signature)机制。它的核心思路:
对一批"特征几乎相同"的 Pod(比如同一 Deployment 一次性创建 100 个),调度器只对第一个 Pod 做完整 Filter / Score,后续 Pod 直接复用第一个的调度结果(同一 Node)。这在大规模 Deployment 扩容场景能减少 90%+ 的重复计算。
签名机制由 3 个角色组成(scheduling_queue.go:91-94、scheduling_queue.go:215):
当 2 个 Pod 的签名相同时,调度器认为它们"调度特征一致",第二个 Pod 跳过 Filter / Score,直接拿第一个 Pod 的 SuggestedHost。
注意
签名机制的不变量:若 2 个 Pod 的签名相同,必须保证它们的 Filter / Score 结果也相同。因此依赖"其他 Pod 位置"的插件(InterPodAffinity、PodTopologySpread)必须返回 nil 让签名失败(Unschedulable 状态),强制走慢路径(详见本系列上一篇插件精讲)。这是 v1.36.1 起所有"不能签名"插件的共同行为。
症状:生产集群 Node 新建后,本应可以调度的 Pod 永远卡 unschedulablePods;重启 scheduler 后立刻恢复。
原因:v1.27 时期,调度器有个 preCheck 机制:Node/Add 事件触发时,调度器会先检查"现有 cache 里是否已有该 Node"。若 cache 还没同步,事件会被丢弃。v1.34 之前靠每个插件额外注册 UpdateNodeLabel 事件绕过,但容易漏。
修复:升级到 v1.34+(QueueingHint GA),SchedulerQueueingHints LockToDefault 后 preCheck 彻底关闭(scheduling_queue.go:160-162 注释明确指出 "This problematic preCheck feature is disabled when QHint is enabled")。
症状:Out-of-Tree 插件过滤时拒绝了一批 Pod,但 Node 资源释放后 Pod 永远不被重排。
原因:插件没实现 EnqueueExtensions.EventsToRegister(),调度器不知道它关心哪些事件,pluginToEventsMap 里没有它的条目,事件广播时被 isEventOfInterest() 过滤掉了。
修复:所有"有可重试条件"的插件必须实现 EnqueueExtensions,至少注册一个 {Resource: Node, ActionType: Add} 事件作为兜底。
症状:Gang Scheduling 插件里,把"等兄弟"的 Pod 错误地放进 UnschedulablePlugins,导致兄弟到齐后还要等 backoff。
原因:Gang 失败时 Pod 是"暂时不可调度"而非"永久不可调度",应该用 PendingPlugins(v1.33+)。Pending 插件返回 Queue 会跳过 backoff 立刻调度(scheduling_queue.go:554),更符合 Gang 的"等齐即跑"语义。
activeQ(active_queue.go,heap):调度器主循环 Pop 的唯一来源。backoffQ(backoff_queue.go,heap):调度失败 Pod 的退避重试。unschedulablePods(unschedulable_pods.go,map + LRU):硬不可调度 Pod 的长停留区,由 QueueingHint 事件触发回到 activeQ。三者在 PriorityQueue(scheduling_queue.go:172)内共享一把锁。
KEP-3094 提出、v1.28 Beta(默认 false,pkg/features/kube_features.go:1948)→ v1.32 Beta(默认 true)→ v1.34 GA(LockToDefault,无法关闭)。v1.36.1 起 QueueingHint 是强制行为,老代码里的 preCheck fallback 已删除。
视为 Queue(scheduling_queue.go:546)。这是兜底:插件写错代码不能让 Pod 卡死。但代价是可能做无用重排。建议插件 hint 函数内 defer recover() 防止 panic,且对无法识别的对象类型明确 return QueueSkip, nil。
kube-scheduler 启 --v=6 日志,搜索 "queueing hint" 或 "worth requeuing":
# /var/log/kube-scheduler.log
# 命中 Queue(Pod 会被重排):
I0620 ... "pod is worth requeuing" pod="default/my-app-0" plugin="TaintToleration" event="NodeUpdateNodeTaint" hint="Queue"
# 命中 QueueSkip(Pod 留在 unschedulable):
I0620 ... "pod is not worth requeuing" pod="default/my-app-1" plugin="NodeResourcesFit" event="NodeUpdateNodeLabel" hint="QueueSkip"
还可以看 metrics:scheduler_queueing_hint_duration_seconds(按 plugin + event + result label 切片)。
无界。activeQ 没有容量上限,靠 backoffQ 的退避和 unschedulablePods 的LRU 淘汰做软限制。如果生产中看到 activeQ 堆积,说明调度器处理速度跟不上入队速度,常见原因:每个 Pod 调度耗时过长(如 InterPodAffinity 在大集群),需要排查 metrics scheduler_queue_incoming_pods_total 与 scheduler_pod_scheduling_duration_seconds。
Pod 在 unschedulablePods 待超过 podMaxInUnschedulablePodsDuration(默认 5 分钟,scheduling_queue.go:66)后,会被 flushUnschedulablePodsLeftover()(scheduling_queue.go:443,30 秒一轮)强行搬到 backoffQ。这是兜底,确保 QueueingHint 漏报时 Pod 也能重试。生产中如果你怀疑漏报,调小这个值比"重启 scheduler"更优雅。
v1.34 起无法关闭(LockToDefault)。v1.32 还能用 --feature-gates=SchedulerQueueingHints=false 关闭。生产集群升级到 v1.34 后,所有 hint 都会被强制执行。建议在升级前先在 staging 用 --feature-gates=SchedulerQueueingHints=true 试跑 1~2 周。
EventHandler(eventhandlers.go)是 apiserver watch 事件的入口:所有 Node/Pod/PVC 事件先经过 EventHandler,转成 MoveAllToActiveOrBackoffQueue 调用。QueueingHint 是 EventHandler 触发后的精筛器:决定"哪些 Pod 值得被搬"。EventHandler 总是被调用,但 QueueingHint 可以拒绝重排。
通过 KubeSchedulerConfiguration 的 podInitialBackoffSeconds(默认 1)和 podMaxBackoffSeconds(默认 10)。
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
podInitialBackoffSeconds: 1
podMaxBackoffSeconds: 10
podMaxInUnschedulablePodsDuration: 5m
大集群里建议调大 podMaxBackoffSeconds 到 30~60s,避免大量 Pod 同时 backoff 完成后"撞车"重排。
v1.33 起 Beta 默认开启(kube_features.go:720),v1.36.1 仍为 Beta。该特性依赖每个插件的 SignPod() 实现;如果某个插件没实现,调度器自动跳过该插件的签名,不影响其他插件生效。
调度器监听 Pod 事件:Delete 事件触发 PriorityQueue.Delete()(scheduling_queue.go:127),从所有队列(activeQ / backoffQ / unschedulablePods)按 UID 删除。如果 Pod 当时在 InFlight(已 Pop 未 Done),靠 Activate() 的 wildcard 机制重新调度(scheduling_queue.go:495-513)。
少而精。v1.34+ 引入了细粒度事件(framework/types.go:46-74),比如 UpdatePodToleration 代替通用 Update,UpdateNodeLabel 代替通用 UpdateNodeXYZ。用窄事件:hint 函数被调用的频率低,但每次判断更准。避免注册 {Resource: Pod, ActionType: All},会让 hint 函数每次 Pod 事件都被调用。
看 --v=6 日志的 "Moving Pods to active":
I0620 12:00:00.123 "Received Node event, attempting to move all pods" event="NodeAdd" node="node-1"
I0620 12:00:00.125 "Moved 5 pods to active" count=5
如果 count=0,说明 hint 函数把所有 Pod 都判 QueueSkip 了 —— 这通常是插件 hint 写得过于严格。
在 scheduler.New()(pkg/scheduler/scheduler.go:387-401)的 profile 循环里:
for profileName, profile := range profiles {
// 1) 收集 PreEnqueue 插件
plugins := profile.PreEnqueuePlugins()
preEnqueuePluginMap[profileName] = ...
// 2) 收集 QueueingHint(每个 EnqueueExtensions 插件的 EventsToRegister)
queueingHintsPerProfile[profileName], err = buildQueueingHintMap(ctx, profile.EnqueueExtensions())
...
}
然后传给 internalqueue.WithQueueingHintMapPerProfile()(scheduling_queue.go:300-305)。
defaultQueueingHintFn(pkg/scheduler/scheduler.go:472)是一个永远返回 Queue的占位 hint,用于插件不提供 hint 时的兜底(让事件触发不会"无 hint 可用")。
Pod 的 types.UID(unschedulable_pods.go)。这样保证同一 Pod 的多次失败合并而不是堆叠 100 个 entry。
让你的插件实现 fwk.EnqueueExtensions 接口:
func (pl *MyPlugin) EventsToRegister(_ context.Context) ([]fwk.ClusterEventWithHint, error) {
return []fwk.ClusterEventWithHint{
{Event: fwk.ClusterEvent{Resource: fwk.Node, ActionType: fwk.UpdateNodeLabel},
QueueingHintFn: pl.isSchedulableAfterNodeLabelChange},
}, nil
}
func (pl *MyPlugin) isSchedulableAfterNodeLabelChange(
logger klog.Logger, pod *v1.Pod, oldObj, newObj interface{}) (fwk.QueueingHint, error) {
_, newNode, err := util.As[*v1.Node](nil, newObj)
if err != nil { return fwk.Queue, err }
if newNode.Labels["my.io/zone"] == pod.Spec.NodeSelector["my.io/zone"] {
return fwk.Queue, nil
}
return fwk.QueueSkip, nil
}
var _ fwk.EnqueueExtensions = &MyPlugin{} // 编译期断言
QueueingHint 是返回值类型(QueueSkip / Queue,framework/types.go:209-218);QueueingHintFn 是函数类型(framework/types.go:207,func(logger, pod, oldObj, newObj) (QueueingHint, error))。命名上确实容易混淆,但读源码时记"Fn 后缀是函数"即可。
不冲突,是正交的两个特性:
v1.36.1 默认两者都开。生产中如果看到调度抖动(Pop 频繁切换队列),可能是 backoffQ 中有大量"快到期"Pod 在抢调度器资源,可以考虑关 SchedulerPopFromBackoffQ。
v1.32+ OpportunisticBatching 启用时:
QueueingHintMapPerProfile(scheduling_queue.go:295)是 map[profileName]QueueingHintMap。每个 Pod 按 pod.Spec.SchedulerName 查自己 profile 的 hint map(scheduling_queue.go:515)。这样多 profile 集群不会互相干扰 hint 判断。
通过 metrics + debug endpoints:
# 1. metrics(Prometheus)
scheduler_queue_size{queue="active"} # activeQ 长度
scheduler_queue_size{queue="backoff"} # backoffQ 长度
scheduler_queue_size{queue="unschedulable"} # unschedulablePods 长度
# 2. debug endpoint(默认 :10251/debug)
curl http://<scheduler>:10251/debug/pprof/symbol # 看 pprof
curl http://<scheduler>:10251/metrics | grep queue_size
是。v1.27 时代插件不实现 EnqueueExtensions 也能用,调度器把它们的失败 Pod 放到 unschedulablePods 时不带 plugin 关联,重排时按"无 rejected plugins"处理(scheduling_queue.go:490),退回 backoff 兜底路径。这是兼容老插件的关键设计。
不能。hint 函数的签名是 func(logger, pod, oldObj, newObj),事件本身不作为参数传入(framework/types.go:207)。需要根据 oldObj / newObj 的类型判断事件类型:*v1.Node 表示 Node 事件,*v1.Pod 表示 Pod 事件。
queueingHintMap 在 PriorityQueue 构造时填好后只读,没有并发修改入口。pluginToEventsMap 同理。读时不需要加锁(scheduling_queue.go:466-473 的 isEventOfInterest 无锁读)。这种"init-only"设计是 v1.36.1 简化并发模型的关键。
本篇拆解了 SchedulingQueue 的三段队列与 v1.36.1 GA 的 QueueingHint。调度专题接下来还有几条主线:
调度专题的目标读者是资深运维开发:能读 Go 源码、有集群运维经验、对 k8s 整体架构已有认知。下一篇会从 Scheduler Profile 与 Coordinated LeaderElection 切入,把"多调度器"这件事讲透。
本文参考与源码链接:
• scheduling_queue.go · PriorityQueue
• active_queue.go · activeQ 实现
• backoff_queue.go · backoffQ 退避
• unschedulable_pods.go · unschedulablePods LRU
• framework/types.go · QueueingHint 类型
• tainttoleration.go · EventsToRegister 示例
• kube_features.go · SchedulerQueueingHints 门控
• scheduler.go · 多 Profile 初始化
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。