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

推荐订阅源

L
LINUX DO - 最新话题
P
Privacy International News Feed
NISL@THU
NISL@THU
T
Threatpost
Latest news
Latest news
A
Arctic Wolf
Security Latest
Security Latest
K
Kaspersky official blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
Blog — PlanetScale
Blog — PlanetScale
AWS News Blog
AWS News Blog
H
Hacker News: Front Page
IT之家
IT之家
Hacker News: Ask HN
Hacker News: Ask HN
C
Cyber Attacks, Cyber Crime and Cyber Security
V
V2EX
云风的 BLOG
云风的 BLOG
N
News and Events Feed by Topic
B
Blog RSS Feed
Project Zero
Project Zero
B
Blog
N
News | PayPal Newsroom
大猫的无限游戏
大猫的无限游戏
AI
AI
L
Lohrmann on Cybersecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 叶小钗
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
酷 壳 – CoolShell
酷 壳 – CoolShell
D
Docker
Y
Y Combinator Blog
爱范儿
爱范儿
The Hacker News
The Hacker News
Simon Willison's Weblog
Simon Willison's Weblog
The GitHub Blog
The GitHub Blog
G
GRAHAM CLULEY
博客园 - Franky
腾讯CDC
Scott Helme
Scott Helme
The Register - Security
The Register - Security
V2EX - 技术
V2EX - 技术
F
Fortinet All Blogs
Cloudbric
Cloudbric
T
The Exploit Database - CXSecurity.com
雷峰网
雷峰网
H
Heimdal Security Blog
S
Securelist
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org

博客园 - 左扬

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 源码 / 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(调度专题 · 二):内置插件逐个精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity
左扬 · 2026-06-20 · via 博客园 - 左扬

Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 二):内置插件逐个精读 — NodeResourcesFit / NodeAffinity / TaintToleration / PodTopologySpread / VolumeBinding / InterPodAffinity

调度专题第一篇我们梳理了 framework六大扩展点,第二篇(上一篇)讲透了 CycleState 这个"黑板"。这一篇我们顺着 PreFilter → Filter → PreScore → Score → Reserve → PreBind 的数据流,把 6 个内置插件逐个拆开看:每个插件在哪个扩展点干什么事、读写了 CycleState 的哪个 key、有什么性能细节踩坑点

读完本篇,你应该能回答:NodeResourcesFit 的 Filter 为什么要在 PreFilter 阶段就合并 initContainers 资源请求?PodTopologySpread 为什么用 [2]criticalPaths 而不是把所有路径都存下来?VolumeBinding 凭什么能跨 PreFilter/Reserve/PreBind 三个阶段持久化 PVC 绑定结果?

Kubernetes Scheduler Plugin NodeResourcesFit PodTopologySpread VolumeBinding k8s v1.36.1

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

重点掌握(必须)

  • 6 个插件的源码位置pkg/scheduler/framework/plugins/{noderesources,nodeaffinity,tainttoleration,podtopologyspread,volumebinding,interpodaffinity}/*.go
  • 实现的扩展点矩阵:每个插件的 var _ fwk.XxxPlugin = &Plugin{} 是它能力的唯一真相
  • CycleState key 命名:所有插件统一约定 preFilterStateKey = "PreFilter" + NamepreScoreStateKey = "PreScore" + Name
  • state.Clone() 的三种模式:(1) 直接 return 原 state;(2) 浅拷贝 slice/map;(3) nil-safe return

次重点(了解即可)

  • PodTopologySpread 的 [2]criticalPaths:只保留 2 条最小匹配路径的工程妥协
  • VolumeBinding 的 sync.Mutex 嵌入 state:Filter 阶段并发修改 podVolumesByNode 的保护机制
  • InterPodAffinity 的 IgnorePreferredTermsOfExistingPods:跳过其他 Pod 的 preferred 词条优化策略

文章目录

一、综述:6 个内置插件全景

我们先把 6 个插件的扩展点实现矩阵拉出来看一眼:

插件PreFilterFilterPreScoreScoreReservePreBind核心职责
NodeResourcesFit CPU/内存/自定义资源是否够
NodeAffinity 节点选择器 + required/preferred
TaintToleration NoSchedule 硬过滤 + PreferNoSchedule 软分数
PodTopologySpread 多 zone/host 打散
VolumeBinding PVC/PV 绑定 + 容量评分
InterPodAffinity Pod 亲和 / 反亲和

小贴士关于扩展点矩阵的读法

VolumeBinding 是唯一一个同时实现 Reserve + PreBind 的内置插件——它要"占座 + 实际绑定"两件事都做。其他 5 个都到 Score 为止,后续交给 Reserve(如果用了 noderesources 占座)或直接 PreBind。

二、NodeResourcesFit —— 资源容量"是否装得下"

2.1 源码定位 & 接口实现

pkg/scheduler/framework/plugins/noderesources/fit.go。struct 定义在 fit.go:93-105

// fit.go:93 (k8s v1.36.1) — NodeResourcesFit 插件结构体
type Fit struct {
    ignoredResources                              sets.Set[string]
    ignoredResourceGroups                         sets.Set[string]
    enableInPlacePodVerticalScaling               bool
    enableSidecarContainers                       bool
    enableSchedulingQueueHint                     bool
    enablePodLevelResources                       bool
    enableDRAExtendedResource                     bool
    enableInPlacePodLevelResourcesVerticalScaling bool
    handle                                        fwk.Handle
    *resourceAllocationScorer
    placementScorer *resourceAllocationScorer
}

// fit.go:44-50 — 该插件实现的接口列表
var _ fwk.PreFilterPlugin  = &Fit{}
var _ fwk.FilterPlugin     = &Fit{}
var _ fwk.EnqueueExtensions = &Fit{}
var _ fwk.PreScorePlugin   = &Fit{}
var _ fwk.ScorePlugin      = &Fit{}
var _ fwk.SignPlugin       = &Fit{}
var _ fwk.PlacementScorePlugin = &Fit{}

2.2 PreFilter:合并 Pod 的资源请求

fit.go:334PreFilter 会把 Pod 的 所有 containers + initContainers + sidecars + overhead 的资源请求合并到一份 framework.Resource,写入 CycleState:

// fit.go:334 (k8s v1.36.1) — PreFilter 入口
func (f *Fit) PreFilter(ctx context.Context, cycleState fwk.CycleState, pod *v1.Pod, nodes []fwk.NodeInfo) (*fwk.PreFilterResult, *fwk.Status) {
    // 计算 Pod 总资源请求(合并 containers + initContainers 取 max + overhead + sidecar)
    s := f.calculateResource(pod)
    cycleState.Write(preFilterStateKey, &preFilterState{Resource: s})  // fit.go:58
    return nil, nil
}

设计精髓

为什么要在 PreFilter 而不是 Filter 里合并资源请求?因为这个 Pod 在调度中只会算一次,但 Filter 会跑 N 个节点。PreFilter 把 "每个节点都要用的计算" 提到调度循环入口算一次,结果写到 CycleState,Filter 阶段直接读——这就是"写一次读多次" 的标准实践。initContainers 的合并规则(取 regularContainers.max,而不是 sum)也是在这里做。

2.3 Filter:逐节点资源比对

fit.go:615Filter 是真正的"装得下吗"判断:

// fit.go:615 (k8s v1.36.1) — Filter 入口
func (f *Fit) Filter(ctx context.Context, cycleState fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
    s, err := getPreFilterState(cycleState)
    if err != nil {
        return fwk.AsStatus(err)
    }

    // Fits() 函数:返回所有装不下的资源维度(含原因)
    insufficient := Fits(pod, nodeInfo, f.draManager, ResourceRequestsOptions{
        EnablePodLevelResources:   f.enablePodLevelResources,
        EnableDRAExtendedResource: f.enableDRAExtendedResource,
    })
    if len(insufficient) != 0 {
        return fwk.NewStatus(fwk.Unschedulable, fitError+insufficient[0].Reason)
    }
    return nil
}

注意

Filter 只返回第一条 装不下的资源维度作为 reason。这是 k8s 的设计选择:调试友好(你能立刻看到最关键的瓶颈),但也会隐藏其他资源不足。如果你想看完整的不匹配原因,看 fit.go:674Fits() 函数的返回值([]InsufficientResource)。

2.4 Score:四种打分策略

fit.go:65-90nodeResourceStrategyTypeMap 注册了3 种打分散列:

策略公式(简化)特点
LeastAllocated(默认) (capacity - requested) / capacity 分高 = 剩余多 = 打散
MostAllocated requested / capacity 分高 = 用得多 = 堆积
RequestedToCapacityRatio 基于 Shape 函数(线性/log) 可配权重 + 阈值

三、NodeAffinity —— 节点选择器与节点亲和性

3.1 源码定位 & 接口实现

pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go。struct 在 node_affinity.go:39-44

// node_affinity.go:39 (k8s v1.36.1)
type NodeAffinity struct {
    handle                    fwk.Handle
    addedNodeSelector         *nodeaffinity.NodeSelector          // 调度器全局强制节点选择器
    addedPrefSchedTerms       *nodeaffinity.PreferredSchedulingTerms // 调度器全局 preferred 项
    enableSchedulingQueueHint bool
}

// node_affinity.go:46-51 — 实现的扩展点
var _ fwk.PreFilterPlugin = &NodeAffinity{}
var _ fwk.FilterPlugin    = &NodeAffinity{}
var _ fwk.PreScorePlugin  = &NodeAffinity{}
var _ fwk.ScorePlugin     = &NodeAffinity{}
var _ fwk.EnqueueExtensions = &NodeAffinity{}
var _ fwk.SignPlugin      = &NodeAffinity{}

3.2 PreFilter:解析 Pod 的 RequiredDuringSchedulingIgnoredDuringExecution

node_affinity.go:159 的 PreFilter 把 Pod 的 节点选择器 + 节点亲和 合并成一份 RequiredNodeAffinity 写到 CycleState,并返回一个节点集合优化

// node_affinity.go:159 (k8s v1.36.1) — PreFilter
func (pl *NodeAffinity) PreFilter(...) (*fwk.PreFilterResult, *fwk.Status) {
    affinity := pod.Spec.Affinity
    noNodeAffinity := (affinity == nil || affinity.NodeAffinity == nil ||
        affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil)
    if noNodeAffinity && pl.addedNodeSelector == nil && pod.Spec.NodeSelector == nil {
        return nil, fwk.NewStatus(fwk.Skip)   // 没活干
    }

    state := &preFilterState{
        requiredNodeSelectorAndAffinity: nodeaffinity.GetRequiredNodeAffinity(pod),
    }
    cycleState.Write(preFilterStateKey, state)

    // 关键:尝试把 affinity 转化为"只可能落在这些节点上"
    terms := affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms
    var nodeNames sets.Set[string]
    for _, t := range terms {
        // 如果 term 包含 key==metadata.name 且 operator==In
        // → 这些节点可能匹配
        for _, r := range t.MatchFields {
            if r.Key == metav1.ObjectNameField && r.Operator == v1.NodeSelectorOpIn {
                s := sets.New(r.Values...)
                // 取交集(terms 间是 OR,term 内是 AND)
                ...
            }
        }
    }
    if nodeNames != nil && len(nodeNames) > 0 {
        return &fwk.PreFilterResult{NodeNames: nodeNames}, nil   // node_affinity.go:205
    }
    return nil, nil
}

设计精髓

PreFilter 的关键返回值fwk.PreFilterResult{NodeNames: ...}——framework 拿到这个集合后会跳过不在集合里的节点(直接当成 Filter 失败)。这是 k8s 的"早期剪枝"机制:如果你的 Pod 显式亲和 nodeName in {node-a, node-b},框架根本不会让 NodeResourcesFit 在 node-c 上跑——节省了大量 Filter 调用。这就是 PreFilter 优于 Filter 的核心收益

3.3 Filter:节点亲和匹配

node_affinity.go:218 的 Filter 几乎不做计算,只读 CycleState + 调 RequiredNodeAffinity.Match(node)

// node_affinity.go:218 (k8s v1.36.1) — Filter
func (pl *NodeAffinity) Filter(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
    node := nodeInfo.Node()

    // 先匹配 scheduler-enforced(admin 通过配置加的全局强制节点选择器)
    if pl.addedNodeSelector != nil && !pl.addedNodeSelector.Match(node) {
        return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, errReasonEnforced)
    }

    // 再匹配 Pod 自带的
    s, err := getPreFilterState(state)
    if err != nil {
        // PreFilter 跳过时的兜底:直接现场算
        s = &preFilterState{requiredNodeSelectorAndAffinity: nodeaffinity.GetRequiredNodeAffinity(pod)}
    }
    match, _ := s.requiredNodeSelectorAndAffinity.Match(node)
    if !match {
        return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, ErrReasonPod)
    }
    return nil
}

小贴士关于 Fallback 算亲和

注意 node_affinity.go:227-230兜底逻辑:如果 PreFilter 被跳过(返回 Skip),CycleState 里就没有这个 key,Filter 会现场重新算 RequiredNodeAffinity。这是 CycleState 的容错模式:写入是 best-effort,读取是必须兼容"没写"的情况——但是绝大多数情况下不应该走兜底,否则就丢掉了 PreFilter 的优化收益。

四、TaintToleration —— 污点与容忍

4.1 源码定位 & 接口实现

pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go。struct 在 taint_toleration.go:35-39

// taint_toleration.go:35 (k8s v1.36.1)
type TaintToleration struct {
    handle                                   fwk.Handle
    enableSchedulingQueueHint                bool
    enableTaintTolerationComparisonOperators bool
}

// taint_toleration.go:41-45 — 唯一没有 PreFilter 的内置插件!
var _ fwk.FilterPlugin     = &TaintToleration{}
var _ fwk.PreScorePlugin   = &TaintToleration{}
var _ fwk.ScorePlugin      = &TaintToleration{}
var _ fwk.EnqueueExtensions = &TaintToleration{}
var _ fwk.SignPlugin       = &TaintToleration{}

小贴士为什么 TaintToleration 没有 PreFilter?

TaintToleration 的判断逻辑是per-node 的("这个 Pod 的 tolerations 能不能容忍这个节点的 taints"),没有跨节点可剪枝的可能——每个节点都需要独立检查。所以 PreFilter 没有优化空间,直接 Filter 即可。

4.2 Filter:硬过滤 NoSchedule taints

taint_toleration.go:119 的 Filter 极简——只过滤 NoSchedule / NoExecute 效应的 taints:

// taint_toleration.go:119 (k8s v1.36.1) — Filter 入口
func (pl *TaintToleration) Filter(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) *fwk.Status {
    node := nodeInfo.Node()

    taint, isUntolerated := v1helper.FindMatchingUntoleratedTaint(
        logger, node.Spec.Taints, pod.Spec.Tolerations,
        helper.DoNotScheduleTaintsFilterFunc(),   // 只看 NoSchedule + NoExecute
        pl.enableTaintTolerationComparisonOperators,
    )
    if !isUntolerated {
        return nil
    }
    return fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node(s) had untolerated taint(s)")
}

4.3 PreScore + Score:PreferNoSchedule 软分数

taint_toleration.go:157 的 PreScore 把 Pod 的 PreferNoSchedule 容忍 过滤出来写到 CycleState,:195 的 Score 给"不耐受 PreferNoSchedule 越多,分越低":

// taint_toleration.go:146 (k8s v1.36.1) — 提取 PreferNoSchedule 容忍
func getAllTolerationPreferNoSchedule(tolerations []v1.Toleration) (tolerationList []v1.Toleration) {
    for _, toleration := range tolerations {
        if len(toleration.Effect) == 0 || toleration.Effect == v1.TaintEffectPreferNoSchedule {
            tolerationList = append(tolerationList, toleration)
        }
    }
    return
}

// taint_toleration.go:195 — Score 入口
func (pl *TaintToleration) Score(ctx context.Context, state fwk.CycleState, pod *v1.Pod, nodeInfo fwk.NodeInfo) (int64, *fwk.Status) {
    s, _ := getPreScoreState(state)
    score := int64(pl.countIntolerableTaintsPreferNoSchedule(logger, node.Spec.Taints, s.tolerationsPreferNoSchedule))
    return score, nil   // 分越高越不可取(intolerable 越少分越低)
}

五、PodTopologySpread —— 拓扑打散

5.1 源码定位 & 接口实现

pkg/scheduler/framework/plugins/podtopologyspread/plugin.go + filtering.go + scoring.go。struct 在 plugin.go:60-73

// plugin.go:60 (k8s v1.36.1)
type PodTopologySpread struct {
    systemDefaulted                              bool
    parallelizer                                 fwk.Parallelizer
    defaultConstraints                           []v1.TopologySpreadConstraint
    sharedLister                                 fwk.SharedLister
    services, replicationCtrls, replicaSets, statefulSets ...
    enableNodeInclusionPolicyInPodTopologySpread bool
    enableMatchLabelKeysInPodTopologySpread      bool
    enableSchedulingQueueHint                    bool
}

// plugin.go:46-57 — 系统默认约束(如果 Pod 没写,自动加这两条)
var systemDefaultConstraints = []v1.TopologySpreadConstraint{
    {TopologyKey: v1.LabelHostname,     WhenUnsatisfiable: v1.ScheduleAnyway, MaxSkew: 3},
    {TopologyKey: v1.LabelTopologyZone, WhenUnsatisfiable: v1.ScheduleAnyway, MaxSkew: 5},
}

5.2 preFilterState 的关键设计:[2]criticalPaths

filtering.go:41-52 的 preFilterState 是 6 个插件里最复杂的:

// filtering.go:41 (k8s v1.36.1)
type preFilterState struct {
    Constraints []topologySpreadConstraint
    // 关键设计:每个 constraint 只存 2 条 critical path
    // CriticalPaths[i][0].MatchNum 永远是最小匹配数
    // CriticalPaths[i][1].MatchNum 永远 >= [0],但不保证是第二小
    CriticalPaths []*criticalPaths
    // 每个 constraint 每个 topology value 的匹配 Pod 数
    TpValueToMatchNum []map[string]int
}

// filtering.go:97 — criticalPaths 是 [2]struct 的定长数组,不是 slice
type criticalPaths [2]struct {
    TopologyValue string
    MatchNum      int
}

设计精髓

为什么是 [2] 而不是 []?filtering.go:90-95 的注释:当前 k8s 的抢占算法保证:(1) 抢占只发生在同一节点上的 Pod;(2) 每个节点的抢占周期用独立拷贝的 preFilterState。两者叠加——每次抢占只"挪走"一个 Pod 上的若干 Pod,最多影响 2 条 critical path。所以存 [2] 就够用,避免 slice 的动态分配。这是一个用领域知识换性能的经典例子。

5.3 Clone 模式:浅拷贝 + 显式 deep copy map

filtering.go:71-88 的 Clone 是 6 个插件里最复杂的,因为 preFilterState 含 slice + map 嵌套:

// filtering.go:71 (k8s v1.36.1) — Clone
func (s *preFilterState) Clone() fwk.StateData {
    if s == nil {
        return nil                          // ① nil-safe
    }
    copy := preFilterState{
        Constraints:       s.Constraints,                          // ② slice 共享(不可变)
        CriticalPaths:     make([]*criticalPaths, len(s.CriticalPaths)),  // ③ 重新分配 slice
        TpValueToMatchNum: make([]map[string]int, len(s.TpValueToMatchNum)),
    }
    for i, paths := range s.CriticalPaths {
        copy.CriticalPaths[i] = &criticalPaths{paths[0], paths[1]}  // ④ 拷贝 [2] 结构体
    }
    for i, tpMap := range s.TpValueToMatchNum {
        copy.TpValueToMatchNum[i] = maps.Clone(tpMap)  // ⑤ 用 Go 1.21+ maps.Clone 浅拷贝 map
    }
    return &copy
}

六、VolumeBinding —— 存储绑定(横跨三阶段)

6.1 源码定位 & 接口实现

pkg/scheduler/framework/plugins/volumebinding/volume_binding.go。struct 在 volume_binding.go:73-79

// volume_binding.go:73 (k8s v1.36.1)
type VolumeBinding struct {
    Binder      SchedulerVolumeBinder         // Reserve/PreBind 阶段真实绑定 PV
    PVCLister   corelisters.PersistentVolumeClaimLister
    classLister storagelisters.StorageClassLister
    scorer      volumeCapacityScorer
    fts         feature.Features
}

// volume_binding.go:81-88 — 唯一横跨 PreFilter/Filter/PreScore/Score/Reserve/PreBind 的插件
var _ fwk.PreFilterPlugin  = &VolumeBinding{}
var _ fwk.FilterPlugin     = &VolumeBinding{}
var _ fwk.ReservePlugin    = &VolumeBinding{}
var _ fwk.PreBindPlugin    = &VolumeBinding{}
var _ fwk.PreScorePlugin   = &VolumeBinding{}
var _ fwk.ScorePlugin      = &VolumeBinding{}
var _ fwk.EnqueueExtensions = &VolumeBinding{}
var _ fwk.SignPlugin       = &VolumeBinding{}

6.2 stateData:嵌入 sync.Mutex 的跨阶段状态

volume_binding.go:50-68 的 stateData 是 6 个插件里唯一嵌入 sync.Mutex 的:

// volume_binding.go:53 (k8s v1.36.1)
type stateData struct {
    allBound bool
    // podVolumesByNode: Filter 阶段为每个候选节点缓存的"卷假设绑定结果"
    podVolumesByNode map[string]*PodVolumes
    podVolumeClaims  *PodVolumeClaims
    hasStaticBindings bool   // 是否有 StaticBinding PV(影响是否需要 Skip Score)
    sync.Mutex       // ← 关键:保护并发修改 podVolumesByNode
}

func (d *stateData) Clone() fwk.StateData {
    return d   // 浅拷贝:state 内部有 sync.Mutex,不能随便 deep copy
}

注意

VolumeBinding 的 Clone() 直接返回 dvolume_binding.go:66-68),不做任何拷贝。这是有意为之——stateData 含 sync.Mutex不能拷贝,拷贝后锁状态会乱),而且 stateData 的生命周期严格在 schedulingCycle 内,不需要 deep copy。这是"Clone 是 best-effort"的另一面——有时不动就是最正确的。

6.3 Reserve + PreBind:实际绑定 PV

volume_binding.go:531 的 Reserve 把"假设绑定"转为"实际预留",:577 的 PreBind 触发 apiserver 真正创建 PV/PVC binding:

// volume_binding.go:531 (k8s v1.36.1) — Reserve
func (pl *VolumeBinding) Reserve(ctx context.Context, cs fwk.CycleState, pod *v1.Pod, nodeName string) *fwk.Status {
    state, _ := getStateData(cs)
    // 1. 获取在 Filter 阶段为该节点假设绑定的 PodVolumes
    pvMap := state.podVolumesByNode[nodeName]
    // 2. 告诉 assumeCache:这个 Pod 已经"占座",未来其他 Pod 不能假设绑定这些 PV
    return pl.Binder.AssumePodVolumes(pod, nodeName, pvMap)
}

// volume_binding.go:577 — PreBind
func (pl *VolumeBinding) PreBind(ctx context.Context, cs fwk.CycleState, pod *v1.Pod, nodeName string) *fwk.Status {
    return pl.Binder.BindPodVolumes(ctx, pod, nodeName)   // 真实调 apiserver
}

设计精髓

AssumeCacheassume_cache.go)是 VolumeBinding 的灵魂——它维护一个"已假设绑定的 PV 集合"。当一个 Pod 假设绑定了 PV-A,其他 Pod 在 Filter 阶段通过 assumeCache 看到"PV-A 已被占用",就不会再尝试假设绑定同一个 PV。这避免了两个 Pod 同时被调度到同一 PV 上的冲突。Reserve 把"假设" 固化 到 assumeCache,PreBind 把"假设" 落地 到 apiserver。

七、InterPodAffinity —— Pod 之间的亲和 / 反亲和

7.1 源码定位 & 接口实现

pkg/scheduler/framework/plugins/interpodaffinity/plugin.go + filtering.go + scoring.go。struct 在 plugin.go:47-53

// plugin.go:47 (k8s v1.36.1)
type InterPodAffinity struct {
    parallelizer              fwk.Parallelizer
    args                      config.InterPodAffinityArgs
    sharedLister              fwk.SharedLister       // 关键:能跨 Pod 看所有 Pod
    nsLister                  listersv1.NamespaceLister
    enableSchedulingQueueHint bool
}

// plugin.go:39-44 — 接口实现列表
var _ fwk.PreFilterPlugin = &InterPodAffinity{}
var _ fwk.FilterPlugin    = &InterPodAffinity{}
var _ fwk.PreScorePlugin  = &InterPodAffinity{}
var _ fwk.ScorePlugin     = &InterPodAffinity{}
var _ fwk.EnqueueExtensions = &InterPodAffinity{}
var _ fwk.SignPlugin      = &InterPodAffinity{}

7.2 preFilterState:3 个 topology-to-count map

filtering.go:44-56 的 preFilterState 是 6 个插件里语义最丰富的:

// filtering.go:44 (k8s v1.36.1)
type preFilterState struct {
    // 已有 Pod 的 anti-affinity → 每个 (topologyKey, topologyValue) 对下有多少"现有 Pod"和本 Pod 反亲和
    existingAntiAffinityCounts topologyToMatchedTermCount
    // 本 Pod 的 affinity → 每个 (topologyKey, topologyValue) 对下有多少"现有 Pod"和本 Pod 亲和
    affinityCounts topologyToMatchedTermCount
    // 本 Pod 的 anti-affinity → 每个 (topologyKey, topologyValue) 对下有多少"现有 Pod"和本 Pod 反亲和
    antiAffinityCounts topologyToMatchedTermCount
    podInfo        fwk.PodInfo
    namespaceLabels labels.Set
}

7.3 Clone 模式:显式 clone 3 个嵌套 map

filtering.go:59-72 的 Clone 显式调每个 map 自己的 clone() 方法:

// filtering.go:59 (k8s v1.36.1) — Clone
func (s *preFilterState) Clone() fwk.StateData {
    if s == nil {
        return nil
    }
    copy := preFilterState{}
    copy.affinityCounts         = s.affinityCounts.clone()
    copy.antiAffinityCounts     = s.antiAffinityCounts.clone()
    copy.existingAntiAffinityCounts = s.existingAntiAffinityCounts.clone()
    copy.podInfo        = s.podInfo          // podInfo 是不可变快照,共享即可
    copy.namespaceLabels = s.namespaceLabels
    return &copy
}

小贴士为什么 InterPodAffinity 是最慢的插件?

plugin.go:63-79SignPod 实现:当 Pod 有 PodAffinityPodAntiAffinity 时,直接放弃 signing(返回 Unschedulable 状态)。原因是 InterPodAffinity 的判断依赖"其他所有 Pod 的位置",而签名机制是为了让Pod 自身的字段能决定 Cache 命中——两者语义不兼容。这是 InterPodAffinity 不能用 Cache 优化、Filter 阶段最慢的根本原因。

八、横向对比表:6 插件能力速查

维度NodeResourcesFitNodeAffinityTaintTolerationPodTopologySpreadVolumeBindingInterPodAffinity
能否 Sign
PreFilter 节点剪枝 N/A
preFilterState 含 sync.Mutex N/A
Clone 模式 return s return s return s 浅拷贝 + maps.Clone return d(含锁) 显式 clone 3 map
Filter 复杂度 O(资源种类) O(terms) O(taints) O(constraints) O(PVC × 节点) O(所有 Pod)
跨调度周期复用 无(per-pod) AssumeCache

九、踩坑实录

9.1 在 Filter 阶段才合并 initContainers 资源

问题现象:自定义 Filter 阶段才做容器资源合并,每次 Filter 都重算一遍 initContainers,调度慢 30%+。

根本原因:错过了 PreFilter 的优化机会,违反"写一次读多次"原则。

正确做法:任何不依赖具体节点的 Pod 字段计算,都应该放 PreFilter 阶段。NodeResourcesFit 的 fit.go:334 是个标准范例。

9.2 自己加 PreFilterResult.NodeNames 剪枝,但忘考虑 OR 关系

问题现象:NodeAffinity 自己实现时只算了第一个 term 的节点名集合就返回,导致其他 term 能匹配的节点被剪掉。

根本原因:NodeSelectorTerms 之间是 OR(任一匹配即可),term 内 MatchExpressions 是 AND。看 node_affinity.go:179-198,正确做法是取所有 term 节点名的并集,term 内 MatchFields 是 交集

9.3 PodTopologySpread 的 criticalPaths [2] 改成 slice

问题现象:想"通用化"成 []criticalPath,结果抢占逻辑错乱。

根本原因filtering.go:90-95 注释明确说"基于当前抢占算法的两个事实"。改成 slice 后表面上"灵活",但语义已经变了,抢占逻辑就不再能保证正确。

9.4 VolumeBinding 的 Clone 改成 deep copy

问题现象:认为 return d 是 bug,改成 deep copy 后发生 deadlock。

根本原因sync.Mutex 不能拷贝(拷贝的锁状态会乱),而且 preemption 时 framework 需要拿原始 stateData 的锁,deep copy 后两份 stateData 各自锁,Filter 阶段可能死锁

9.5 InterPodAffinity 自己加签名优化

问题现象:以为只要 Pod 没写 affinity 就能签名,结果有时命中 Cache 后调度错了

根本原因:InterPodAffinity 的判断依赖其他 Pod 的位置,这信息不写在 Pod 自身里。Cache 只能基于 Pod 自身字段命中——本质冲突,签不了。k8s 在 plugin.go:63-79 明确禁止了。

十、FAQ & Roadmap

Q1:6 个插件的执行顺序是什么?谁先谁后?

A:由 framework 的 pluginOrder 配置决定,不是源码写死的。默认顺序在 pkg/scheduler/apis/config/v1/default_plugins.go,大致是:NodeResourcesFit → NodeAffinity → NodeName → TaintToleration → NodeUnschedulable → NodeVolumeLimits → ... → InterPodAffinity → PodTopologySpread。建议不要随便改顺序,除非你能证明调换后调度结果仍然正确。

Q2:为什么 PodTopologySpread 默认不开 PreFilter 节点剪枝?

A:因为它的判断依赖"这个 Pod 在这个 topology 下有多少匹配 Pod",每个 topology 的 Pod 数不同,不能提前剪掉节点(除非整个 topology 域都不可能满足 maxSkew,但这种判断本身和 Filter 等价,不如直接 Filter)。NodeAffinity 不一样——它的"必须匹配这条 label"是0/1 判断,可以提前剪。

Q3:怎么决定把计算放 PreFilter 还是 Filter?

A:判断标准:(1) 如果计算只依赖 Pod 不依赖 Node,放 PreFilter;(2) 如果结果能提前剪掉节点集合,放 PreFilter;(3) 否则放 Filter。NodeResourcesFit 的资源请求合并符合 (1),但无法提前剪节点(每个节点的可分配资源不同),所以 PreFilter 只写 CycleState,不返回 NodeNames 剪枝。

Q4:VolumeBinding 的 AssumeCache 是什么?为什么只有它有?

A:AssumeCache 是一个跨 Pod 共享的 PV 占用快照——记录"哪些 PV 已经被假设绑定"。其他插件的计算只和 Pod+Node 有关,不涉及集群全局资源,所以不需要这种机制。PV 是独占资源,必须避免两个 Pod 同时假设占用——AssumeCache 解决了这个问题。

Q5:TaintToleration 为什么是唯一一个没有 PreFilter 的"硬过滤"插件?

A:因为 TaintToleration 的判断是纯 per-node 的(节点 taints vs Pod tolerations),没有Pod 级别的预计算可做。NodeResourcesFit 的 PreFilter 虽然不剪节点,但合并 initContainers 资源请求有Pod 级别的 CPU,可以 PreFilter 算一次。NodeAffinity 的 PreFilter 直接解析 Pod 的 nodeAffinity 表达式,可以提前算好 requiredNodeSelectorAndAffinity + 提前剪节点。

Q6:怎么判断一个插件能不能签(sign)?

A:SignPod(ctx, pod) 函数。如果返回 nil, nil 表示可以签;返回 nil, fwk.NewStatus(Unschedulable, ...) 表示不能签。判断依据:插件的判断是否完全基于 Pod 自身的字段。NodeResourcesFit 看 pod.Spec.Containers,NodeAffinity 看 pod.Spec.Affinity.NodeAffinity——都是 Pod 字段,可以签。InterPodAffinity 依赖其他 Pod 的位置,签不了。

Q7:可以禁用某个内置插件吗?

A:可以。在 KubeSchedulerConfigurationprofiles 里把插件的 enabled: false。但要谨慎——比如禁用 TaintToleration 等于允许 Pod 跳过所有 taint 检查,破坏节点隔离模型。除非你有特殊场景(如自定义调度器接管了污点逻辑),不建议禁用。

Q8:所有内置插件都是用同一个调度器配置吗?

A:可以多个 profile。在 KubeSchedulerConfiguration.profiles 里能定义多个 Profile,每个 Profile 有自己的插件启用列表 + 插件参数。可以让某些 workload 用"打散"profile,某些用"堆积"profile——但需要配 schedulingGates 或扩展点。


本文参考与源码链接:
  • noderesources/fit.go · NodeResourcesFit 实现
  • nodeaffinity/node_affinity.go · NodeAffinity 实现
  • tainttoleration/taint_toleration.go · TaintToleration 实现
  • podtopologyspread/filtering.go · preFilterState + criticalPaths [2]
  • volumebinding/volume_binding.go · stateData + sync.Mutex
  • interpodaffinity/filtering.go · 3 个 topology-to-count map
  • volumebinding/assume_cache.go · 假设绑定 PV 缓存
  • kube-scheduler/framework · 插件接口定义
  • Scheduling, Preemption and Eviction · 官方文档