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

推荐订阅源

SecWiki News
SecWiki News
量子位
The Cloudflare Blog
美团技术团队
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 三生石上(FineUI控件)
T
Tor Project blog
博客园 - 司徒正美
宝玉的分享
宝玉的分享
T
Threatpost
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
T
Threat Research - Cisco Blogs
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
博客园 - 聂微东
A
Arctic Wolf
I
Intezer
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
小众软件
小众软件
T
Tailwind CSS Blog
The Hacker News
The Hacker News
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
TaoSecurity Blog
TaoSecurity Blog
Project Zero
Project Zero
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cloudbric
Cloudbric
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Troy Hunt's Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
V2EX - 技术
V2EX - 技术
The GitHub Blog
The GitHub Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog

博客园 - 左扬

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(调度专题 · 五):SchedulingQueue 与 QueueingHint —— 三段队列的细节、v1.36 新引入的 QueueingHint 工作机制 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 四):抢占(Preemption)算法剖析 —— DefaultPreemption 如何选 victim、PodDisruptionBudget 如何约束 Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 二):内置插件逐个精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity 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 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件 逐个精读
左扬 · 2026-06-19 · via 博客园 - 左扬

Kubernetes 源码 / Operator 专题【左扬精讲】——kube-scheduler(调度专题):调度器内置插件 逐个精读

当你 kubectl apply 一个 Pod 之后,Kubernetes 内部发生了什么事?Pod 是怎么被分配到某个节点上的?为什么同样一份 Deployment,有时会调度到这台机器,有时又跑到另一台?这背后默默工作的核心组件,就是 kube-scheduler调度器)。

本文深入 kube-scheduler 源码,按调度流水线实际触发顺序,逐一拆解 NodeResourcesFitNodeAffinityTaintTolerationInterPodAffinityPodTopologySpreadVolumeBinding 六大核心插件的职责边界与源码结构。本文源码分析基于 k8s v1.36.1,全部以 pkg/scheduler/framework/plugins/ 目录下的实际代码为依据。

Kubernetes Scheduler Scheduling Framework 事件驱动 Go k8s v1.36.1


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

★ 重点掌握(必须)

  • 两阶段决策模型:Filter(硬门槛,行/不行)vs Score(软打分,好/更好)
  • 插件名常量集中定义于 pkg/scheduler/framework/plugins/names/names.go L19-43
  • NewInTreeRegistry() 将插件名映射到工厂函数(registry.go L50-79)
  • VolumeBinding 唯一跨越 PreFilter / Filter / Reserve / PreBind 四个阶段
  • UnschedulableAndUnresolvable 立即中断短路,不调用后续插件

☆ 次重点(了解即可)

  • 每个插件的 EventsToRegister 决定哪些事件触发 Pending Pod 重新入队
  • Unschedulable(资源不足,等扩容)vs UnschedulableAndUnresolvable(任何节点都不行)
  • SchedulingQueueHint(v1.28+)通过 QueueingHintFn 减少无效重试,降低调度延迟

一、调度框架的演进:为什么拆成多个插件

早期 k8s 调度器(v1.14 及之前)是一个 monolithic 大函数,内置几十个 predicate 和 priority 函数,每加一个策略都要改主循环代码。这带来三个根本痛点:改一个策略必须重新编译整个调度器二进制;用户的特殊调度需求无法动态扩展;所有 predicate 必须串行跑完才能进 priority 阶段,没有短路能力。

从 v1.15 起,调度器重构成 Scheduler Framework:将"过滤 / 打分 / 绑定"切成固定扩展点,每个扩展点可挂载多个插件。内置插件是出厂预装,第三方可以 OutOfTree 方式注册自己的插件。

1.1 传统调度器 vs 框架方案对比

维度传统 monolithic(v1.14 及之前)Scheduler Framework + 内置插件(v1.15+)
扩展性 改源码 + 重新编译 OutOfTree 注册自定义插件
代码组织 所有 predicate/priority 堆在主文件 每个插件独占一个目录
替换单个策略 无法实现 SchedulerConfiguration 中 disablePlugin 禁用
短路能力 所有 predicate 必须跑完 UnschedulableAndUnresolvable 立刻中断
调试可见性 日志看不到哪个 predicate 失败 每个插件返回 Status 带 reason,describe pod 可看
扩展点数量 硬编码:预选 -> 优选 -> 绑定 14 个扩展点(PreEnqueue ~ Bind)

注意

不要直接修改 kube-scheduler 源码来定制内置插件的行为。正确做法是在 SchedulerConfiguration 中 disablePlugin: - NodeResourcesFit 后,实现自己的同名插件 OutOfTree 注册。直接改源码会让集群脱离上游,无法升级。

二、插件总览与调度流水线

2.1 两阶段决策模型

调度器对每个 Pod 的决策分为两个阶段:

TEXT

┌──────────────────────────────────────────────────────────────┐ │ Filter(硬门槛,行/不行) │ │ │ │ NodeResourcesFit ──► NodeAffinity ──► TaintToleration │ │ │ │ │ │ │ 资源够吗? 节点 label 匹配? 容忍污点吗? │ │ │ │ 任一插件返回 UnschedulableAndUnresolvable ──► 短路跳过 │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Score(软打分,好/更好) │ │ │ │ NodeResourcesFit ──► NodeAffinity ──► TaintToleration │ │ InterPodAffinity ──► PodTopologySpread ──► VolumeBinding │ │ │ │ ▼ NormalizeScore(归一化到 0-MaxNodeScore) │ │ 所有分数加权求和 ──► 选出分数最高节点 ──► Bind │ └──────────────────────────────────────────────────────────────┘

2.2 插件名称常量(names/names.go, L19-43)

所有内置插件的名字都定义在同一 const 块中,NewInTreeRegistry() 通过这个名字映射到对应的工厂函数:

pkg/scheduler/framework/plugins/names/names.go (L19-43, k8s v1.36.1)

TEXT

1const (

2 PrioritySort = "PrioritySort"

3 DefaultBinder = "DefaultBinder"

4 DefaultPreemption = "DefaultPreemption"

5 DynamicResources = "DynamicResources"

6 ImageLocality = "ImageLocality"

7 InterPodAffinity = "InterPodAffinity"

8 NodeAffinity = "NodeAffinity"

9 NodeResourcesBalancedAllocation = "NodeResourcesBalancedAllocation"

10 NodeResourcesFit = "NodeResourcesFit"

11 NodeUnschedulable = "NodeUnschedulable"

12 PodTopologySpread = "PodTopologySpread"

13 SchedulingGates = "SchedulingGates"

14 TaintToleration = "TaintToleration"

15 VolumeBinding = "VolumeBinding"

16 // ... 共 22 个内置插件,完整定义见 names.go L19-43

17)

2.3 插件注册表(registry.go, L50-79)

pkg/scheduler/framework/plugins/registry.go (L50-79, k8s v1.36.1)

TEXT

1func NewInTreeRegistry() runtime.Registry {

2 fts := plfeature.NewSchedulerFeaturesFromGates(feature.DefaultFeatureGate)

3 registry := runtime.Registry{

4 imagelocality.Name: imagelocality.New,

5 tainttoleration.Name: runtime.FactoryAdapter(fts, tainttoleration.New),

6 nodeaffinity.Name: runtime.FactoryAdapter(fts, nodeaffinity.New),

7 podtopologyspread.Name: runtime.FactoryAdapter(fts, podtopologyspread.New),

8 noderesources.Name: runtime.FactoryAdapter(fts, noderesources.NewFit),

9 volumebinding.Name: runtime.FactoryAdapter(fts, volumebinding.New),

10 interpodaffinity.Name: runtime.FactoryAdapter(fts, interpodaffinity.New),

11 // ... 共 25 个插件,完整注册见 registry.go L50-79

12 }

13 return registry

14}

补充

每个插件工厂函数被 runtime.FactoryAdapter 包裹,以支持 feature gate 动态控制插件启用状态。feature gate 关闭时,对应插件工厂返回 nil,插件不被实例化。

三、六大插件逐个精读

3.1 NodeResourcesFit

核心职责:判断节点 CPU / 内存 / GPU / 临时存储是否满足 Pod 的 resource.requests。资源不够时 Filter 直接返回 Unschedulable。Score 阶段越闲的节点分越高(默认 LeastAllocated 策略)。

pkg/scheduler/framework/plugins/noderesources/fit.go (k8s v1.36.1)

TEXT

1// Fit struct(L92-105)

2type Fit struct {

3 ignoredResources sets.Set[string]

4 ignoredResourceGroups sets.Set[string]

5 enableInPlacePodVerticalScaling bool

6 enableSidecarContainers bool

7 enableSchedulingQueueHint bool

8 enablePodLevelResources bool

9 enableDRAExtendedResource bool

10 enableInPlacePodLevelResourcesVerticalScaling bool

11 handle fwk.Handle

12 *resourceAllocationScorer

13 placementScorer *resourceAllocationScorer

14}

15

16// PreFilter(L334-346)- 计算 Pod 所有容器的 resource requests

17func (f *Fit) PreFilter(ctx context.Context, cycleState fwk.CycleState,

18 pod *v1.Pod, nodes []fwk.NodeInfo) (*fwk.PreFilterResult, *fwk.Status) {

19 result := computePodResourceRequest(pod, ResourceRequestsOptions{

20 EnablePodLevelResources: f.enablePodLevelResources,

21 })

22 cycleState.Write(preFilterStateKey, result)

23 return nil, nil

24}

25

26// PreScore(L138-155)- 计算 podRequests 存入 CycleState

27func (f *Fit) PreScore(ctx context.Context, cycleState fwk.CycleState,

28 pod *v1.Pod, nodes []fwk.NodeInfo) *fwk.Status {

29 podRequests := f.calculatePodResourceRequestList(pod, f.resources)

30 cycleState.Write(preScoreStateKey, &preScoreState{podRequests: podRequests})

31 return nil

32}

33

34// Score(L768-786)- LeastAllocated 默认策略

35// 公式: score = (capacity - requested) * MaxNodeScore / capacity

36// 越闲 → 分子越大 → 分越高

37func (f *Fit) Score(ctx context.Context, state fwk.CycleState,

38 pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {

39 s, err := getPreScoreState(state)

40 return f.score(ctx, pod, nodeInfo, s.podRequests, s.draPreScoreState)

41}

42

43// EventsToRegister(L368-398)

44// 关键事件:Pod 删除、节点 Add/UpdateNodeAllocatable/UpdateNodeTaint/UpdateNodeLabel

45// 配合 enableSchedulingQueueHint 时,只注册 Add | UpdateNodeAllocatable

补充

LeastAllocated(默认):最闲优先,适合通用计算。MostAllocated:最忙优先,适合批量回收。RequestedToCapacityRatio:自定义资源权重,适合 GPU/FPGA 异构集群。BalancedAllocation:CPU/内存均衡。placementScorer(v1.36.1 新增)用于 Gang Scheduling 场景,按 MostAllocated 策略对 Pod 组副本做资源均衡分配。

TEXT

1# 触发 NodeResourcesFit 的 Pod(k8s v1.36.1)

2apiVersion: v1

3kind: Pod

4metadata:

5 name: resource-demo

6spec:

7 containers:

8 - name: nginx

9 image: nginx:1.25

10 resources:

11 requests:

12 cpu: "2000m"

13 memory: "512Mi"

14 limits:

15 cpu: "4000m"

16 memory: "1Gi"

3.2 NodeAffinity

核心职责:解析 pod.spec.affinity.nodeAffinity 和 pod.spec.nodeSelector,根据节点 label 判断是否匹配。requiredDuringSchedulingIgnoredDuringExecution 是硬条件(Filter),preferredDuringSchedulingIgnoredDuringExecution 是软打分(Score)。

pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go (k8s v1.36.1)

TEXT

1// NodeAffinity struct(L39-44)

2type NodeAffinity struct {

3 handle fwk.Handle

4 addedNodeSelector *nodeaffinity.NodeSelector

5 addedPrefSchedTerms *nodeaffinity.PreferredSchedulingTerms

6 enableSchedulingQueueHint bool

7}

8

9// PreFilter(L159-209)- 解析 requiredDuringSchedulingIgnoredDuringExecution

10func (pl *NodeAffinity) PreFilter(ctx context.Context, cycleState fwk.CycleState,

11 pod *v1.Pod, nodes []fwk.NodeInfo) (*fwk.PreFilterResult, *fwk.Status) {

12 state := &preFilterState{requiredNodeSelectorAndAffinity: nodeaffinity.GetRequiredNodeAffinity(pod)}

13 cycleState.Write(preFilterStateKey, state)

14 if nodeNames != nil && len(nodeNames) == 0 {

15 return nil, fwk.NewStatus(fwk.UnschedulableAndUnresolvable, errReasonConflict)

16 } else if len(nodeNames) > 0 {

17 return &fwk.PreFilterResult{NodeNames: nodeNames}, nil

18 }

19 return nil, nil

20}

21

22// Filter(L216-239)- 硬条件匹配

23func (pl *NodeAffinity) Filter(ctx context.Context, state fwk.CycleState,

24 pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {

25 node := nodeInfo.Node()

26 if pl.addedNodeSelector != nil && !pl.addedNodeSelector.Match(node) {

27 return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, errReasonEnforced)

28 }

29 match, _ := s.requiredNodeSelectorAndAffinity.Match(node)

30 if !match {

31 return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, ErrReasonPod)

32 }

33 return nil

34}

35

36// PreScore(L252-267)- 解析 preferredDuringSchedulingIgnoredDuringExecution

37func (pl *NodeAffinity) PreScore(ctx context.Context, cycleState fwk.CycleState,

38 pod *v1.Pod, nodes []fwk.NodeInfo) *fwk.Status {

39 preferredNodeAffinity, _ := getPodPreferredNodeAffinity(pod)

40 cycleState.Write(preScoreStateKey, &preScoreState{preferredNodeAffinity: preferredNodeAffinity})

41 return nil

42}

43

44// Score(L269-297)- 软偏好打分

45func (pl *NodeAffinity) Score(ctx context.Context, state fwk.CycleState,

46 pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {

47 node := nodeInfo.Node()

48 var count int64

49 if pl.addedPrefSchedTerms != nil {

50 count += pl.addedPrefSchedTerms.Score(node)

51 }

52 s, _ := getPreScoreState(state)

53 if s.preferredNodeAffinity != nil {

54 count += s.preferredNodeAffinity.Score(node)

55 }

56 return count, nil

57}

58

59// NormalizeScore(L299-302)

60func (pl *NodeAffinity) NormalizeScore(ctx context.Context, state fwk.CycleState,

61 pod *v1.Pod, scores fwk.NodeScoreList) *fwk.Status {

62 return helper.DefaultNormalizeScore(fwk.MaxNodeScore, false, scores)

63}

64

65// EventsToRegister(L101-115)

66// 关键:enableSchedulingQueueHint 时只注册 Add | UpdateNodeLabel

67// 无 QHint 时注册 Add | UpdateNodeLabel | UpdateNodeTaint(覆盖预检漏掉的 Add 事件)

TEXT

1# 触发 NodeAffinity 的 Pod(k8s v1.36.1)

2apiVersion: v1

3kind: Pod

4metadata:

5 name: affinity-demo

6spec:

7 nodeSelector:

8 az: cn-east-1

9 affinity:

10 nodeAffinity:

11 requiredDuringSchedulingIgnoredDuringExecution:

12 nodeSelectorTerms:

13 - matchExpressions:

14 - key: node-type

15 operator: In

16 values: [general, compute]

17 preferredDuringSchedulingIgnoredDuringExecution:

18 - weight: 100

19 preference:

20 matchExpressions:

21 - key: az

22 operator: In

23 values: [cn-east-1]

3.3 TaintToleration

核心职责:判断 Pod 的 tolerations 是否覆盖节点的污点。NoSchedule / NoExecute 污点无匹配时 Filter 返回 UnschedulableAndUnresolvable。PreferNoSchedule 污点无匹配时 Score 扣分。NoExecute 的驱逐由 kubelet 处理,不参与调度决策。

pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go (k8s v1.36.1)

TEXT

1// TaintToleration struct(L35-39)

2type TaintToleration struct {

3 handle fwk.Handle

4 enableSchedulingQueueHint bool

5 enableTaintTolerationComparisonOperators bool

6}

7

8// Filter(L119-132)

9func (pl *TaintToleration) Filter(ctx context.Context, state fwk.CycleState,

10 pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {

11 logger := klog.FromContext(ctx)

12 node := nodeInfo.Node()

13 _, isUntolerated := v1helper.FindMatchingUntoleratedTaint(

14 logger, node.Spec.Taints, pod.Spec.Tolerations,

15 helper.DoNotScheduleTaintsFilterFunc(),

16 pl.enableTaintTolerationComparisonOperators)

17 if !isUntolerated {

18 return nil

19 }

20 return fwk.NewStatus(fwk.UnschedulableAndUnresolvable,

21 "node(s) had untolerated taint(s)")

22}

23

24// PreScore(L156-164)- 提取 PreferNoSchedule 类型的 tolerations

25func (pl *TaintToleration) PreScore(ctx context.Context, cycleState fwk.CycleState,

26 pod *v1.Pod, nodes []fwk.NodeInfo) *fwk.Status {

27 tolerationsPreferNoSchedule := getAllTolerationPreferNoSchedule(pod.Spec.Tolerations)

28 cycleState.Write(preScoreStateKey, &preScoreState{tolerationsPreferNoSchedule: tolerationsPreferNoSchedule})

29 return nil

30}

31

32// Score(L195-207)- 计算节点上无法容忍的 PreferNoSchedule 污点数

33func (pl *TaintToleration) Score(ctx context.Context, state fwk.CycleState,

34 pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {

35 logger := klog.FromContext(ctx)

36 s, _ := getPreScoreState(state)

37 score := int64(pl.countIntolerableTaintsPreferNoSchedule(

38 logger, nodeInfo.Node().Spec.Taints, s.tolerationsPreferNoSchedule))

39 return score, nil

40}

41

42// NormalizeScore(L209-212)

43func (pl *TaintToleration) NormalizeScore(ctx context.Context, _ fwk.CycleState,

44 pod *v1.Pod, scores fwk.NodeScoreList) *fwk.Status {

45 return helper.DefaultNormalizeScore(fwk.MaxNodeScore, true, scores)

46}

47

48// EventsToRegister(L70-91)

49// enableSchedulingQueueHint 时:Node Add|UpdateNodeTaint + Pod UpdatePodToleration

50// 无 QHint 时:Node Add|UpdateNodeTaint|UpdateNodeLabel(覆盖预检漏掉的 Add 事件)

TEXT

1# 触发 TaintToleration 的 Pod(k8s v1.36.1)

2apiVersion: v1

3kind: Pod

4metadata:

5 name: taint-demo

6spec:

7 tolerations:

8 - key: "dedicated"

9 operator: "Equal"

10 value: "true"

11 effect: "NoSchedule"

12 - key: "node.kubernetes.io/not-ready"

13 operator: "Exists"

14 effect: "NoExecute"

15 tolerationSeconds: 300

16 - key: "node.kubernetes.io/unreachable"

17 operator: "Exists"

18 effect: "NoExecute"

19 tolerationSeconds: 300

3.4 InterPodAffinity

核心职责:根据 pod.spec.affinity.podAffinity(亲和)和 podAntiAffinity(反亲和)判断 Pod 之间的关系。硬亲和/反亲和在 Filter 阶段强制约束,软打分在 Score 阶段做偏好评估。

pkg/scheduler/framework/plugins/interpodaffinity/ (k8s v1.36.1)

TEXT

1// InterPodAffinity struct(plugin.go L47-53)

2type InterPodAffinity struct {

3 parallelizer fwk.Parallelizer

4 args config.InterPodAffinityArgs

5 sharedLister fwk.SharedLister

6 nsLister listersv1.NamespaceLister

7 enableSchedulingQueueHint bool

8}

9

10// PreFilter(filtering.go L274-312)- 构建拓扑打分数据结构

11func (pl *InterPodAffinity) PreFilter(ctx context.Context, cycleState fwk.CycleState,

12 pod *v1.Pod, allNodes []fwk.NodeInfo) (*fwk.PreFilterResult, *fwk.Status) {

13 state := &preScoreState{topologyScore: make(map[string]map[string]int64)}

14 // 处理 podAffinity + podAntiAffinity 的拓扑打分

15 cycleState.Write(preScoreStateKey, state)

16 return nil, nil

17}

18

19// Filter(filtering.go L412-440)- 三层判断

20func (pl *InterPodAffinity) Filter(ctx context.Context, cycleState fwk.CycleState,

21 pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {

22 state, _ := getPreFilterState(cycleState)

23 if !satisfyPodAffinity(state, nodeInfo) {

24 return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, ErrReasonAffinityRulesNotMatch)

25 }

26 if !satisfyPodAntiAffinity(state, nodeInfo) {

27 return fwk.NewStatus(fwk.Unschedulable, ErrReasonAntiAffinityRulesNotMatch)

28 }

29 if !satisfyExistingPodsAntiAffinity(state, nodeInfo) {

30 return fwk.NewStatus(fwk.Unschedulable, ErrReasonExistingAntiAffinityRulesNotMatch)

31 }

32 return nil

33}

34

35// Score(scoring.go L240-255)

36func (pl *InterPodAffinity) Score(ctx context.Context, cycleState fwk.CycleState,

37 pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {

38 node := nodeInfo.Node()

39 s, _ := getPreScoreState(cycleState)

40 var score int64

41 for tpKey, tpValues := range s.topologyScore {

42 if v, exist := node.Labels[tpKey]; exist {

43 score += tpValues[v]

44 }

45 }

46 return score, nil

47}

48

49// NormalizeScore(scoring.go L258-290)- 归一化到 [0, MaxNodeScore]

50// maxMinDiff = maxCount - minCount,归一化公式同 PodTopologySpread

51

52// EventsToRegister(plugin.go L96-103)

53// enableSchedulingQueueHint 时:Pod Add|UpdatePodLabel|Delete + Node Add|UpdateNodeLabel

54// 无 QHint 时:Pod Add|UpdatePodLabel|Delete + Node Add|UpdateNodeLabel|UpdateNodeTaint

TEXT

1# 触发 InterPodAffinity 的 Pod(k8s v1.36.1)

2apiVersion: v1

3kind: Pod

4metadata:

5 name: web-pod

6spec:

7 affinity:

8 podAntiAffinity:

9 requiredDuringSchedulingIgnoredDuringExecution:

10 - labelSelector:

11 matchLabels:

12 app: redis-cache

13 topologyKey: topology.kubernetes.io/zone

14 podAffinity:

15 preferredDuringSchedulingIgnoredDuringExecution:

16 - weight: 100

17 podAffinityTerm:

18 labelSelector:

19 matchLabels:

20 app: web-frontend

21 topologyKey: topology.kubernetes.io/zone

3.5 PodTopologySpread

核心职责:解析 pod.spec.topologySpreadConstraints,让同一组 Pod 按 maxSkew 均匀分布在 zone / node 等拓扑域里。v1.18 引入,替代手动写反亲和规则。对没有声明约束的 Pod,会自动注入 system-defaulted 约束(v1.27+)。

pkg/scheduler/framework/plugins/podtopologyspread/plugin.go (k8s v1.36.1)

TEXT

1// PodTopologySpread struct(plugin.go L60-73)

2type PodTopologySpread struct {

3 systemDefaulted bool

4 parallelizer fwk.Parallelizer

5 defaultConstraints []v1.TopologySpreadConstraint

6 sharedLister fwk.SharedLister

7 services corelisters.ServiceLister

8 replicaSets appslisters.ReplicaSetLister

9 statefulSets appslisters.StatefulSetLister

10 enableNodeInclusionPolicyInPodTopologySpread bool

11 enableMatchLabelKeysInPodTopologySpread bool

12 enableSchedulingQueueHint bool

13 enableTaintTolerationComparisonOperators bool

14}

15

16// system-defaulted 约束(plugin.go L46-57)

17// 当 SchedulerConfiguration 的 defaultingType = SystemDefaulting 时自动注入

18var systemDefaultConstraints = []v1.TopologySpreadConstraint{

19 {TopologyKey: v1.LabelHostname, WhenUnsatisfiable: v1.ScheduleAnyway, MaxSkew: 3},

20 {TopologyKey: v1.LabelTopologyZone, WhenUnsatisfiable: v1.ScheduleAnyway, MaxSkew: 5},

21}

22

23// PreFilter(filtering.go L139-152)

24// 若 defaultingType = SystemDefaulting 且 Pod 无约束,注入 systemDefaultConstraints

25// 若 WhenUnsatisfiable = DoNotSchedule,返回 NodeNames 候选集合

26

27// Filter(filtering.go L314-380)

28// skew = count(当前拓扑域匹配 Pod) - min(所有拓扑域匹配 Pod)

29// 超过 maxSkew → UnschedulableAndUnresolvable

30

31// Score(scoring.go L199-226)- 打分公式

32func (pl *PodTopologySpread) Score(ctx context.Context, cycleState fwk.CycleState,

33 pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {

34 var score float64

35 for i, c := range s.Constraints {

36 if tpVal, ok := node.Labels[c.TopologyKey]; ok {

37 cnt := int64(countPodsMatchSelector(...))

38 score += scoreForCount(cnt, c.MaxSkew, s.TopologyNormalizingWeight[i])

39 }

40 }

41 return int64(math.Round(score)), nil

42}

43

44// NormalizeScore(scoring.go L229-270)

45// 公式: fScore = MaxNodeScore * (score - minScore) / (maxScore - minScore)

TEXT

# PodTopologySpread skew 计算示例 假设 3 个 zone,各有 Pod 数: zone-a: 5 个 Pod ← skew = 5 - 2 = 3 ← 超过 maxSkew zone-b: 2 个 Pod ← skew = 2 - 2 = 0 ← OK zone-c: 2 个 Pod ← skew = 2 - 2 = 0 ← OK 若 maxSkew = 2: → zone-a skew=3 超过 maxSkew,新 Pod 无法调度到 zone-a → 只能调度到 zone-b 或 zone-c 若 whenUnsatisfiable = ScheduleAnyway: → 允许调度到 zone-a,但记录拓扑违规(分数降低)

TEXT

1# 触发 PodTopologySpread 的 Pod(k8s v1.36.1)

2apiVersion: v1

3kind: Pod

4metadata:

5 name: topology-spread-demo

6spec:

7 topologySpreadConstraints:

8 - maxSkew: 1

9 topologyKey: topology.kubernetes.io/zone

10 whenUnsatisfiable: DoNotSchedule

11 labelSelector:

12 matchLabels:

13 app: demo-app

14 - maxSkew: 2

15 topologyKey: topology.kubernetes.io/hostname

16 whenUnsatisfiable: ScheduleAnyway

17 labelSelector:

18 matchLabels:

19 app: demo-app

3.6 VolumeBinding

核心职责:唯一横跨四个调度阶段的内置插件。解决"并发调度时 PV 被两个 Pod 同时抢占"的并发安全问题。核心机制是 AssumeCache:PreFilter 阶段"先假设能绑",Reserve 占位,PreBind 提交真实绑定,冲突时 Unreserve 回退。

pkg/scheduler/framework/plugins/volumebinding/ (k8s v1.36.1)

TEXT

1// VolumeBinding struct(volume_binding.go L73-79)

2type VolumeBinding struct {

3 Binder SchedulerVolumeBinder

4 PVCLister corelisters.PersistentVolumeClaimLister

5 classLister storagelisters.StorageClassLister

6 scorer volumeCapacityScorer

7 fts feature.Features

8}

9

10// SchedulerVolumeBinder 接口(binder.go L153-196)

11type SchedulerVolumeBinder interface {

12 GetPodVolumeClaims(pod) (*PodVolumeClaims, error)

13 FindPodVolumes(pod, node) (*PodVolumes, ConflictReasons, error)

14 AssumePodVolumes(assumedPod, nodeName, volumes) (allFullyBound bool, err error)

15 RevertAssumedPodVolumes(volumes)

16 BindPodVolumes(ctx, assumedPod, volumes) error

17}

18

19// PreFilter(volume_binding.go L360-390)

20// 获取 Pod 所有 PVC,分为 bound / unboundImmediate / unboundDelayBinding

21// unboundImmediate 未绑定 → 返回 UnschedulableAndUnresolvable

22// 无 PVC → 写空 stateData,返回 Skip

23

24// Filter(volume_binding.go L424-453)

25func (pl *VolumeBinding) Filter(ctx context.Context, cs fwk.CycleState,

26 pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {

27 state, _ := getStateData(cs)

28 podVolumes, reasons, _ := pl.Binder.FindPodVolumes(logger, pod, state.podVolumeClaims, node)

29 if len(reasons) > 0 {

30 return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, reasons...)

31 }

32 state.podVolumesByNode[node.Name] = podVolumes

33 return nil

34}

35

36// Reserve(volume_binding.go L531-549)

37func (pl *VolumeBinding) Reserve(ctx context.Context, cs fwk.CycleState,

38 pod *v1.Pod, nodeName string) *fwk.Status {

39 state, _ := getStateData(cs)

40 podVolumes := state.podVolumesByNode[nodeName]

41 allBound, _ := pl.Binder.AssumePodVolumes(logger, pod, nodeName, podVolumes)

42 state.allBound = allBound

43 return nil

44}

45

46// PreBind(volume_binding.go L577-604)

47// 同步等待 PV 控制器完成真实绑定,失败则触发 Unreserve 回退

48

49// Unreserve(volume_binding.go L604-615)

50// 绑定失败时调用 RevertAssumedPodVolumes 回退假设状态

51

52// Score(volume_binding.go L471-530)

53// StorageClass 剩余容量打分,需要 --feature-gates=StorageCapacityScoring=true(v1.36.1 为 alpha)

54

55// EventsToRegister(volume_binding.go L107-143)

56// StorageClass/PVC/PV Add|Update / Node Add|UpdateNodeLabel|UpdateNodeTaint

57// CSINode Add|Update / CSIDriver Update / CSIStorageCapacity Add|Update

注意

PreFilter 阶段调用 GetPodVolumeClaims 把 PVC 分类。Reserve 阶段调用 AssumePodVolumes 把 PVC 信息写入 AssumeCache 占位。PreBind 阶段调用 BindPodVolumes 同步等待 PV 控制器完成真实绑定。如果另一个 Pod 在此期间已经真正绑定了这个 PV,Unreserve 阶段会调用 RevertAssumedPodVolumes 回退。这就是 VolumeBinding 要跨越四个阶段的原因——解决"并发调度时 PV 抢用"的 race condition。PreBindPreFlight(L555)用于判断是否需要真正执行绑定,跳过无 volume 的 Pod。

TEXT

1# 触发 VolumeBinding 的 Pod(k8s v1.36.1)

2apiVersion: v1

3kind: Pod

4metadata:

5 name: volume-demo

6spec:

7 containers:

8 - name: app

9 image: nginx

10 volumeMounts:

11 - name: data

12 mountPath: /usr/share/nginx/html

13 volumes:

14 - name: data

15 persistentVolumeClaim:

16 claimName: my-pvc

17

18---

19apiVersion: storage.k8s.io/v1

20kind: StorageClass

21metadata:

22 name: fast-ssd

23provisioner: pd.csi.storage.gke.io

24parameters:

25 type: pd-ssd

26volumeBindingMode: WaitForFirstConsumer

27allowVolumeExpansion: true

四、插件对比总结表

插件Filter 职责Score 职责跨越阶段核心数据结构
NodeResourcesFit CPU/内存/GPU/临时存储是否足够(L334+) LeastAllocated 最闲优先(L768) PreFilter / Filter / PreScore / Score preFilterState / preScoreState
NodeAffinity nodeSelector + nodeAffinity required 硬匹配(L216) preferred 软偏好打分(L269) PreFilter / Filter / PreScore / Score nodeaffinity.NodeSelector(L90-92)
TaintToleration NoSchedule/NoExecute 污点容忍判断(L119) PreferNoSchedule 污点越多分越低(L195) Filter / PreScore / Score Toleration[] vs NodeSpec.Taints[]
InterPodAffinity 硬亲和/反亲和拓扑域匹配(filtering.go L412) 软亲和/反亲和加权打分(scoring.go L240) PreFilter / Filter / PreScore / Score topologyScore(scoring.go L166-168)
PodTopologySpread 拓扑域 skew 是否超过 maxSkew(filtering.go L314) 匹配 Pod 数越少分越高(scoring.go L199) PreFilter / Filter / PreScore / Score preScoreState / systemDefaultConstraints(plugin.go L46)
VolumeBinding PV 节点亲和/容量/拓扑匹配(volume_binding.go L424) StorageClass 剩余容量打分(L471,alpha) PreFilter / Filter / Reserve / PreBind / PreScore / Score SchedulerVolumeBinder / stateData(L53-64)

五、生产踩坑实录

坑一:NodeResourcesFit 漏算了 Pod 级 resources(生产高危)

问题现象

Pod 申请了 GPU 资源(nvidia.com/gpu: 1),节点确认有 GPU,但调度反复报 Insufficient nvidia.com/gpu,Pod 始终 Pending。

根因:v1.26 之前 NodeResourcesFit 读取的是 container[].resources.limits 计算资源;v1.26 起改为读取 container[].resources.requests。如果 Pod 只写了 limits 没写 requests,v1.26+ 的行为与之前不一致。

TEXT

# 查看节点真实 Allocatable kubectl describe node <node-name> | grep -A5 "Allocated resources" # 查看 Pod 实际 requests/limits kubectl get pod <pod-name> -o jsonpath='{.spec.containers[*].resources}'

修复:

TEXT

containers: - name: gpu-worker resources: requests: nvidia.com/gpu: "1" limits: nvidia.com/gpu: "1"

坑二:NodeAffinity required 条件与节点 label 同步延迟

问题现象

Pod 的 nodeSelector 写了 node-type: general,节点确认打上了 label,但调度一直失败。

根因:调度器通过 node informer 同步节点 label。如果 Pod 提交后、调度前 label 刚更新但 informer 还未同步完成,调度器看到的是旧数据。Filter 阶段匹配失败。

修复:使用 preferredDuringSchedulingIgnoredDuringExecution 替代硬匹配,或确保 Pod 提交前 label 已存在。

坑三:InterPodAffinity 硬亲和导致死锁

问题现象

两个 Deployment 互为 required 硬亲和,结果双方各有部分 Pod 调度成功,但始终有部分 Pod 一直 Pending。

根因:requiredDuringSchedulingIgnoredDuringExecution 的硬亲和在 Filter 阶段强制要求拓扑域内已有匹配 Pod。初始部署时双方 Pod 互相依赖,出现"鸡生蛋蛋生鸡"的死锁。

修复:将 required 改为 preferred,降低初始部署阶段的耦合度。

坑四:PodTopologySpread 隐式注入 system-defaulted 约束

问题现象

Pod 没有写任何 topologySpreadConstraints,但调度器仍然给它应用了 zone 打散约束,导致 Pod 被调度到不符合业务预期的 zone。

根因:v1.27+ 中,当 Pod 不声明约束且 SchedulerConfiguration 的 defaultingType = SystemDefaulting 时,PodTopologySpread 自动注入 hostname skew=3 和 zone skew=5 两个 system-defaulted 约束(plugin.go L46-57)。

修复:显式声明自己的约束覆盖默认行为,或在 SchedulerConfiguration 中把 defaultingType 设为 CustomDefaulting 并清空 defaultConstraints。

坑五:VolumeBinding AssumeCache 导致的调度循环

问题现象

使用 WaitForFirstConsumer PVC 的 Pod 反复调度、始终不绑定,卡在 Pending。

根因:VolumeBinding 在 PreFilter 阶段假设 PVC 能绑定。如果 StorageClass 是 WaitForFirstConsumer,真正绑定要等 Pod 调度到节点后才触发。假设期间节点信息变化(污点/label 更新),AssumeCache 里的假设失效,导致调度循环。

修复:确保 Pod 的 nodeSelector 或亲和性先选定候选节点,再让 PVC 在该节点附近绑定。

六、FAQ 高频答疑(分类版)

6.1 基础概念(5 题)

Q1: Filter 和 Score 的本质区别是什么?

Answer:Filter 是"行 / 不行"的二元判断,返回 Unschedulable 或 nil。任何 Filter 返回 UnschedulableAndUnresolvable 时调度立即短路,不再调用后续 Filter 和所有 Score。Score 是"好 / 更好"的打分,返回 0-MaxNodeScore 的整数值,所有 Score 分数加权求和后决定节点最终排名。Filter 是硬约束,Score 是软偏好。

Q2: Unschedulable 和 UnschedulableAndUnresolvable 的差别?

Answer:Unschedulable 表示当前节点不行,但其他节点可能行(如资源不够,扩容节点就能解决),调度器会等事件触发重试。UnschedulableAndUnresolvable 表示不仅当前节点不行,任何节点都不行(如 PVC 不存在),调度器直接让 Pod 进入 Unschedulable 队列,不做无意义的重试。

Q3: EventsToRegister 和调度队列重试是什么关系?

Answer:每个插件在 EventsToRegister 中声明自己关心哪些 Kubernetes 事件。当这些事件发生时(如节点 label 更新、Pod 删除、PV 绑定完成),调度器的 SchedulingQueue 把等待中的 Pod 重新入队触发重试,如果没有注册对应事件,即使资源释放了 Pod 也不会被重新调度。

Q4: 为什么 VolumeBinding 是唯一跨越四个阶段的内置插件?

Answer:因为它需要解决并发调度时的 PV 抢用问题。PreFilter 假设能绑(AssumePodVolumes),Reserve 占住节点,PreBind 提交真实绑定。如果另一个 Pod 在此期间已经真正绑定了这个 PV,Unreserve 阶段会发现冲突并 RevertAssumedPodVolumes 回退。这个两阶段设计(假设 + 确认)是并发安全的核心。

Q5: NodeResourcesFit 的四个 Score 策略分别适合什么场景?

Answer:LeastAllocated(默认):最闲优先,适合通用计算。MostAllocated:最忙优先,适合批量回收。RequestedToCapacityRatio:自定义每种资源权重,适合 GPU/FPGA 异构集群。BalancedAllocation:CPU/内存均衡,适合计算密集型。

6.2 源码原理(8 题)

Q6: PodTopologySpread 的 skew 公式是什么?

Answer:skew = count(当前拓扑域匹配 Pod) - min(所有拓扑域匹配 Pod)。例如 zone-a 有 5 个 Pod,zone-b 有 2 个,zone-c 有 2 个,则 zone-a 的 skew=5-2=3。如果 maxSkew=2,zone-a 超过约束,新 Pod 无法调度到 zone-a。评分公式为 score += scoreForCount(cnt, maxSkew, weight)(scoring.go L222),最终 NormalizeScore 归一化到 [0, MaxNodeScore](scoring.go L229-270)。

Q7: TaintToleration 的 tolerationSeconds 只对哪种 effect 有效?

Answer:只对 NoExecute effect 有效。如果节点被打上 NoExecute 污点,有 tolerationSeconds: 300 的 Pod 会等 300 秒后再驱逐(给节点恢复或 Pod 迁移留出缓冲)。NoSchedule 和 PreferNoSchedule 不支持此字段。

Q8: NodeAffinity 的 addedNodeSelector 和 addedPrefSchedTerms 是什么?

Answer:这两个字段用于 SchedulerConfiguration 级别的 NodeAffinity 扩展(node_affinity.go L41-42)。可以在配置中为所有 Pod 统一追加节点选择器和优先调度条件,让所有 Pod 自动继承额外的亲和性规则,而不需要每个 Pod 单独配置。

Q9: InterPodAffinity 的 Filter 阶段具体计算什么?

Answer:对硬亲和(requiredDuringSchedulingIgnoredDuringExecution)的每个 topologyTerm,统计当前节点所在拓扑域内已运行 Pod 的加权匹配数。Filter 阶段分三层判断(filtering.go L412-440):satisfyPodAffinity、satisfyPodAntiAffinity、satisfyExistingPodsAntiAffinity,任一层不满足就过滤。软打分(preferred)不参与 Filter。

Q10: VolumeBinding 的 SchedulerVolumeBinder 接口有哪些核心方法?

Answer:五个核心方法(binder.go L153-196):GetPodVolumeClaims 获取 Pod 所需 PVC;FindPodVolumes 查找匹配的 PV 并返回冲突原因;AssumePodVolumes 预占 PV(AssumeCache);RevertAssumedPodVolumes 绑定失败时回退;BindPodVolumes 在 PreBind 阶段提交真实绑定到 etcd 并同步等待 PV 控制器完成。

Q11: enableSchedulingQueueHint(调度队列提示)功能是什么?

Answer:v1.28+ beta 特性。每个插件在 EventsToRegister 中除了声明关心的 Kubernetes 事件,还可以注册 QueueingHintFn 智能判断"某个事件发生后,Pod 是否真的需要重新调度"。避免每次节点 label 变化都盲目重试所有 Pending Pod。这能显著减少无效调度循环,降低调度延迟和 API server 压力。

Q12: PodTopologySpread 的 systemDefaulted 字段何时为 true?

Answer:当 Pod 没有声明任何 topologySpreadConstraints 且 SchedulerConfiguration 的 defaultingType = SystemDefaulting(v1.27+ 默认)时,New() 函数(plugin.go L126-129)将 systemDefaultConstraints 自动注入,并将 systemDefaulted 设为 true。不想自动注入可设为 CustomDefaulting 并清空 defaultConstraints。

Q13: VolumeBinding 的 Score 策略 volumeCapacityScoring 是什么?

Answer:VolumeBinding 的 Score 目标不是节点资源,而是指定 StorageClass 的剩余卷容量。按 StorageClass 聚合后采用 broken linear function 公式(volume_binding.go L486-530),节点剩余容量越多得分越高。需要 --feature-gates=StorageCapacityScoring=true 开启(v1.36.1 中为 alpha,默认关闭)。

6.3 生产实践(9 题)

Q14: 如何确认某个 Pod 最终由哪个插件决定了调度失败?

Answer:kubectl describe pod <pod-name> 的 Events 会按时间顺序列出调度失败原因(如 0/3 nodes are available: 1 node(s) had taints..., 2 node(s) resource fit failed)。开启调度器详细日志 --v=5,每个 Filter 插件的通过/失败都有记录。

Q15: 如何禁用某个内置插件而不影响其他插件?

Answer:在 SchedulerConfiguration 的 profiles[].plugins.multiPoint.disabled 中禁用指定插件名:disabledPlugin: - TaintToleration。禁用后该插件在所有 Profile 所有扩展点都不会被调用。如果只想禁用 Score 而保留 Filter,需要在 Score 扩展点单独配置。

Q16: InterPodAffinity 和 PodTopologySpread 都能做 Pod 打散,区别是什么?

Answer:InterPodAffinity 通过 pod.spec.affinity.podAntiAffinity 做两两 Pod 之间的精确控制,适合"Pod A 必须和 Pod B 在一起/分开"的场景。PodTopologySpread 是 v1.18 引入的声明式拓扑打散,通过 topologySpreadConstraints 自动计算全局最优分布,适合"让 N 个副本均匀散开"的场景。两者可以叠加使用。

Q17: 如何解决 InterPodAffinity 硬亲和导致的新建 Deployment 死锁?

Answer:将 requiredDuringSchedulingIgnoredDuringExecution 改为 preferredDuringSchedulingIgnoredDuringExecution。初始部署阶段使用软约束降低耦合度,Pod 调度成功后自然形成拓扑分布。如果必须用硬亲和,先单独部署一方到目标节点,再部署依赖方。

Q18: VolumeBinding 的 WaitForFirstConsumer 和 Immediate 模式分别适合什么场景?

Answer:Immediate(默认):PVC 创建时立即绑定 PV,适合已知 Pod 会调度到哪个节点的场景。WaitForFirstConsumer:等第一个 Pod 调度到节点后才绑定 PV,让 PV 优先选择 Pod 所在节点或拓扑域,适合云盘等有区域限制的存储类型,可减少跨区流量。

Q19: 六个插件的执行顺序是怎么确定的?

Answer:Filter 阶段按 SchedulerConfiguration/profiles[].plugins.filter.enabled 数组顺序执行(默认按 registry.go 注册顺序)。Score 阶段先并行执行所有 Score 插件,再用 NormalizeScore 归一化到 [0, MaxNodeScore] 范围,最后加权求和。插件执行顺序不影响最终结果,只影响中间短路时机。

Q20: NodeResourcesFit 中 placementScorer 字段是做什么用的?

Answer:placementScorer(fit.go L104)由 v1.36.1 新增,仅在 enableTopologyAwareWorkloadScheduling feature gate 开启时初始化(fit.go L249-258)。它使用 MostAllocated 策略对 Pod 组(Gang)的多个副本做资源均衡分配,是 Gang Scheduling 场景下的资源打分器。普通 Pod 不涉及此字段。

Q21: TaintToleration 的 enableTaintTolerationComparisonOperators 参数有什么用?

Answer:默认情况下 toleration 的 key 和 taint 的 key 必须完全匹配。启用此参数后支持 Exists 操作符:operator: Exists 且不指定 value 表示"容忍所有带此 key 的污点"。例如 tolerations: - operator: Exists 可以容忍所有 NoExecute 污点。

Q22: Pod 调度失败时,SchedulingQueue 的 Unschedulable 队列和 BackoffQ 队列有什么区别?

Answer:Unschedulable 队列存放调度失败的 Pod,等待触发事件重新入队(如节点 label 变化、Pod 删除)。BackoffQ 是退避队列,对反复调度失败的 Pod 做指数退避等待,减少无效重试对 API server 的压力。当有 BackoffPods 或 UnschedulablePods 时,可用 kubectl drain 或调整 Pod 优先级让它优先重试。