惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
S
Security @ Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
A
Arctic Wolf
Webroot Blog
Webroot Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Security Latest
Security Latest
H
Heimdal Security Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
T
Tor Project blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
The Last Watchdog
The Last Watchdog
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
Scott Helme
Scott Helme
A
About on SuperTechFans
M
MIT News - Artificial intelligence
V
V2EX
V
Visual Studio Blog
Recorded Future
Recorded Future
博客园 - 叶小钗
F
Fortinet All Blogs
L
Lohrmann on Cybersecurity
The GitHub Blog
The GitHub Blog
博客园 - Franky
P
Proofpoint News Feed
MyScale Blog
MyScale Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Secure Thoughts
D
DataBreaches.Net
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)
I
InfoQ
SecWiki News
SecWiki News
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
B
Blog RSS Feed
AWS News Blog
AWS News Blog
Know Your Adversary
Know Your Adversary
V
Vulnerabilities – Threatpost
H
Help Net Security

博客园 - 左扬

VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— VictoriaLogs 协同:Metrics 到 Logs 的一体化监控 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 源码阅读路线图:如何高效阅读 VM 源码 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 开源生态:VM 在 CNCF 生态中的位置 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 与其他 TSDB 对比:Prometheus/InfluxDB/Thanos/VM VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 写入吞吐/查询延迟/内存占用的数学模型 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 模块依赖图——从 import 语句看组件关系 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Goroutine 池/atomic/零拷贝/sync.Pool VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 多租户架构——accountID/projectID 与 tenant 隔离 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 版本演进:1.146.0 LTS 重大更新解析 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 整体数据流:一条监控数据的完整生命周期 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— 架构演进:从 TSDB 到 MergeSet 的设计取舍 VictoriaMetrics 1.146.0 源码专题【左扬精讲】—— Single-Node vs Cluster 模式本质区别 VictoriaMetrics 1.146.0 源码【左扬精讲】—— 开篇总览 Rust 专题【左扬精讲】—— 从语法到灵魂:Ownership、Borrowing 与多语言对比 kubernetes 源码【左扬精讲】—— kube-scheduler 启动流程源码分析 Rust 专题【左扬精讲】—— 选择控制语句、运算符与格式化输出 Rust 专题【左扬精讲】—— 所有权详解 Rust 专题【左扬精讲】—— 作用域详解 Rust 专题【左扬精讲】—— 变量、常量与标量数据类型 kubernetes 源码 / Operator 专题【左扬精讲】—— Deployment Controller 源码分析:从对象创建到滚动更新 kubernetes 源码 / Operator 专题【左扬精讲】—— Operator 开发中的 Webhook:从准入控制到生产部署 Kubernetes源码 / Operator 专题【左扬精讲】—— 实现 Application Controller:从零构建生产级控制器 Kubernetes 编程 / Operator 专题【左扬精讲】—— 定义 Application 资源 + 添加自定义新 API 完整指南 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八):内部架构与核心组件 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 八): —— 从入口到调度的全链路源码剖析(k8s v1.36.1) DeepSeek-R1 多模态 R1 / VLM-GRPO【左扬精讲】—— Qwen2-VL 微调与视觉推理强化学习实战 DeepSeek-R1 工业 RAG + 微调混合系统【左扬精讲】—— R1 系列收官之作:从 Prompt → RAG → 微调 选型决策树 DeepSeek-R1 推理时扩展【左扬精讲】—— o1 / R1 慢思考机制:Self-Consistency + ToT + PRM 详解 DeepSeek-R1 端侧 LLM 工程【左扬精讲】—— llama.cpp 调参与 Apple Silicon / 国产 NPU / Android 端侧落地全攻略 DeepSeek-R1 vLLM + k8s 生产部署【左扬精讲】—— 从单卡 7B 到 100 卡 671B MoE 集群的工业化部署实战 DeepSeek-R1 评估与系统(Evaluation & Systems)【左扬精讲】—— 从 GSM8K/MMLU 到 LLM-as-Judge 的工业级评估方法论 DeepSeek-R1 模型训练与算法【左扬精讲】—— GRPO 进阶算法:DAPO / PRIME / RLVR / PRM 四大 2025 前沿改进 DeepSeek-R1 模型训练与算法【左扬精讲】—— 数据蒸馏:用 DeepSeek-R1-671B 生成 800K 高质量 CoT 样本的完整流水线 DeepSeek-R1 优化与微调实战【左扬精讲】—— 从 R1 强化学习新范式到 GRPO 微调一站式入门 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 七):自定义插件开发实战 —— 手写一个 Score 插件并注册到集群 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 六):Scheduler Profile 与多调度器 —— 如何配置多个 profile 实现多租户、Coordinated LeaderElection Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 四):抢占(Preemption)算法剖析 —— DefaultPreemption 如何选 victim、PodDisruptionBudget 如何约束 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 二):内置插件逐个精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件 逐个精读 k8s 源码级精讲(二十六):调度器内置插件逐个精读 Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):Scheduling Framework 扩展点逐个源码拆解 Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):初识调度模型、内部架构与事件驱动机制 Kubernetes 编程 / client-go 专题【左扬精讲】—— 四种客户端:为什么、怎么选、怎么用 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime、kubebuilder、operator-sdk 三大框架深度对比 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入理解 ManagedFields 字段冲突协调机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Finalizers 深度解析:对象的生命周期与删除控制 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference 字段与级联删除机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— 深入学习 Server-Side Apply:managedFields 替代 last-applied-configuration 的演进方向 Kubernetes 编程 / Operator 专题【左扬精讲】—— k8s Annotations 与元数据体系(Operator 专题) Kubernetes 编程 / Operator 专题【左扬精讲】—— RESTMapper:把 Group / Version / Kind / Resource 四元组翻译成 REST 路径的"查字典"大师 Kubernetes 编程 / Operator 专题【左扬精讲】—— Converter 资源版本转换器 Kubernetes 编程 / Operator 专题【左扬精讲】—— Application 业务扩展:从单 Deployment 到多 Workload 的复合 Operator 演进 Kubernetes 编程 / Operator 专题【左扬精讲】—— OwnerReference / Finalizer / 准入控制:k8s 资源生命周期的三大支柱 Kubernetes 编程 / Operator 专题【左扬精讲】—— controller-runtime 框架内幕:从 Manager 到 Reconcile 的全栈拆解 Kubernetes 编程 / Operator 专题【左扬精讲】—— 生产级 Operator 最佳实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— application-operator Reconcile 循环源码精讲:从 client-go Informer 到 workqueue 的全链路解剖 Kubernetes 编程 / Operator 专题【左扬精讲】—— 从零搭建一个 application-operator 新项目:脚手架、API 设计与基于原生 DeploymentStatus/ServiceStatus 的状态建模 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:生产级 Controller 实践:并发安全、资源清理与高可用设计 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析: Controller 调试与诊断工具:从日志分析到问题定位 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:控制器与 APIServer 完整交互流程:从 Watch 到缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:错误处理与重试机制:WorkQueue 限速器详解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Leader 选举机制:高可用控制器的必备技能 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Controller 开发模式完整实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:SharedInformerFactory 与等待缓存同步 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:从认证配置到 Deployment 操作 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:版本对应、架构组件与组件关系 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Informer 源码深度解析:从底层原理到实战应用 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Reflector 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:ListWatcher 源码深度解析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:Indexer 与 ThreadSafeStore 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DeltaFIFO 核心原理与源码深度剖析 Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:workqueue 核心原理与实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— runtime.Codec 资源编解码:serializer 与 codec 差异、编解码数据结构、codec 核心调用链路 Kubernetes 编程 / Operator 专题【左扬精讲】—— Scheme 资源注册机制全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 自定义资源的内部版本与外部版本:从源码看版本定义机制 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 1.36.1 核心 API 数据结构全解 Kubernetes 编程 / Operator 专题【左扬精讲】—— Kubernetes 构建过程 【AIOPS】一文读懂LLM【左扬精讲】:从诞生到普及,解锁大语言模型的核心密码 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Hooks 源码精讲:Hooks 可观测性的无侵入式实现 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 配置解析源码精讲 ——SRE 自定义 Agent 核心技巧 【AIOPS】AI Agent 专题【左扬精讲】核心功能篇:MCP-VictoriaMetrics Golang 并发模型解析 ——SRE 应对高并发采集的调优思路 【AIOPS】AI Agent 专题【左扬精讲】基础架构篇:MCP-VictoriaMetrics Golang 源码整体架构拆解 ——SRE 必懂的核心模块与数据流 OpenTelemetry 开发实战【左扬精讲】—— 云原生可观测体系构建与分布式追踪二次开发 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(下篇)—— 预测式弹性扩缩容 Operator 落地实现) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 7 —— 基于流量预测模型的智能弹性扩缩容 Operator 实战(AIOps 模型训练与智能扩容(上篇)—— 时序预测模型构建与离线训练) Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 6 —— 基于运维专家知识库的智能故障诊断与排查 Operator 实战 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 5 —— 基于大语言模型(LLM)的实时日志流智能监测 Operator 实现 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 4 —— 基于 Operator 实现大模型私有化部署与管理 Kubernetes 编程 / Operator 专题【左扬精讲】—— Operator 开发实战项目 3(上篇)—— 面向 AI / 算力调度场景:GPU 竞价实例资源池统一调度管理 Operator 开发 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目 2 —— 面向零售 / 电商潮汐流量难题:多云多集群数据中心级全链路弹性伸缩 DataCenter Scaler Operator 从 0 到 1 全链路开发 Kubernetes编程 / Operator专题【左扬精讲】—— 深入理解Kubebuilder注解:为什么Operator开发离不开这些特殊注释 Kubernetes编程 / Operator专题【左扬精讲】—— Operator 开发实战项目1 —— Applicaion Operator(通用应用生命周期管理 Operator 实战) Pod 镜像拉取失败?kubectl edit pods修改镜像地址的底层原理与实操 (该方法仅为临时应急方案,并非长期解决方案) Kubernetes编程/Operator专题精讲—— 理解控制器模式 —— 控制器模式的核心原理与实现逻辑(从原理到实践) 【AIOPS】AI Agent 专题【左扬精讲】模型微调实战:一站式平台 LLaMA-Factory 【AIOPS】AI Agent 专题【左扬精讲】基于 k8s+vLLM+Ray 分布式部署全指南:架构设计、资源调度与性能优化 【AIOPS】AI Agent专题【左扬精讲】非量化版DeepSeek分布式部署全指南:精度保障、显存规划与Ollama/vLLM选型 【AIOPS】AI Agent 专题【左扬精讲】零开发框架实现 ReAct Agent(Go SRE友好)
Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 五):SchedulingQueue 与 QueueingHint —— 三段队列的细节、v1.36 新引入的 QueueingHint 工作机制
左扬 · 2026-06-20 · via 博客园 - 左扬

Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 五):SchedulingQueue 与 QueueingHint —— 三段队列的细节、v1.36 新引入的 QueueingHint 工作机制

上一篇调度专题【左扬精讲】拆解了 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

学习重点提示建议先通读全文,再重点回顾标注内容

重点掌握(必须)

  • 三段队列的数据结构与位置PriorityQueuepkg/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 的特性门控SchedulerQueueingHintspkg/features/kube_features.go:1948,v1.34 LockToDefault),SchedulerPopFromBackoffQkube_features.go:1944,v1.33 Beta)
  • PluginToEvents 反向索引buildEventMap()scheduling_queue.go:423)+ pluginToEventsMapscheduling_queue.go:202

次重点(了解即可)

  • PendingPluginsUnschedulablePlugins 的区别(v1.33 引入的"待重试"扩展点)
  • PreEnqueueQueueingHint 的关系:PreEnqueue 拦截"入队",QueueingHint 拦截"重试"
  • OpportunisticBatching 的 PodSignerscheduling_queue.go:94)如何利用调度签名加速快路径

文章目录

一、Why:Pod 为什么要排队

很多人以为 kube-scheduler 是"看到 Pod 就立刻调度",实际上从 Pod 被 Informer 监听到被 Bind 中间要经过三段队列的多次往返。这个设计的核心动机有四:

  • 流量削峰:Deployment 一次性创建 1000 个 Pod,如果全部同时进入 Filter 阶段,apiserver 会被刷爆。activeQ 的优先级队列 + backoffQ 的退避重试,天然限流
  • 容错退避:第一次调度失败(节点资源不够),不可能立刻就重试。backoffQ 的指数退避1s → 10s)让 Pod 在等待期间让位给其他能调度的 Pod
  • 事件驱动重试:失败的 Pod 不是"等够时间"才重试,而是等"能让它成功"的事件发生。Node 资源释放了 → NodeUpdate 事件 → 触发 QueueingHint → 回到 activeQ
  • 资源公平:通过 QueueSort 插件按优先级排序,Priority 高的 Pod 优先出队

设计精髓

三段队列不是三个相互独立的 queue,而是一个 PriorityQueue 结构(scheduling_queue.go:172)内的三块内存。三者共享一把 sync.RWMutexscheduling_queue.go:181),按"lock → activeQ.lock → backoffQ.lock → nominator.nLock"顺序加锁。这种单结构多段的设计避免了跨 queue 的协调,并发安全也只在一处维护。

二、What:SchedulingQueue 接口全景

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 被永久"锁住"。

三、How:三段队列的内部实现

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 漏报。

3.1 backoffQ 的指数退避算法

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)。

3.2 activeQ 的优先级排序

activeQ 的堆排序键由 QueueSort 插件提供(默认 PrioritySort,按 PodSpec.Priority 倒序)。堆的 Less 函数在 scheduling_queue.go:417convertLessFn 包装。

注意

v1.33 起,SchedulerPopFromBackoffQ 特性门控(pkg/features/kube_features.go:1944)开启后,调度器主循环会同时从 activeQ 和 backoffQ 取 Pod(scheduling_queue.go:407backoffQPopper = backoffQ)。这避免了"backoff 中 Pod 等不到调度器空闲"的死等。v1.36.1 该特性已 Beta 默认开启。

四、Detail:QueueingHint 的工作机制(v1.28~v1.36 演进)

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。

4.1 QueueingHint 的 Go 接口

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 可调度,重排
)

4.2 插件如何注册 QueueingHint

插件通过实现 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
}

4.3 QueueingHint 的触发链(5 步)

当 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 到对应队列,触发下一轮调度                                  │
└──────────────────────────────────────────────────────────────────────┘

4.4 QueueingHint 的关键源码:isPodWorthRequeuing

// 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 的两层兜底设计堪称教科书:

  1. hint 函数报错时默认 Queuescheduling_queue.go:546):防止"插件写错代码导致 Pod 卡死"
  2. hint 全 Skip 时保留默认保守路径scheduling_queue.go:490-493):当 Pod 的 failed plugins 列表为空(极少见,比如 Permitted 的 Pod),调度器仍走 backoff 重试

这两个兜底让 QueueingHint 即使在边界场景下也不会把调度器卡死,只是退化为旧的"盲目重试"行为。

4.5 buildEventMap:plugin → events 反向索引

调度器在初始化时为每个 profile构建一张反向表 pluginToEventsMapscheduling_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
}

五、Compare:QueueingHint vs PreEnqueue vs Permit

很多新人混淆这三个扩展点,它们长得像、都在调度框架外层,但职责完全不同

维度QueueingHintPreEnqueuePermit
触发时机 事件触发 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 = 等 backoffscheduling_queue.go:554-557scheduling_queue.go:562-565)。

六、Detail:OpportunisticBatching 与 PodSigner

v1.32 起,OpportunisticBatchingpkg/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-94scheduling_queue.go:215):

  1. PodSignerscheduling_queue.go:94):类型签名 func(ctx, pod) fwk.PodSignature
  2. SignPod 接口(插件侧):每个插件实现自己的 SignPod(ctx, pod) ([]SignFragment, *Status)
  3. Profile.SignPodscheduler.go:410):每个 profile 把插件的 SignFragment 合并成一个签名

当 2 个 Pod 的签名相同时,调度器认为它们"调度特征一致",第二个 Pod 跳过 Filter / Score,直接拿第一个 Pod 的 SuggestedHost。

注意

签名机制的不变量:若 2 个 Pod 的签名相同,必须保证它们的 Filter / Score 结果也相同。因此依赖"其他 Pod 位置"的插件(InterPodAffinity、PodTopologySpread)必须返回 nil 让签名失败(Unschedulable 状态),强制走慢路径(详见本系列上一篇插件精讲)。这是 v1.36.1 起所有"不能签名"插件的共同行为。

七、Pitfall:踩坑实录

P1. preCheck 漏事件导致 Pod 永久卡 unschedulable

症状:生产集群 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")。

P2. 自研插件忘了实现 EventsToRegister

症状:Out-of-Tree 插件过滤时拒绝了一批 Pod,但 Node 资源释放后 Pod 永远不被重排

原因:插件没实现 EnqueueExtensions.EventsToRegister(),调度器不知道它关心哪些事件,pluginToEventsMap 里没有它的条目,事件广播时被 isEventOfInterest() 过滤掉了。

修复:所有"有可重试条件"的插件必须实现 EnqueueExtensions,至少注册一个 {Resource: Node, ActionType: Add} 事件作为兜底。

P3. PendingPlugins 与 UnschedulablePlugins 误用

症状:Gang Scheduling 插件里,把"等兄弟"的 Pod 错误地放进 UnschedulablePlugins,导致兄弟到齐后还要等 backoff。

原因:Gang 失败时 Pod 是"暂时不可调度"而非"永久不可调度",应该用 PendingPlugins(v1.33+)。Pending 插件返回 Queue 会跳过 backoff 立刻调度scheduling_queue.go:554),更符合 Gang 的"等齐即跑"语义。

八、FAQ:常见疑问

Q1. 三段队列分别负责什么?

activeQactive_queue.go,heap):调度器主循环 Pop 的唯一来源。backoffQbackoff_queue.go,heap):调度失败 Pod 的退避重试unschedulablePodsunschedulable_pods.go,map + LRU):硬不可调度 Pod 的长停留区,由 QueueingHint 事件触发回到 activeQ。三者在 PriorityQueuescheduling_queue.go:172)内共享一把锁

Q2. QueueingHint 是什么时候引入的?

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 已删除。

Q3. hint 函数返回 error 怎么处理?

视为 Queuescheduling_queue.go:546)。这是兜底:插件写错代码不能让 Pod 卡死。但代价是可能做无用重排。建议插件 hint 函数内 defer recover() 防止 panic,且对无法识别的对象类型明确 return QueueSkip, nil。

Q4. 怎么确认 hint 函数真的被调用了?

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 切片)。

Q5. activeQ 是有界还是无界?

无界。activeQ 没有容量上限,靠 backoffQ 的退避和 unschedulablePods 的LRU 淘汰做软限制。如果生产中看到 activeQ 堆积,说明调度器处理速度跟不上入队速度,常见原因:每个 Pod 调度耗时过长(如 InterPodAffinity 在大集群),需要排查 metrics scheduler_queue_incoming_pods_totalscheduler_pod_scheduling_duration_seconds

Q6. unschedulablePods 满了怎么办?

Pod 在 unschedulablePods 待超过 podMaxInUnschedulablePodsDuration(默认 5 分钟scheduling_queue.go:66)后,会被 flushUnschedulablePodsLeftover()scheduling_queue.go:443,30 秒一轮)强行搬到 backoffQ。这是兜底,确保 QueueingHint 漏报时 Pod 也能重试。生产中如果你怀疑漏报,调小这个值比"重启 scheduler"更优雅。

Q7. 怎么关闭 QueueingHint?

v1.34 起无法关闭(LockToDefault)。v1.32 还能用 --feature-gates=SchedulerQueueingHints=false 关闭。生产集群升级到 v1.34 后,所有 hint 都会被强制执行。建议在升级前先在 staging 用 --feature-gates=SchedulerQueueingHints=true 试跑 1~2 周。

Q8. QueueingHint 和 EventHandler 是什么关系?

EventHandlereventhandlers.go)是 apiserver watch 事件的入口:所有 Node/Pod/PVC 事件先经过 EventHandler,转成 MoveAllToActiveOrBackoffQueue 调用。QueueingHint 是 EventHandler 触发后的精筛器:决定"哪些 Pod 值得被搬"。EventHandler 总是被调用,但 QueueingHint 可以拒绝重排。

Q9. backoffQ 的退避时间在哪里调?

通过 KubeSchedulerConfigurationpodInitialBackoffSeconds(默认 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 完成后"撞车"重排。

Q10. OpportunisticBatching 在 v1.36.1 是默认开启的吗?

v1.33 起 Beta 默认开启(kube_features.go:720),v1.36.1 仍为 Beta。该特性依赖每个插件的 SignPod() 实现;如果某个插件没实现,调度器自动跳过该插件的签名,不影响其他插件生效。

Q11. Pod 被调度过程中被删除会怎样?

调度器监听 Pod 事件:Delete 事件触发 PriorityQueue.Delete()scheduling_queue.go:127),从所有队列(activeQ / backoffQ / unschedulablePods)按 UID 删除。如果 Pod 当时在 InFlight(已 Pop 未 Done),靠 Activate() 的 wildcard 机制重新调度(scheduling_queue.go:495-513)。

Q12. QueueingHint 的 eventsToRegister 多还是少好?

少而精。v1.34+ 引入了细粒度事件framework/types.go:46-74),比如 UpdatePodToleration 代替通用 UpdateUpdateNodeLabel 代替通用 UpdateNodeXYZ用窄事件:hint 函数被调用的频率低,但每次判断更准。避免注册 {Resource: Pod, ActionType: All},会让 hint 函数每次 Pod 事件都被调用。

Q13. 怎么确认 Node 添加事件触发了重排?

--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 写得过于严格

Q14. scheduler 启动时 QueueingHint 怎么初始化?

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)。

Q15. defaultQueueingHintFn 是干嘛的?

defaultQueueingHintFnpkg/scheduler/scheduler.go:472)是一个永远返回 Queue的占位 hint,用于插件不提供 hint 时的兜底(让事件触发不会"无 hint 可用")。

Q16. unschedulablePods 的 map key 是什么?

Pod 的 types.UIDunschedulable_pods.go)。这样保证同一 Pod 的多次失败合并而不是堆叠 100 个 entry。

Q17. 怎么给自研插件加 QueueingHint?

让你的插件实现 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{}  // 编译期断言

Q18. QueuingHint 与 QueuingHintFn 的命名关系?

QueueingHint返回值类型QueueSkip / Queueframework/types.go:209-218);QueueingHintFn函数类型framework/types.go:207func(logger, pod, oldObj, newObj) (QueueingHint, error))。命名上确实容易混淆,但读源码时记"Fn 后缀是函数"即可。

Q19. SchedulerPopFromBackoffQ 和 QueueingHint 冲突吗?

不冲突,是正交的两个特性:

  • QueueingHint 决定"该不该重排"
  • SchedulerPopFromBackoffQ 决定"从哪儿取 Pod"(activeQ ∪ backoffQ)

v1.36.1 默认两者都开。生产中如果看到调度抖动(Pop 频繁切换队列),可能是 backoffQ 中有大量"快到期"Pod 在抢调度器资源,可以考虑关 SchedulerPopFromBackoffQ

Q20. podSigners 在调度器里怎么用?

v1.32+ OpportunisticBatching 启用时:

  1. scheduler.New() 把每个 profile 的 SignPod 收集到 podSigners map(scheduler.go:407-411
  2. 传给 WithPodSigners()scheduling_queue.go:336-343)注入到 PriorityQueue
  3. 每次 Pop 一个 Pod 时计算签名,与上一次 Pop 的签名比对
  4. 若相同所有 SignPlugin 都同意复用结果 → 跳过 Filter/Score

Q21. QueueingHint 的多 profile 是怎么处理的?

QueueingHintMapPerProfilescheduling_queue.go:295)是 map[profileName]QueueingHintMap。每个 Pod 按 pod.Spec.SchedulerName 查自己 profile 的 hint map(scheduling_queue.go:515)。这样多 profile 集群不会互相干扰 hint 判断。

Q22. 怎么知道 Pod 当前在哪个队列?

通过 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

Q23. QueueingHint 对老插件是向后兼容的吗?

。v1.27 时代插件不实现 EnqueueExtensions 也能用,调度器把它们的失败 Pod 放到 unschedulablePods 时不带 plugin 关联,重排时按"无 rejected plugins"处理(scheduling_queue.go:490),退回 backoff 兜底路径。这是兼容老插件的关键设计。

Q24. hint 函数能读 ClusterEvent 的 Resource 吗?

不能。hint 函数的签名是 func(logger, pod, oldObj, newObj),事件本身作为参数传入(framework/types.go:207)。需要根据 oldObj / newObj 的类型判断事件类型:*v1.Node 表示 Node 事件,*v1.Pod 表示 Pod 事件。

Q25. queueingHintMap 是线程安全的吗?

queueingHintMap 在 PriorityQueue 构造时填好后只读,没有并发修改入口。pluginToEventsMap 同理。读时不需要加锁(scheduling_queue.go:466-473isEventOfInterest 无锁读)。这种"init-only"设计是 v1.36.1 简化并发模型的关键。

九、Roadmap:调度专题后续预告

本篇拆解了 SchedulingQueue 的三段队列与 v1.36.1 GA 的 QueueingHint。调度专题接下来还有几条主线:

  • 多 Profile 与 Coordinated LeaderElection:同集群多调度器、LeaseCandidate、协调选主
  • Out-of-Tree 插件实战:从零写一个 Score 插件并发布到 Helm chart
  • 抢占(DefaultPreemption)机制:调度失败时如何挪走低优先级 Pod
  • DRA(Dynamic Resource Allocation)与 Scheduling:v1.36 起的新资源维度对调度的影响

调度专题的目标读者是资深运维开发:能读 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 初始化