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

推荐订阅源

The Last Watchdog
The Last Watchdog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
Y
Y Combinator Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
博客园_首页
小众软件
小众软件
I
InfoQ
J
Java Code Geeks
月光博客
月光博客
S
Secure Thoughts
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Stack Overflow Blog
Stack Overflow Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
T
Threat Research - Cisco Blogs
A
About on SuperTechFans
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Latest news
Latest news
G
GRAHAM CLULEY
IT之家
IT之家
C
Cisco Blogs
Last Week in AI
Last Week in AI
Engineering at Meta
Engineering at Meta
L
LangChain Blog
The Register - Security
The Register - Security
SecWiki News
SecWiki News
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
T
Tenable Blog
博客园 - Franky
美团技术团队
I
Intezer
U
Unit 42
雷峰网
雷峰网
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
SegmentFault 最新的问题
C
Cyber Attacks, Cyber Crime and Cyber 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(调度专题 · 六):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 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(调度专题 · 七):自定义插件开发实战 —— 手写一个 Score 插件并注册到集群
左扬 · 2026-06-20 · via 博客园 - 左扬

Kubernetes 源码【左扬精讲】—— kube-scheduler(调度专题 · 七):自定义插件开发实战 —— 手写一个 Score 插件并注册到集群

上一篇调度专题【左扬精讲】讲了多 Profile 与 Coordinated LeaderElection。本篇跳出使用者视角,从开发者视角带大家手写一个 Out-of-Tree 的 Score 插件,把它打包成镜像、注册到集群、观察它在 kube-scheduler 里的实际行为。

读完本篇,你将掌握:ScorePlugin 接口的全部方法签名(Score / ScoreExtensions / SignPod),PluginFactoryRegistry 的注册机制,以及如何用 go build --buildmode=plugin 之外的"main 包内 init"方式完成插件注入。最后给出一个真实可用的示例:ZoneSpreadScore —— 优先把 Pod 调度到当前 zone 内 Pod 数最少的节点

Kubernetes Scheduler Out-of-Tree Plugin Score 实战 Go k8s v1.36.1

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

重点掌握(必须)

  • ScorePlugin 接口 4 个方法Name() / Score() / ScoreExtensions()(返回 fwk.ScoreExtensions 本身或 nil)/ 可选 SignPod()
  • PluginFactory 与 Registry 注册pkg/scheduler/framework/runtime/registry.go:75-81Register()
  • 3 种集成方式:(1) 替换 kube-scheduler 二进制 + 静态注册;(2) 通过 OutOfTreeRegistry 注入;(3) go build --buildmode=plugin 动态加载
  • PluginConfig 解析registry.go:44-66 DecodeInto + 自定义 Args 类型

次重点(了解即可)

  • NormalizeScore 的设计意图:把任意范围的原始分压回 [0, MaxScore]staging/src/k8s.io/kube-scheduler/framework/interface.go:329
  • SignPod 的返回策略:本插件不依赖其他 Pod 位置 → 无条件签名加速
  • EnqueueExtensions 实现:本插件不拒绝 Pod → 可不实现

文章目录

一、Why:为什么要自研 Score 插件

k8s 内置的 6 大 Score 插件覆盖了"通用"调度需求:

  • NodeResourcesFit:CPU / 内存 / 扩展资源
  • NodeAffinity:软亲和
  • TaintToleration:PreferNoSchedule 打分
  • InterPodAffinity:Pod 间亲和 / 反亲和
  • PodTopologySpread:拓扑打散
  • ImageLocality:镜像已缓存

业务特定的需求不在内置范围:

  • AI 训练:同 zone 内 Pod 越少越好(避免跨 zone 通信)
  • 金融业务:机房断电影响最小的节点优先
  • 电商大促:节点已缓存业务镜像的优先级 +10
  • 能耗优化:CPU 利用率接近 70% 的节点优先(避免热点)

这些"千人千面"的规则,必须靠 Out-of-Tree 插件实现。

设计精髓

自研插件有两种成本,常被新人低估:

  1. 维护成本:插件是 Go 代码,要跟 k8s 主版本一起升级
  2. 可观测成本:插件行为不对时,没有内置 metrics,必须自己加埋点

建议先评估用 NodeAffinity + Taint + Label能解决 80% 的"伪业务需求",剩下 20% 再写插件。

二、What:ScorePlugin 接口全景

Score 插件要实现 3 个强制接口(staging/src/k8s.io/kube-scheduler/framework/interface.go:617-626)+ 1 个可选接口:

// staging/src/k8s.io/kube-scheduler/framework/interface.go (行 607-626, k8s v1.36.1)
type ScoreExtensions interface {
    // NormalizeScore 把任意范围的原始分压到 [0, MaxScore]
    NormalizeScore(ctx context.Context, state CycleState, p *v1.Pod, scores NodeScoreList) *Status
}

type ScorePlugin interface {
    Plugin  // 嵌入:需要 Name() string
    // Score 为每个 Feasible Node 打分
    Score(ctx context.Context, state CycleState, p *v1.Pod, nodeInfo NodeInfo) (int64, *Status)
    // ScoreExtensions 返回 NormalizeScore 实现或 nil
    ScoreExtensions() ScoreExtensions
}

// 可选:实现 SignPlugin 加速快路径
type SignPlugin interface {
    Plugin
    SignPod(ctx context.Context, p *v1.Pod) ([]SignFragment, *Status)
}

// 常量
const (
    MaxScore int64 = 100   // 任何 Score 函数返回值最大 100
    MinScore int64 = 0     // 任何 Score 函数返回值最小 0
)

Score 调用时机

┌──────────────────────────────────────────────────────────────────────┐
│  ScheduleOne()  调用流程                                              │
│                                                                      │
│  PreFilter → Filter (并行) → PreScore → Reserve                      │
│                            │                                         │
│                            ▼                                         │
│                    ┌──────────────────┐                              │
│                    │ Score (并行)     │ ←── 你的插件在这里被调用       │
│                    │ 每个 Feasible     │     每个 Node 各调一次        │
│                    │ Node 一次        │     返回 0~100 的分           │
│                    └────────┬─────────┘                              │
│                             ▼                                        │
│                    ┌──────────────────┐                              │
│                    │ NormalizeScore   │ ←── 把全集群分数归一化到 [0,100]│
│                    │ (集群级,一次)    │                              │
│                    └────────┬─────────┘                              │
│                             ▼                                        │
│                    ┌──────────────────┐                              │
│                    │ 加权求和         │ ←── 多插件按 weight 加权       │
│                    │ 选出最高分 Node  │                              │
│                    └──────────────────┘                              │
└──────────────────────────────────────────────────────────────────────┘

小贴士关于 NormalizeScore

为什么需要 NormalizeScore?假设插件 A 返回分范围 [0, 1000],插件 B 返回分范围 [0, 10]。如果不归一化,A.weight=1, B.weight=1 加权后 A 完全压制 B。NormalizeScore 把分压回 [0, MaxScore] 后,所有插件"同台竞技"。

框架提供了 helper.DefaultNormalizeScore(maxPriority, reverse, scores)pkg/scheduler/framework/plugins/helper/normalize_score.go:27)开箱即用。

三、How:插件的 3 种注册方式

方式机制代价适用
(1) 静态注册 把插件源码塞进 plugins/registry.go,重新编译 kube-scheduler 必须 fork scheduler 几乎不用
(2) Out-of-Tree Registry 在 scheduler 启动时通过 --config 加载包含 outOfTreeRegistry 的 main 包 重新编译自定义镜像 生产首选
(3) go build --buildmode=plugin 插件编译成 .so 文件,scheduler 启动时 dlopen 与 scheduler Go 版本严格对齐,运维复杂 极少

方式 (2) 的源码入口cmd/kube-scheduler/app/server.go:444-449):

// cmd/kube-scheduler/app/server.go (节选, k8s v1.36.1)
outOfTreeRegistry := make(runtime.Registry)
for _, option := range outOfTreeRegistryOptions {
    if err := option(outOfTreeRegistry); err != nil {
        return nil, nil, err
    }
}

// 后续传给 scheduler.New(... scheduler.WithFrameworkOutOfTreeRegistry(outOfTreeRegistry))

注意

v1.36.1 起 k/k 仓库不再提供独立的 sample-scheduler官方推荐做法:fork cmd/kube-scheduler 目录,改 main.go 注册自己的插件,再编译成镜像。

四、实战:从零写 ZoneSpreadScore

本节实现一个生产可用的 Score 插件:ZoneSpreadScore。它的逻辑是:

对每个 Feasible Node,统计其所在 zone 内已调度的同 Priority Pod 数。zone 内 Pod 数越少,节点得分越高。这样能让 Pod 均匀打散到所有 zone。

4.1 项目结构

my-scheduler/
├── go.mod
├── main.go                         # 注册插件入口
└── pkg/plugins/zonespread/
    ├── zonespread.go               # 插件实现
    └── zonespread_test.go          # 单元测试

4.2 go.mod

module github.com/myorg/my-scheduler

go 1.26

require (
    k8s.io/api v0.36.1
    k8s.io/apimachinery v0.36.1
    k8s.io/kube-scheduler v0.36.1
    k8s.io/kubernetes v1.36.1
)

4.3 插件主体代码

// pkg/plugins/zonespread/zonespread.go (k8s v1.36.1)

package zonespread

import (
    "context"
    "fmt"

    v1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/kubernetes/pkg/scheduler/framework/plugins/helper"

    fwk "k8s.io/kube-scheduler/framework"
)

// Name 是插件名,必须全局唯一,与 KubeSchedulerConfiguration 中的 name 一致
const Name = "ZoneSpreadScore"

// Args 是从 KubeSchedulerConfiguration.pluginConfig 解析的配置
type Args struct {
    // 哪些 label 算 zone(默认用 topology.kubernetes.io/zone)
    ZoneLabel string `json:"zoneLabel,omitempty"`
    // 分数反转:true 表示 zone 内 Pod 越多 越好(适合 binpack)
    Reverse bool `json:"reverse,omitempty"`
}

// 编译期断言:必须实现这些接口
var _ fwk.ScorePlugin = &ZoneSpread{}
var _ fwk.SignPlugin = &ZoneSpread{}  // 启用快路径签名

// ZoneSpread 是插件主体
type ZoneSpread struct {
    handle  fwk.Handle
    args    *Args
}

func (pl *ZoneSpread) Name() string { return Name }

// New 初始化插件(被 PluginFactory 调用)
func New(ctx context.Context, obj runtime.Object, h fwk.Handle) (fwk.Plugin, error) {
    args := &Args{ZoneLabel: "topology.kubernetes.io/zone"}
    if obj != nil {
        // DecodeInto 把 YAML/JSON 字节流转成 Args
        if err := runtime.DefaultUnstructuredConverter.
            FromUnstructured(obj.UnstructuredContent(), args); err != nil {
            return nil, fmt.Errorf("invalid args for %s: %w", Name, err)
        }
    }
    return &ZoneSpread{handle: h, args: args}, nil
}

// Score 为单个节点打分
func (pl *ZoneSpread) Score(
    ctx context.Context, state fwk.CycleState,
    pod *v1.Pod, nodeInfo fwk.NodeInfo,
) (int64, *fwk.Status) {

    node := nodeInfo.Node()
    zoneLabel := pl.args.ZoneLabel
    nodeZone, hasLabel := node.Labels[zoneLabel]
    if !hasLabel {
        return 0, fwk.NewStatus(fwk.Error, fmt.Sprintf("node %s missing label %s", node.Name, zoneLabel))
    }

    // 1. 统计同 zone 内已调度的同优先级 Pod 数
    podsInZone := int64(0)
    for _, existing := range nodeInfo.GetPods() {
        // 这里简化:只看 Pod 自身的 zone label(实际生产建议从 Node 上读)
        if podZone := pl.getPodZone(existing.Pod, zoneLabel); podZone == nodeZone {
            podsInZone++
        }
    }

    // 2. 简单映射:pod 越少分越高
    //    0 pod → 100,  100+ pod → 0
    score := fwk.MaxScore - podsInZone
    if score < fwk.MinScore {
        score = fwk.MinScore
    }
    return score, nil
}

// ScoreExtensions 返回 NormalizeScore 实现
func (pl *ZoneSpread) ScoreExtensions() fwk.ScoreExtensions {
    return pl
}

// NormalizeScore 把原始分压回 [0, MaxScore]
func (pl *ZoneSpread) NormalizeScore(
    ctx context.Context, state fwk.CycleState,
    pod *v1.Pod, scores fwk.NodeScoreList,
) *fwk.Status {
    reverse := pl.args.Reverse
    // 复用框架 helper
    return helper.DefaultNormalizeScore(fwk.MaxScore, reverse, scores)
}

// SignPod 返回调度签名(用于 OpportunisticBatching)
func (pl *ZoneSpread) SignPod(
    ctx context.Context, pod *v1.Pod,
) ([]fwk.SignFragment, *fwk.Status) {
    // 本插件逻辑依赖 nodeInfo.GetPods(),但同一 Pod 调度的 Node 不同 GetPods 也不同
    // → 拒绝签名,强制走慢路径(更准)
    return nil, fwk.NewStatus(fwk.Unschedulable,
        "ZoneSpreadScore cannot sign because scoring depends on per-node pod count")
}

// getPodZone 从 Pod 的 NodeName 反查 Node label(这里简化)
func (pl *ZoneSpread) getPodZone(pod *v1.Pod, label string) string {
    if pod.Spec.NodeName == "" {
        return ""
    }
    node, err := pl.handle.SharedInformerFactory().Core().V1().Nodes().
        Lister().Get(pod.Spec.NodeName)
    if err != nil || node == nil {
        return ""
    }
    return node.Labels[label]
}

设计精髓

SignPod 的设计决策:虽然插件本身"似乎"只读 Pod 的 zone 字段(与已有 Pod 无关),但实际打分依赖 nodeInfo.GetPods()而 GetPods 在不同 Node 上结果不同。如果让 100 个相似 Pod 共用同一个签名,会全部调度到同一 Node,反而违反打散本意。

所以正确做法是拒绝签名,让每个 Pod 都走完整 Score —— 这是 v1.36.1 OpportunisticBatching 的标准取舍。

4.4 main.go 注册插件

// main.go (k8s v1.36.1)
package main

import (
    "os"

    "k8s.io/kubernetes/cmd/kube-scheduler/app"
    zonespread "github.com/myorg/my-scheduler/pkg/plugins/zonespread"
)

func main() {
    // 注册 Out-of-Tree 插件
    command := app.NewSchedulerCommand(
        app.WithPlugin(zonespread.Name, zonespread.New),
    )

    if err := command.Execute(); err != nil {
        os.Exit(1)
    }
}

五、构建与镜像打包

5.1 本地编译

# 1. 初始化模块依赖
go mod init github.com/myorg/my-scheduler
go mod tidy

# 2. 编译成二进制
go build -o /usr/local/bin/my-scheduler .

# 3. 验证(这必须能跑)
my-scheduler --version

5.2 容器化

# Dockerfile
FROM golang:1.26 AS builder
WORKDIR /workspace
COPY . .
RUN go mod download && CGO_ENABLED=0 go build -o /out/my-scheduler .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/my-scheduler /my-scheduler
USER nonroot:nonroot
ENTRYPOINT ["/my-scheduler"]
# 构建并推送
docker build -t harbor.myorg.com/k8s/my-scheduler:v1.36.1-zone .
docker push harbor.myorg.com/k8s/my-scheduler:v1.36.1-zone

六、部署到集群并验证

6.1 替换 scheduler 镜像

# /etc/kubernetes/manifests/my-scheduler.yaml (k8s v1.36.1)
apiVersion: v1
kind: Pod
metadata:
  name: kube-scheduler-my
  namespace: kube-system
spec:
  containers:
  - name: kube-scheduler
    image: harbor.myorg.com/k8s/my-scheduler:v1.36.1-zone
    command:
    - /my-scheduler
    - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
    - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
    - --config=/etc/kubernetes/scheduler-config.yaml
    - --v=4
    livenessProbe:
      httpGet: { path: /healthz, port: 10259 }
    readinessProbe:
      httpGet: { path: /healthz, port: 10259 }
    volumeMounts:
    - name: kubeconfig
      mountPath: /etc/kubernetes
  hostNetwork: true
  priorityClassName: system-node-critical
  volumes:
  - name: kubeconfig
    hostPath: { path: /etc/kubernetes, type: DirectoryOrCreate }

6.2 写 Profile 配置启用插件

# /etc/kubernetes/scheduler-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: zone-spread-scheduler
  plugins:
    score:
      enable:
      - name: ZoneSpreadScore         # 启用我们的插件
      disabled:
      - name: PodTopologySpread       # 关闭内置,避免重复打散
  pluginConfig:
  - name: ZoneSpreadScore
    args:
      apiVersion: kubescheduler.config.k8s.io/v1
      kind: ZoneSpreadArgs           # 必须与 Args 类型名一致
      zoneLabel: topology.kubernetes.io/zone
      reverse: false

6.3 创建 Pod 验证

apiVersion: v1
kind: Pod
metadata:
  name: spread-test
spec:
  schedulerName: zone-spread-scheduler   # 指定用我们的 profile
  containers:
  - name: nginx
    image: nginx:1.27
    resources:
      requests: { cpu: 100m, memory: 64Mi }

6.4 看 metrics 验证插件被调用

# 看 Score 阶段耗时(按插件名)
kubectl logs -n kube-system kube-scheduler-my | grep -E "Score.*ZoneSpread"

# 期望日志(v=4):
# "plugin scored" plugin="ZoneSpreadScore" pod="default/spread-test" node="node-1" score=92
# "plugin scored" plugin="ZoneSpreadScore" pod="default/spread-test" node="node-2" score=85
# "plugin scored" plugin="ZoneSpreadScore" pod="default/spread-test" node="node-3" score=78

Scheduler 框架会自动记录每个插件每次 Score 调用的延迟 + 结果,结合 Prometheus metrics scheduler_plugin_score_duration_seconds 可看到插件是否过热。

小贴士关于 metrics

如果你想给自己的插件加专属 metrics,最方便的是引入 k8s.io/component-base/metrics

var zoneSpreadScoreTotal = metrics.NewCounterVec(
    &metrics.CounterOpts{
        Name: "zonespread_score_total",
        Help: "ZoneSpreadScore invocation count",
    }, []string{"result"})

// 在 Score 函数末尾:
zoneSpreadScoreTotal.WithLabelValues("success").Inc()

需要 metrics.Register() 在 main.go 注册一次。

七、Pitfall:踩坑实录

P1. Score 返回值超过 100

症状normalizer panic: score exceeds MaxScore,调度器崩溃。

原因:插件 Score 函数返回了 int64(200),违反 fwk.MaxScore = 100 的契约。

修复:始终在 Score 函数里做边界裁剪:

score := calculatedScore
if score > fwk.MaxScore { score = fwk.MaxScore }
if score < fwk.MinScore { score = fwk.MinScore }
return score, nil

P2. 插件初始化报错,scheduler 启动失败

症状kube-scheduler CrashLoopBackOff,日志 "plugin ZoneSpreadScore initialization failed"

原因Args 类型名与 YAML 里 kind 字段不一致。YAML 写 kind: ZoneSpreadScore,Go 里 type Args struct{} 没有匹配。

修复:YAML 的 kind 必须等于 args 类型的 Go 类型名加 Args

kind: ZoneSpreadArgs   # 必须对应 Go type ZoneSpreadArgs(不是 ZoneSpreadScoreArgs)

实际上生产中常用"插件名 + Args"命名规范(如 CapacitySchedulingArgs)。

P3. 插件只 Score 不 NormalizeScore,导致分数压制

症状:业务反馈只有 ZoneSpread 插件影响调度,其他插件(如 NodeAffinity)几乎不起作用。

原因:插件 Score 返回范围 [0, 1000] 而内置 NodeAffinity 返回 [0, 100],加权求和时 ZoneSpread 完全压制。

修复永远实现 NormalizeScore(或 ScoreExtensions() 返回 helper.DefaultNormalizeScore)。

P4. 编译插件时报"undefined: fwk.Handle"

症状go buildundefined: fwk.Handle

原因k8s.io/kube-schedulerk8s.io/kubernetes 内部的 framework是同一个,但 import 路径写错。

修复

// 正确的 import:
import fwk "k8s.io/kube-scheduler/framework"   // staging 仓库里的 framework

// 错误:
import fwk "k8s.io/kubernetes/pkg/scheduler/framework"  // 内部路径,会冲突

八、FAQ:常见疑问

Q1. 插件必须实现哪些接口?

Score 插件强制Plugin(含 Name()) + ScorePlugin可选ScoreExtensions(用于 NormalizeScore)、SignPlugin(用于签名加速)、EnqueueExtensions(用于 QueueingHint)。

Q2. 插件可以同时挂多个扩展点吗?

可以。同一个 Go struct 通过多个编译期断言挂多个扩展点。例如 6 大内置插件 NodeResourcesFit 一次断言 6 个接口(详见本系列上一篇)。

Q3. 插件能读 Node 的容量信息吗?

可以,通过 nodeInfo.Node().Status.Allocatablev1.ResourceList)。还能通过 nodeInfo.GetPods() 拿该节点上所有 Pod。

Q4. 插件能调用 apiserver 吗?

不推荐。Score 阶段被并行调用,频繁调 apiserver 会拖垮调度。强烈建议只用 informer cache(handle.SharedInformerFactory())。

Q5. 插件的 Args 怎么写?

把 Args 定义成能被 runtime.DefaultUnstructuredConverter 识别的结构(基本类型 + struct tag):

type Args struct {
    ZoneLabel string `json:"zoneLabel,omitempty"`
    Reverse   bool   `json:"reverse,omitempty"`
}

Q6. 怎么调试插件?

三种手段:

  1. --v=6 日志:框架会打印每次插件调用
  2. pprof:scheduler 默认开 :10251/debug/pprof
  3. scheduler_framework_extension_point_duration metrics:按扩展点 + 插件名 + status 分桶

Q7. 插件需要热加载吗?

不能。插件是编译进二进制的,修改后必须重启 scheduler。这是 k8s 调度的设计取舍。

Q8. 插件能影响抢占吗?

Score 插件间接影响:抢占(DefaultPreemption)时调度器会跑 NominatedPod 的过滤 + 打分,得分最高的 Node 被腾出来。所以 Score 插件逻辑会间接影响抢占顺序。

Q9. 怎么测试插件?

用框架的 framework.Handle mock + cache.NewSnapshot() 构造 Node 列表:

func TestZoneSpread_Score(t *testing.T) {
    pl := &ZoneSpread{args: &Args{ZoneLabel: "topology.kubernetes.io/zone"}}

    node := &v1.Node{ObjectMeta: metav1.ObjectMeta{
        Name: "node-1",
        Labels: map[string]string{"topology.kubernetes.io/zone": "us-east-1a"},
    }}

    // 通过 framework.Handle 拿 nodeInfo(生产里用 NewNodeInfo())
    score, _ := pl.Score(ctx, nil, &v1.Pod{}, newNodeInfo(node))
    assert.Equal(t, int64(100), score)  // 没 Pod 时最高分
}

Q10. 插件能跨 Pod 共享状态吗?

不推荐。每个 Pod 调度都重新创建 Framework 实例(v1.32 之前)/ 复用 Framework 实例(v1.32+),所以插件实例可能被复用。建议用 sync.Map 等线程安全结构存跨 Pod 状态。

Q11. Args 怎么支持 YAML 直接解析?

New() 里用 runtime.DefaultUnstructuredConverter.FromUnstructured()runtime.Object 反序列化成结构体。YAML 的 kind 必须匹配 Go 类型名 + Args 后缀。

Q12. 插件能影响 Pod 的 schedulerName 路由吗?

不能schedulerName 是 Pod 入队时根据 spec.schedulerName 决定的,插件在 Score 阶段才被调用,已经太晚

Q13. 怎么让自己的插件在 Plugin 列表中显示?

在 main.go 用 app.WithPlugin(name, factory) 注册。重启后 kube-scheduler --config 配置的 Profile 里 plugins.score.enable 加上插件名即可。

Q14. 多个 Score 插件怎么加权?

通过 pluginsConfig.score 设置 weight(默认 1):

pluginsConfig:
- name: ZoneSpreadScore
  args: {...}
  weight: 50        # 高分

Q15. 插件里的 client-go 客户端怎么拿?

通过 handle.KubeClientSet()

cs := pl.handle.KubeClientSet()
pods, _ := cs.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})

强烈建议用 informer cache(handle.SharedInformerFactory())。

Q16. Score 阶段超时了会怎样?

v1.32 起,框架有 pluginMetricsSamplePercent 控制插件 metric 采样率。Score 阶段没有硬超时(与 Permit 不同),但调度器主循环每 100ms 检查一次累计耗时,过长会拖慢整个调度周期。

Q17. 插件能感知 FeatureGate 吗?

可以,但需要用 runtime.FactoryAdapterpkg/scheduler/framework/runtime/registry.go:38)包装 factory,把 feature.Features 注入到构造函数。

Q18. 怎么在生产中灰度发布?

两步灰度:

  1. 先在一个 Profile 里启用插件,配 weight=1,观察调度行为
  2. 无问题后再 weight=50 提升权重

Q19. PluginConfig 里 args 可以是嵌套对象吗?

可以。YAML 支持嵌套 + 数组:

pluginConfig:
- name: ZoneSpreadScore
  args:
    apiVersion: kubescheduler.config.k8s.io/v1
    kind: ZoneSpreadArgs
    zoneLabel: topology.kubernetes.io/zone
    extras:
    - key: team
      value: ai

Go 结构用 []struct{ Key, Value string } 映射。

Q20. 插件需要 license header 吗?

需要。k8s 强制 Apache 2.0 license header(hack/verify-boilerplate.sh 校验)。新文件最前面加:

/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0
*/

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

本篇是调度专题实战首篇,演示了 Score 插件从 0 到 1 的全流程。后续主线:

  • 抢占(DefaultPreemption)机制:调度失败时如何挪走低优先级 Pod
  • DRA 与 Scheduling:v1.36 起的新资源维度调度
  • Multi-scheduler 实战:跑两套 kube-scheduler 各自选主

调度专题的目标读者是资深运维开发:能读 Go 源码、有集群运维经验、对 k8s 整体架构已有认知。本系列到本篇告一段落,下一篇会从 抢占机制 切入,把调度失败的兜底路径讲透。


本文参考与源码链接:
  • framework/interface.go · ScorePlugin 接口
  • registry.go · PluginFactory 与 Registry
  • normalize_score.go · DefaultNormalizeScore
  • server.go · OutOfTreeRegistry 注入
  • scheduler.go · WithFrameworkOutOfTreeRegistry
  • noderesources/fit.go · 内置 Score 插件参考
  • imagelocality · 简洁的内置 Score 插件示例