深色模式
自定义调度器与 Scheduler Framework
摘要:本文面向平台工程师 / 调度器开发者,讲解 Kubernetes Scheduler Framework 的插件化架构、扩展点(Filter/Score/Permit/Reserve…)、如何编写并注册自定义插件、如何配置多调度 profile 与多调度器,并分析"自研调度器 vs 默认调度器 + 插件"的边界与权衡。适用 Kubernetes v1.28+(Scheduler Framework 自 v1.19 稳定,QueueingHint 自 v1.34 稳定)。
适用版本与前提
- Kubernetes:v1.28+(Scheduler Framework 稳定自 v1.19)
- 工具:Go 工具链、
kubectl、kubectl get pods -o wide - 前提:对 Go 与 kube-scheduler 配置(
kubescheduler.config.k8s.io/v1)有基础了解
背景与问题
默认 kube-scheduler 已覆盖绝大多数场景(资源、亲和、污点、拓扑、抢占)。但当你需要:
- 基于业务指标(如 GPU 利用率、就近数据)做自定义打分;
- 在绑定前预置网络卷 / 分配 IP(PreBind);
- 实现批量调度 / Gang scheduling(一组 Pod 要么全调度要么都不调度);
- 对特定负载走完全不同的调度策略(多 profile / 多调度器);
就需要在 Scheduler Framework 上扩展,或部署独立调度器。
核心概念:扩展点
一次 Pod 调度被拆成调度周期(串行)与绑定周期(可并发)。插件可注册到以下扩展点(部分):
| 扩展点 | 作用 | 失败后果 |
|---|---|---|
QueueSort | 队列排序,全局仅一个生效 | — |
PreFilter | 预处理 / 前置校验 | 中断调度周期 |
Filter | 过滤不可行节点(predicate) | 剔除节点 |
PostFilter | 无可行节点时触发(默认=抢占) | 标记可调度则继续 |
PreScore/Score | 打分排名 | 中断 / 影响排序 |
Reserve/Unreserve | 预留资源、失败回滚清理 | 触发 Unreserve |
Permit | 批准 / 拒绝 / 延迟绑定 | deny→Unreserve |
PreBind/Bind/PostBind | 绑定前后工作 | 拒绝→回队列 |
关键不变量
Unreserve 的实现必须幂等且不能失败:当 Reserve 或之后阶段失败时,所有插件的 Unreserve 会按 Reserve 的逆序执行以清理状态。
插件 API 骨架
go
package main
import (
"context"
"k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
type DemoPlugin struct{}
func (p *DemoPlugin) Name() string { return "DemoPlugin" }
// Filter: 仅调度到带 demo-ok=true 的节点
func (p *DemoPlugin) Filter(ctx context.Context, cycleState *framework.CycleState,
pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
if nodeInfo.Node.Labels["demo-ok"] != "true" {
return framework.NewStatus(framework.Unschedulable, "node not demo-ok")
}
return framework.NewStatus(framework.Success)
}
// Score: 对已通过过滤的节点打分
func (p *DemoPlugin) Score(ctx context.Context, cycleState *framework.CycleState,
pod *v1.Pod, nodeName string) (int64, *framework.Status) {
return 50, framework.NewStatus(framework.Success)
}
// 注册插件
func New(_ runtime.Object, _ framework.Handle) (framework.Plugin, error) {
return &DemoPlugin{}, nil
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
编译后作为 kube-scheduler 的静态插件(需重新构建调度器二进制)或通过带外调度器 + 多调度器方式部署(见下)。[未实测] 上述骨架字段以 v1.28 framework API 为准,跨大版本(如升级到 v1.33+)时 framework 包签名可能变化,请以目标版 k8s.io/kubernetes/pkg/scheduler/framework 源码为准。
生产实践一:多调度 Profile(同一调度器,不同策略)
通过 KubeSchedulerConfiguration 定义多个 profile,并为插件配置不同参数,实现"按工作负载走不同策略"而无需部署新进程:
yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
- schedulerName: foo-scheduler
pluginConfig:
- name: NodeAffinity
args:
addedAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: scheduler-profile
operator: In
values:
- foo
plugins:
score:
enabled:
- name: InterPodAffinity
weight: 01
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Pod 通过 spec.schedulerName: foo-scheduler 选择对应 profile。注意:DaemonSet 控制器创建的 Pod 由默认调度器处理,不支持调度 profile(官方文档明确)。
生产实践二:多调度器(独立部署)
对"完全不同调度逻辑"(如 Volcano / 批处理 Gang 调度),可部署独立的 kube-scheduler 实例,并通过 leader-election 不同资源名避免冲突:
yaml
# 独立调度器 Deployment 关键参数(示意,非完整清单)
args:
- --config=/etc/kube-scheduler/config.yaml
- --leader-elect-resource-name=shared-scheduler
- --scheduler-name=shared-scheduler1
2
3
4
5
2
3
4
5
Pod 指定 spec.schedulerName: shared-scheduler 即可交由该调度器处理。注意:未指定 schedulerName 的 Pod 仍由默认调度器处理。
多调度器的坑
两个调度器若都认为自己"拥有"某些 Pod 的调度权,可能造成重复绑定或决策冲突。务必通过 schedulerName 严格划分职责,且各自 leader-election 资源名唯一,避免争抢同一 lease。
生产实践三:自研插件仓库
官方维护 kubernetes-sigs/scheduler-plugins 仓库,提供 CapacityScheduling、Coscheduling(Gang)、NodeResourcesFit 变体等可复用插件,避免从零造轮子。
故障排查
bash
# 确认 Pod 由哪个调度器处理
kubectl get pod <pod> -o jsonpath='{.spec.schedulerName}'
# 看调度器日志(Filter/Score 拒绝原因)
kubectl logs -n kube-system kube-scheduler-<node> | grep -i "DemoPlugin\|FailedScheduling"
# 插件未注册会拒绝 Pod 并告警
kubectl describe pod <pod>1
2
3
4
5
6
2
3
4
5
6
| 现象 | 根因 | 处理 |
|---|---|---|
| Pod 永久 Pending,无默认调度器事件 | schedulerName 指向未运行实例 | 启动对应调度器或改正名称 |
| 插件 Filter 拒绝全部节点 | 插件逻辑过严 | 放宽条件 / 查节点 label |
| 调度器启动失败 | 插件注册/配置错误 | 查调度器日志与配置校验 |
回滚与清理
bash
# 移除自定义调度器(先确保无 Pod 引用该 schedulerName)
kubectl delete deployment shared-scheduler -n kube-system
# 将工作负载改回默认调度器
kubectl patch deployment app --type=json -p='[{"op":"remove","path":"/spec/template/spec/schedulerName"}]'1
2
3
4
2
3
4
生产危险
移除自定义调度器前,必须确认没有任何 Pod 的 schedulerName 指向它,否则这些 Pod 将永远无法被调度(无调度器认领)。变更前请先 kubectl get pods -A -o json | grep schedulerName 全量核对。
安全与合规
- 自定义插件运行在控制面,拥有读取全集群资源与绑定 Pod 的权限,代码须经审查与 SBOM/依赖扫描。
- 多调度器实例增加控制面攻击面,应通过 RBAC、网络策略限制其 API 访问面。
性能、容量与成本
- 插件
Filter在每个候选节点上运行,Score在每个通过节点上运行——O(节点数) 开销。插件逻辑应保持轻量,避免在 Score 中做重 IO。 QueueingHint(稳定自 v1.34)可在集群变化时智能决定哪些 Pod 需要重新入队,降低无效重试开销;[版本相关] 在 v1.28 中它为 Alpha,需显式开启特性门控。
替代方案与权衡
| 方案 | 何时选 | 成本/风险 |
|---|---|---|
| 默认调度器 + 内置约束 | 绝大多数场景 | 最低 |
| 自定义插件(静态编译) | 需深度定制打分/过滤 | 需自行构建与维护调度器二进制 |
| scheduler-plugins 复用 | 批处理/Gang/容量 | 中,社区维护 |
| 多调度器独立部署 | 异构负载策略隔离 | 高,运维与冲突风险 |
| 完全自研调度器 | 极端特殊需求(慎用) | 最高,不建议 |
决策建议
优先用"默认调度器 + affinity/taint/topologySpread/priority"组合解决 90% 问题;仅在确有不可表达的策略时,才上 Scheduler Framework 插件;把"独立调度器"作为最后手段。
FAQ
Q:自定义插件必须重新编译 kube-scheduler 吗? A:标准方式是编译进调度器二进制(静态插件)。也可使用 scheduler-plugins 框架或独立调度器进程,避免修改上游。
Q:QueueSort 可以有多个插件吗? A:不可以。全局仅允许一个 QueueSort 插件生效,否则排序语义冲突。