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

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 插件示例

posted @ 2026-06-20 16:32  左扬  阅读(16)  评论(0)    收藏  举报