

















上一篇调度专题【左扬精讲】拆解了 SchedulingQueue 与 QueueingHint,回答了"Pod 进调度器后如何排队"的问题。本篇换一个更高层的视角:一个 kube-scheduler 进程里能跑多种调度策略吗?多副本之间如何选主?
这正是 Scheduler Profile 与 Coordinated LeaderElection 要解决的两件事。前者是"同集群多策略",后者是"跨集群协调选主"。读完后,你应该能回答三个问题:多 Profile 的核心数据是哪个 struct?Coordinated LeaderElection 与普通选主的本质差异是什么?以及一个 v1.36.1 集群最多能跑多少种调度策略?
Kubernetes Scheduler Profile LeaderElection 多租户 Go k8s v1.36.1
学习重点提示 — 建议先通读全文,再重点回顾标注内容
重点掌握(必须)
- Profile 的核心数据结构:profile.Map(pkg/scheduler/profile/profile.go:46)和 KubeSchedulerProfile(pkg/scheduler/apis/config/types.go)
- 同集群多 Profile 配置:profiles[*].schedulerName + Pod 的 spec.schedulerName(pkg/scheduler/eventhandlers.go:458)
- Coordinated LeaderElection 入口:cmd/kube-scheduler/app/server.go:230 与 server.go:307,CoordinatedLeaderElection 门控(pkg/features/kube_features.go:183)
- LeaseCandidate 角色:leaderelection.NewCandidate(server.go:241)+ coordination.k8s.io/v1.LeaseCandidate CRD
次重点(了解即可)
- Profile 内 PluginConfig 的传递链:pluginConfig → DecodeInto()(pkg/scheduler/framework/runtime/registry.go:44)
- cfgValidator 的唯一性约束:profile.go:105 不允许重名 schedulerName
- Multi-scheduler 与多 Profile 的取舍:何时拆 Pod 何时拆 Profile
文章目录
kube-scheduler 早期版本只支持"一种调度策略跑全集群"。当业务出现多种调度诉求时(在线 vs 离线、训练 vs 推理、不同部门配额),运维只能:
Profile 本质是 "调度策略包":每个 Profile 独立包含一份插件集合、插件配置、扩展点启用/禁用列表。一个 kube-scheduler 进程能跑 N 个 Profile(N 没有硬编码上限,但实际生产推荐 ≤ 10)。
设计精髓
Profile 的核心思想是 "策略外置":把"调度时怎么决策"从 Go 代码里抽出来,放到 KubeSchedulerConfiguration 这种声明式 YAML 里。这意味着:
用户视角的 Profile 是 pkg/scheduler/apis/config/types.go 中的 KubeSchedulerProfile 结构体(v1 API 用 kubescheduler.config.k8s.io/v1):
// pkg/scheduler/apis/config/types.go (节选, k8s v1.36.1)
type KubeSchedulerConfiguration struct {
Profiles []KubeSchedulerProfile // 多 Profile 数组
Extenders []Extender // 全局 Extender,被所有 profile 共享
DelayCacheUntilActive bool
LeaderElection *LeaderElectionConfiguration
...
}
type KubeSchedulerProfile struct {
SchedulerName string // 唯一标识,对应 Pod.spec.schedulerName
Plugins *Plugins // 该 profile 用哪些插件(enable/disable 列表)
PluginConfig []PluginConfig // 该 profile 的插件配置(按插件名索引)
PercentOfNodesToScore *int32 // 该 profile 自己的打分节点比例(覆盖全局)
}
type PluginConfig struct {
Name string // 插件名
Args runtime.Object // 插件配置(YAML/JSON 反序列化)
}
运行时,profile.NewMap()(pkg/scheduler/profile/profile.go:49)把 []KubeSchedulerProfile 转成 profile.Map —— 就是一个 map[schedulerName]Framework:
// pkg/scheduler/profile/profile.go (行 45-65, k8s v1.36.1)
// Map holds frameworks indexed by scheduler name.
type Map map[string]framework.Framework
func NewMap(ctx context.Context, cfgs []config.KubeSchedulerProfile,
r frameworkruntime.Registry, recorderFact RecorderFactory,
opts ...frameworkruntime.Option) (Map, error) {
m := make(Map)
v := cfgValidator{m: m}
for _, cfg := range cfgs {
p, err := newProfile(ctx, cfg, r, recorderFact, opts...)
if err != nil {
return nil, fmt.Errorf("creating profile for scheduler name %s: %v", cfg.SchedulerName, err)
}
if err := v.validate(cfg, p); err != nil {
return nil, err
}
m[cfg.SchedulerName] = p
}
return m, nil
}
func (m Map) HandlesSchedulerName(name string) bool {
_, ok := m[name]
return ok
}
小贴士 — 关于 cfgValidator
cfgValidator(profile.go:92)做了3 项校验:
第 4 项校验更隐性:所有 profile 的 QueueSort 插件必须一致(profile.go:117-125)—— 因为全局只有一个 SchedulingQueue,只能有一种排序方式。
最终的 Scheduler 结构体持有 Profiles profile.Map(pkg/scheduler/scheduler.go:102)。整个 Scheduler 的关键字段:
| 字段 | 类型 | 作用 |
|---|---|---|
| Profiles | profile.Map | 所有 profile 的 Framework,键为 schedulerName |
| SchedulingQueue | internalqueue.SchedulingQueue | 全局唯一 三段队列 |
| Cache | internalcache.Cache | 节点 / Pod 快照,被所有 profile 共享 |
| Extenders | []*SchedulerExtender | 所有 profile 共享 的外部扩展 |
注意
虽然有 N 个 Profile,但 Scheduler 只持有 1 个 SchedulingQueue、1 个 Cache、1 个 informer 集。Pod 进队列前会按 schedulerName 路由到对应 Profile 的 plugin 集合,但共享同一份节点视图。这是 Profile 比"多 scheduler 进程"高效的核心原因。
下面是一份"在线 vs 离线"双 Profile 配置:
# /etc/kubernetes/scheduler-config.yaml (k8s v1.36.1)
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: online-scheduler
plugins:
enable:
- name: CapacityScheduling # 在线业务需要容量保证
- name: NodeResourcesFit
- name: NodeAffinity
- name: InterPodAffinity
- name: PodTopologySpread
pluginConfig:
- name: CapacityScheduling
args:
apiVersion: kubescheduler.config.k8s.io/v1
kind: CapacitySchedulingArgs
defaultPreemption: false # 在线业务禁止被抢占
weight: 100
- schedulerName: batch-scheduler
plugins:
enable:
- name: NodeResourcesFit
- name: NodeAffinity
- name: TaintToleration
- name: Coscheduling # 离线用 gang
disable:
- name: CapacityScheduling
pluginConfig:
- name: Coscheduling
args:
apiVersion: kubescheduler.config.k8s.io/v1
kind: CoschedulingArgs
permitWaitingTimeSeconds: 60
leaderElection:
leaderElect: true
resourceNamespace: kube-system
resourceName: kube-scheduler
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
Pod 通过 spec.schedulerName 字段选择 Profile,默认是 "default-scheduler":
# 在线业务 Pod
apiVersion: v1
kind: Pod
metadata:
name: web-server
spec:
schedulerName: online-scheduler # 路由到 online-scheduler Profile
containers:
- name: nginx
image: nginx:1.27
---
# 离线业务 Pod
apiVersion: v1
kind: Pod
metadata:
name: spark-driver
spec:
schedulerName: batch-scheduler # 路由到 batch-scheduler Profile
containers:
- name: spark
image: spark:3.5
当 Pod 被 informer 监听到时,调度器在 pkg/scheduler/eventhandlers.go:458 调用 profiles.HandlesSchedulerName() 判定该 Pod 能否被本进程处理:
// pkg/scheduler/eventhandlers.go (行 450-460, k8s v1.36.1)
return profiles.HandlesSchedulerName(pod.Spec.SchedulerName)
如果集群里没有匹配 schedulerName 的 Profile,Pod 永远不会被调度(也不会被拒绝)—— 这是常见踩坑点:Pod 卡 Pending 但没有任何报错事件。
设计精髓
v1.36.1 起,每个 Profile 独立有一份 QueueingHint、PreEnqueue、SignPod 配置(在 scheduler.go:387-411 的 profile 循环里按 profileName 分组)。这意味着:
这种"共享 cache + 独立 plugin 链"是 Profile 的灵魂。
新人经常困惑:什么时候用 Profile,什么时候拆多个 kube-scheduler 进程?下面是推荐取舍:
| 场景 | 建议方案 | 原因 |
|---|---|---|
| 在线 vs 离线(共享节点池) | Profile | 共享 cache,开销低,资源视图一致 |
| 不同集群(异构节点池) | 多 scheduler | 节点池互不重叠,独立选主更稳 |
| A/B 测试(新旧调度策略对比) | Profile + label selector | 新策略只调度带特定 label 的 Pod |
| 需要独立选主的子集群 | 多 scheduler + Coordinator | 每个 scheduler 副本独立 Lease |
v1.31 引入(Alpha,pkg/features/kube_features.go:2232)、v1.33 Beta 的 CoordinatedLeaderElection 是为"多 scheduler 跨集群共享视图"设计的选主机制。
传统的 kube-scheduler 选主(基于 coordination.k8s.io/v1.Lease)存在两个问题:
Coordinated LeaderElection 引入 LeaseCandidate 机制:每个候选者声明自己的 二进制版本 和 仿真版本,只有相同版本组 内能互选,避免跨版本选主。
┌──────────────────────────────────────────────────────────────────────┐
│ Coordinated LeaderElection · v1.36.1 │
│ │
│ ┌──────────────────────────┐ │
│ │ kube-scheduler (v1.36) │ │
│ │ - binaryVersion=v1.36 │ │
│ │ - emulationVersion=v1.36│ │
│ └─────────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ coordination.k8s.io/v1 │ │ coordination.k8s.io/v1 │ │
│ │ LeaseCandidate │ │ Lease │ │
│ │ - name: kube-scheduler-0 │ │ - name: kube-scheduler │ │
│ │ - binaryVersion: v1.36 │ │ - holderIdentity: ... │ │
│ │ - emulationVersion: v1.36│ │ - renewTime: ... │ │
│ └─────────────┬────────────┘ └──────────────────────────┘ │
│ │ ▲ │
│ └──── 每 1s 心跳 ────────────┘ │
│ │
│ 其他 scheduler 候选者 (v1.35, v1.34, ...) 不会选 v1.36 为主 │
└──────────────────────────────────────────────────────────────────────┘
在 cmd/kube-scheduler/app/server.go 的 runCommand 里有两个关键判断:
// cmd/kube-scheduler/app/server.go (行 230-308, k8s v1.36.1)
// 1) 启动 LeaseCandidate controller(让本节点作为候选者可见)
if cc.LeaderElection != nil && utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
binaryVersion, err := semver.ParseTolerant(...)
emulationVersion, err := semver.ParseTolerant(...)
leaseCandidate, waitForSync, err := leaderelection.NewCandidate(
cc.Client,
metav1.NamespaceSystem,
cc.LeaderElection.Lock.Identity(),
kubeScheduler, // component 名
binaryVersion.FinalizeVersion(),
emulationVersion.FinalizeVersion(),
coordinationv1.OldestEmulationVersion, // 最低兼容版本
)
if err != nil { return err }
readyzChecks = append(readyzChecks, healthz.NewInformerSyncHealthz(waitForSync))
go leaseCandidate.Run(ctx)
}
// 2) 创建 LeaderElector,标记 Coordinated = true
if cc.LeaderElection != nil {
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.CoordinatedLeaderElection) {
cc.LeaderElection.Coordinated = true // 关键:启用协调选主
}
cc.LeaderElection.Callbacks = leaderelection.LeaderCallbacks{...}
leaderElector, err := leaderelection.NewLeaderElector(*cc.LeaderElection)
...
leaderElector.Run(ctx)
}
小贴士 — 关于 OldestEmulationVersion
coordinationv1.OldestEmulationVersion 是 v1.31 引入的"可被仿真运行的最低版本"。比如集群从 v1.35 升到 v1.36 时,v1.36 节点可以声明自己仿真 v1.35,从而接管 v1.35 的 Lease,避免升级期出现"无主"空窗。
cmd/kube-scheduler/app/server.go:309-318 的 OnStartedLeading 回调做了3 件事:
OnStartedLeading: func(ctx context.Context) {
close(waitingForLeader)
if cc.ComponentConfig.DelayCacheUntilActive {
logger.Info("Starting informers and waiting for sync...")
startInformersAndWaitForSync(ctx)
logger.Info("Sync completed")
}
sched.Run(ctx)
},
注意
DelayCacheUntilActive 在多 scheduler 场景非常重要:默认 false 时所有副本都同步 cache,内存放大 N 倍;设为 true 时只 Leader 同步 cache,非 Leader 副本内存占用极低。生产大集群建议必开。
| 模式 | 数据载体 | 选主逻辑 | 适用场景 |
|---|---|---|---|
| 无选主 | 无 | 每副本都调度(会冲突) | 仅开发测试 |
| Lease 选主 (默认) |
coordination.k8s.io/v1.Lease | Lease 持有者即 Leader | 同版本集群 |
| Coordinated 选主 (v1.31+) |
Lease + LeaseCandidate | 同版本组内选主 | 滚动升级、多版本共存 |
症状:业务方反映 Pod 卡 Pending,kubectl describe pod 显示 Events: <none>,没有 FailedScheduling 也没有任何报错。
原因:Pod 的 spec.schedulerName 设成了不存在的 Profile 名(比如 "my-ai-scheduler" 写错了)。调度器在 eventhandlers.go:458 直接丢弃该 Pod,不会入队,不会调度,不会报错。
修复:
# 1. 查 Pod 实际要的 schedulerName
kubectl get pod <name> -o jsonpath='{.spec.schedulerName}'
# 2. 查 scheduler 实际支持的 profile
kubectl -n kube-system get cm kube-scheduler-config -o yaml | grep schedulerName
# 或
kubectl logs -n kube-system kube-scheduler-xxx | grep "scheduler name"
症状:开了 CoordinatedLeaderElection=true 后,kubectl get lease -n kube-system 显示 Lease 的 holderIdentity 不断随机变化,每个副本都当了一会 Leader。
原因:v1.31 Alpha 期间 LeaseCandidate 与 Lease 的协调有 bug,多副本同时声明自己是 Leader。升级到 v1.33+ Beta 后修复。
修复:升级到 v1.33+,或暂时关掉 CoordinatedLeaderElection 回到普通 Lease 选主。
症状:scheduler 启动失败,日志:plugins required for profile with scheduler name "xxx"
原因:Profile 配置里 plugins: null。v1.36.1 的 cfgValidator.validate()(profile.go:102)强制要求每个 profile 都显式写 plugins 段,不能省略。
修复:
profiles:
- schedulerName: my-scheduler
plugins: # 必填,即使为空也要写
enable: [] # 空数组 = 用默认插件
disable: []
症状:多 Profile 集群里,Pod 不按 Priority 出队,低优先级 Pod 先调度。
原因:两个 Profile 配置了不同的 QueueSort 插件。但全局只有一个 activeQ(scheduling_queue.go:409),只能用一种 Less 函数。profile.go:117 的 cfgValidator 第一个 profile 的 QueueSort 决定全局顺序,第二个 profile 写啥都不生效。
修复:所有 Profile 必须用同一种 QueueSort(通常是 PrioritySort)。生产中通过 Helm / Kustomize 模板强制统一。
没有硬编码上限,但每多一个 Profile,scheduler 启动时多 O(N) 个 Framework 实例、调度时多 O(N) 个路由判断。生产经验 ≤ 10,超过建议拆多进程。
共享。scheduler.New()(pkg/scheduler/scheduler.go:314)只创建一份 informerFactory,所有 profile 共用同一份 Node / Pod 快照。
不会。Pod 进 activeQ 时按 pod.Spec.SchedulerName 入对应 profile 的 plugin 集合,但同一份 activeQ。Pop 时根据 Pod 的 schedulerName 选 framework。竞争点是同一节点:两个 profile 的 Pod 可能都想调度到同一节点,由节点的容量裁决。
pkg/scheduler/framework/runtime/registry.go:44-66 的 DecodeInto() 把 YAML/JSON 字节流转成 runtime.Object,由插件构造函数断言成具体类型(如 *CapacitySchedulingArgs)。
改 /etc/kubernetes/scheduler-config.yaml,加一个 profiles 条目,重启 scheduler 即可(k8s 不支持 scheduler 配置热加载)。生产建议先用 --config 走 ConfigMap 挂载,缩短 reload 时间。
需要。CoordinatedLeaderElection 不是替代 LeaderElection,而是叠加:
所以两份配置都得写。
# 1. Lease 当前持有者
kubectl -n kube-system get lease kube-scheduler -o yaml
# 2. 候选者列表
kubectl -n kube-system get leasecandidate
# 3. 每个 scheduler 副本的角色
kubectl -n kube-system logs kube-scheduler-xxx | grep -E "leader|leading"
几乎无影响。LeaseCandidate 每 1 秒一次心跳(server.go:254 go leaseCandidate.Run(ctx)),apiserver 的 LeaseCandidate 写入是异步的,对调度主循环无阻塞。
v1.36 节点声明 binaryVersion=v1.36, emulationVersion=v1.35(最后一个兼容版本),让 v1.35 的 Lease 被 v1.36 节点顺利接管,避免"无主空窗"。升级完成后所有节点统一 v1.36,再统一 emulationVersion。
不能。Pod 的 spec.schedulerName 是唯一路由键。如果业务想要"先 X 调度,失败后 Y 兜底",需要两个 Deployment 或 改 schedulerName(通过 controller 改写)。
Quota 与 scheduler 无关,是 namespace 级 admission control。建议按业务部门拆 namespace,不要靠 Profile 强隔离。Profile 只决定"调度策略",不决定"资源配额"。
# values.yaml
scheduler:
profiles:
- name: online-scheduler
plugins: [...]
- name: batch-scheduler
plugins: [...]
# templates/configmap.yaml
{{- range .Values.scheduler.profiles }}
- schedulerName: {{ .name }}
plugins:
{{ toYaml .plugins | indent 4 }}
{{- end }}
profile.go:98-127 的 3 项校验:
v1.36.1 起 可以关:--feature-gates=CoordinatedLeaderElection=false(Beta 阶段仍可关)。GA 后不可关。生产建议先在 staging 跑 2 周再上生产。
调度器监听 Pod Update 事件:PriorityQueue.Update()(scheduling_queue.go:126)会处理。如果新 schedulerName 在本进程 Profile Map 里,正常调度;否则 Pod 卡 Pending且没有事件。
可以。在 scheduler 启动时通过 WithFrameworkOutOfTreeRegistry()(scheduler.go:209-215)传入 Out-of-Tree 插件,然后 Profile 配置里 plugins.enable 写插件名即可。
不是。但生产强烈建议开,否则多副本会重复调度,Pod 出现"已被调度但又被改写"的诡异行为。单副本开发环境可以关。
kube-scheduler 自己创建。leaderelection.NewCandidate()(server.go:241)内部会通过 EnsureLeaseCandidateRegistration 检查并创建 CRD。
检查两件事:
v1.33 引入的 Priority-based 选主是另一种新机制(LeaderElection.Priority),目前 不能与 Coordinated 共用。要么用 Coordinated,要么用 Priority。
本篇覆盖了 Scheduler Profile 与 Coordinated LeaderElection。调度专题剩余主线:
调度专题的目标读者是资深运维开发:能读 Go 源码、有集群运维经验、对 k8s 整体架构已有认知。下一篇会从 手写 Score 插件 切入,把"自研插件落地"这件事讲透。
本文参考与源码链接:
• profile.go · profile.Map 实现
• scheduler.go · Scheduler + Profiles
• config/types.go · KubeSchedulerProfile
• eventhandlers.go · schedulerName 路由
• server.go · Coordinated LeaderElection 入口
• kube_features.go · CoordinatedLeaderElection 门控
• registry.go · DecodeInto PluginConfig
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。