Scheduler Framework、Profiles 与多调度器
一个延迟敏感服务上线后,团队把 Pod 的 schedulerName 改成 latency-scheduler,希望它优先选择缓存已预热的节点。Deployment 很快创建出副本,Pod 却一直停在 Pending,既没有 FailedScheduling,也没有节点扩容。最终原因不是资源不足,而是集群里根本没有以这个名字工作的调度器。API Server 接受了合法字段,默认 kube-scheduler 只领取与自己名称匹配的 Pod,错误名称因此变成一条无人消费的工作队列。
另一个团队直接修改主调度器插件权重,把某个业务的局部偏好变成全集群默认策略。变更语法正确,普通服务却开始集中到少数节点,调度延迟和容量碎片同时上升。问题不在“插件不够智能”,而在策略作用域没有隔离:Profile、独立 scheduler 进程、Pod 的 schedulerName 和节点供给共同构成控制边界,任何一处名称、版本或所有权不一致,都会把局部实验放大为控制面事故。
从调度周期和绑定周期理解扩展点
Scheduler Framework 把 kube-scheduler 内部流程拆成一组稳定扩展点。一个待调度 Pod 先进入 QueueSort 管理的队列,再经历 PreFilter、Filter 形成可行节点集合,经过 PostFilter 尝试抢占等补救,随后由 PreScore、Score 排序。选定节点后,Reserve 暂存假设状态,Permit 可以允许、拒绝或等待,PreBind 与 Bind 完成提交,PostBind 做通知。与之并行的绑定周期可以和后续 Pod 的调度周期并发,因此插件不能把一次调用顺序误当成全局串行保证。
插件状态不是简单的 true/false。Success 继续前进,Unschedulable 表示当前条件下无法调度并可能在相关事件后重试,UnschedulableAndUnresolvable 表示抢占也难以修复,Wait 会把 Pod 留在 Permit 等待集合,Error 则是插件或内部执行失败。Reserve 之后若后续阶段失败,框架会按逆序调用已执行 Reserve 插件的 Unreserve;自定义插件若只预留外部资源却没有幂等释放,就会留下配额、端口或设备泄漏。
PreEnqueue、QueueingHint 等机制决定 Pod 何时进入或重新激活,错误提示可能造成无意义重试或永久沉睡。调度吞吐不仅取决于 Filter 算法速度,还取决于队列等待、插件外部调用、并行度、API 限流和绑定耗时。官方的 Scheduling Framework 给出了扩展点与周期语义;实现插件前应先判断内置配置是否已经能表达需求,因为进程内插件与 kube-scheduler 共享故障域。
Profile 是进程内策略隔离,不是新的调度器副本
一个 kube-scheduler 进程可以配置多个 Profile。每个 Profile 以唯一的 schedulerName 暴露给 Pod,并分别配置插件启用、禁用、权重和参数。Pod 的 spec.schedulerName 选择 Profile;没有显式设置时,通常由默认调度器处理。多个 Profile 共享同一进程、缓存和选主状态,所以它们能减少部署开销,却不能隔离崩溃、CPU 饥饿或错误插件。
调度器配置稳定入口是 kubescheduler.config.k8s.io/v1;旧 v1beta3 已从 Kubernetes 1.29 移除,不能继续把旧配置文件直接交给新二进制。下面的片段保留默认插件,只在 latency-scheduler Profile 中提高 NodeResourcesFit 权重;演示值不是生产结论,真实权重要通过代表性 Pod 回放和容量基线确定:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
profiles:
- schedulerName: default-scheduler
- schedulerName: latency-scheduler
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 2
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated所有 Profile 的 QueueSort 插件及其参数必须一致,因为进程共享 pending queue;不能让一个 Profile 按优先级排序、另一个按自定义租户规则排序。带 multiPoint 的配置可在一个位置为插件启用多个扩展点,但具体扩展点的显式配置优先级更高,升级时必须检查默认插件集变化。字段和默认行为应以 kube-scheduler 配置及 KubeSchedulerConfiguration v1 API 为准。
自托管控制面启用 Profile 要保留可恢复入口
Profile 不需要安装额外控制器,但修改控制面 kube-scheduler 配置通常只适用于自建或允许定制控制面的发行版。先确认配置文件、静态 Pod manifest、当前镜像与回退通道;托管 Kubernetes 往往不开放主调度器配置,此时应选择供应商支持的能力或部署独立调度器,不能直接修改不可持久化的控制面文件。
在自托管实验集群中,先备份实际配置并验证 API:
kubectl version
kubectl -n kube-system get pods -l component=kube-scheduler -o wide
kubectl -n kube-system get lease
kubectl auth can-i get pods --all-namespaces
kubectl auth can-i get leases.coordination.k8s.io -n kube-system
kube-scheduler --version
kube-scheduler --config=/etc/kubernetes/scheduler-config.yaml --write-config-to=/tmp/effective-scheduler.yaml--write-config-to 适合在启动前完成解码、默认值填充和 API 兼容检查。将上面的 profiles 合并进现有配置后,先在隔离控制面节点或临时进程用不同端口验证,不要让两个不同配置的实例争用同一 leader election lease。静态 Pod 模式还要把配置文件作为只读 volume 挂载,并把 --config 指向容器内路径。修改 manifest 会触发 kubelet 重建控制面 Pod,错误配置可能使调度能力整体消失,因此必须保留原文件、控制面节点登录能力和无需调度即可执行的恢复路径。
预期启动证据是 kube-scheduler Pod Ready、对应 lease 有 holder、日志没有配置解码错误,且 /configz 或启动日志反映两个 Profile。配置文件可能包含 API 端点、证书路径和认证信息,不应放入公开 ConfigMap;文件权限、备份和支持包都要避免泄露控制面 kubeconfig。
托管集群用独立调度器隔离作用域
独立调度器是另一组进程、ServiceAccount、配置、日志、指标和 leader election lease。它可以继续使用原生 kube-scheduler 二进制与内置插件;out-of-tree Framework 插件则必须与目标 Kubernetes 依赖一起编译进自定义调度器二进制,不能在运行中的官方镜像里动态装载。部署前确认目标发行线与控制面 minor 处于 Kubernetes 支持的组件版本偏差内,并优先使用与控制面相同 minor 的发行物;镜像固定到不可变摘要后,再检查最小权限:读取 Pod、Node、Namespace、PV/PVC、StorageClass 和 CSINode,更新 Pod status、绑定 Pod,读写 Event,以及在指定 namespace 读写自己的 Lease。
官方多调度器示例会复用集群自带的 system:kube-scheduler ClusterRole,但它是集群级角色,通常比单一实验命名空间需要的权限更宽。共享或生产环境应把 Node、PV、StorageClass 等集群级只读权限与目标 namespace 的 Pod binding、Event 写权限分开授予,再按实际审计记录补齐所需资源;仅靠配置中的 schedulerName 或 Pod label 不是授权边界。还应使用准入策略限制哪些命名空间可以填写该 schedulerName。先做权限探测,初次返回 no 正好说明角色尚未绑定,不能在没有复查的情况下继续启动调度器:
kubectl create namespace scheduler-lab
kubectl create serviceaccount latency-scheduler -n scheduler-lab
kubectl auth can-i list nodes \
--as=system:serviceaccount:scheduler-lab:latency-scheduler
kubectl auth can-i create pods/binding -n scheduler-lab \
--as=system:serviceaccount:scheduler-lab:latency-scheduler
kubectl auth can-i update leases.coordination.k8s.io -n scheduler-lab \
--as=system:serviceaccount:scheduler-lab:latency-scheduler配置必须使用唯一名称和唯一租约:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
resourceNamespace: scheduler-lab
resourceName: latency-scheduler
clientConnection:
qps: 20
burst: 40
profiles:
- schedulerName: latency-scheduler把该文件放入 scheduler-lab 中只读 ConfigMap,Deployment 使用与集群兼容的 registry.k8s.io/kube-scheduler:<compatible-version>,参数为 --config=/etc/kubernetes/scheduler/config.yaml。高可用形态至少两个副本,共享上述租约;不要通过两个不同 schedulerName 的容器共用同一 lease,也不要让实验调度器抢占主调度器的 lease。镜像版本应从集群发行说明和镜像锁定文件取得,而不是复制示例占位符。
正向实验让一个 Pod 只被指定调度器领取
独立调度器 Ready 且 lease holder 稳定后,创建最小 Pod:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-by-latency
namespace: scheduler-lab
labels:
app: scheduler-profile-lab
spec:
schedulerName: latency-scheduler
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 10m
memory: 16Mi应用后同时观察对象、事件、调度器日志和租约:
kubectl apply -f scheduled-by-latency.yaml
kubectl -n scheduler-lab get pod scheduled-by-latency -w
kubectl -n scheduler-lab get pod scheduled-by-latency \
-o jsonpath='{.spec.schedulerName}{"\t"}{.spec.nodeName}{"\n"}'
kubectl -n scheduler-lab get events \
--field-selector involvedObject.name=scheduled-by-latency \
--sort-by=.lastTimestamp
kubectl -n scheduler-lab logs deploy/latency-scheduler --since=10m
kubectl -n scheduler-lab get lease latency-scheduler -o yaml闭环的预期不是“Pod 变成 Running”这一条结果,而是 spec.schedulerName=latency-scheduler、spec.nodeName 非空、PodScheduled=True、Event 的 reporting controller 指向对应 scheduler、日志出现该 Pod 的调度尝试,并且 lease holder 属于当前副本。容器随后因镜像拉取失败而 Pending/Waiting,仍可能说明调度已经成功;要把 PodScheduled 与容器 Ready 分开判断。
Profile 权重实验还要准备至少两个均满足 Filter 的节点,并记录节点 requests、标签与最终分数日志。单次落点不能证明权重长期有效;应重复创建一组等价 Pod,比较分布趋势、p99 调度延迟和可行节点碎片,同时确认其他 Profile 的分布没有变化。
反向实验用无人认领的名称暴露路由错误
把同样的 Pod 名改为 orphan-scheduler,并把 schedulerName 改成不存在的名称:
apiVersion: v1
kind: Pod
metadata:
name: orphan-scheduler
namespace: scheduler-lab
spec:
schedulerName: no-such-scheduler
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 10m
memory: 16Mikubectl apply -f orphan-scheduler.yaml
kubectl -n scheduler-lab get pod orphan-scheduler -o yaml
kubectl -n scheduler-lab get events \
--field-selector involvedObject.name=orphan-scheduler
kubectl -n scheduler-lab logs deploy/latency-scheduler --since=5m | grep orphan-scheduler预期 spec.nodeName 为空、PodScheduled 条件长期缺失或未转为 True,且可能没有常见 FailedScheduling Event,因为没有调度器领取它并执行 Filter。latency-scheduler 日志也不应出现该 Pod。这一组“对象存在但无调度事件”的负证据与资源不足完全不同;扩容节点、放宽 affinity 都不会修复。恢复方式是把控制器模板中的名称改回已注册的 schedulerName,让 Deployment/Job 创建新 Pod,或在确认字段可更新且对象类型允许时重建测试 Pod。
再停止独立调度器 Deployment,创建一个正确填写 latency-scheduler 的 Pod,可以验证 HA 与失联语义:所有副本停止时新 Pod 无人绑定;恢复至少一个副本并取得 lease 后,队列中的 Pod 应被继续处理。观察从 holder 消失、Pod 等待到新 holder 绑定的时间线,比只看 Deployment Available 更能证明恢复能力。
事件、日志和指标要定位到同一调度尝试
调度故障先按三类分型。完全没有调度 Event,优先检查 schedulerName、调度器副本、selector 和 lease;有 FailedScheduling,读取 Filter/PostFilter 汇总原因并检查 requests、标签、污点、卷和插件状态;已绑定但容器未就绪,则转向 kubelet、镜像、网络和应用启动证据。三类问题不能用同一条“重启 scheduler”处理。
kubectl -n <namespace> get pod <pod> \
-o jsonpath='{.spec.schedulerName}{"\t"}{.spec.nodeName}{"\t"}{.status.conditions}{"\n"}'
kubectl -n <namespace> describe pod <pod>
kubectl -n <scheduler-namespace> get deploy,pod,lease
kubectl -n <scheduler-namespace> logs deploy/<scheduler> --prefix --since=15m
kubectl -n <scheduler-namespace> port-forward deploy/<scheduler> 10259:10259
TOKEN=$(kubectl -n <scheduler-namespace> create token <scheduler-service-account> --duration=10m)
curl -k -H "Authorization: Bearer ${TOKEN}" \
https://127.0.0.1:10259/metricskubectl get --raw /metrics 读取的是 kube-apiserver 指标,不是独立 scheduler 指标;上面的端口转发应只用于受控排障窗口,ServiceAccount 还必须显式拥有读取 /metrics 这个 non-resource URL 的权限。生产采集应通过受保护的 Service、认证代理或监控系统访问安全端口,不能长期复用排障令牌或关闭 TLS 校验。生产观测至少关联 pending queue 年龄、每个 Profile 的调度尝试结果、端到端 scheduling latency、插件执行时延、API client 限流、Permit 等待数量、抢占尝试和 binding 错误。指标标签名称会随发行线变化,采集规则应从实际 /metrics 枚举并固定兼容测试,不能依赖未经校验的示例名字。日志等级临时升高会暴露 Pod、节点、拓扑和约束,故障结束后要恢复并限制留存。
插件故障会扩大到吞吐、正确性和外部资源
进程内插件在热路径执行。Filter 对每个 Pod 与候选节点调用,Score 还会遍历可行节点;一次外部网络查询可能被节点数和排队 Pod 数放大。插件应使用框架快照和 informer 缓存,避免逐节点访问外部 API;对必要外部依赖设置明确超时、缓存新鲜度和失败语义。失败开放可能违反放置政策,失败关闭可能阻断全部工作负载,两种选择都要由业务风险决定。
Reserve/Unreserve、Permit 和 Bind 是最容易留下中间状态的区域。Reserve 插件应以 Pod UID 为幂等键,重复 Unreserve 不得报错;Permit 等待必须有上限并暴露等待原因;自定义 Bind 只有在真正负责该 Pod 时才拦截默认绑定,失败后要说明 API 对象和外部事务如何补偿。调度器重启会丢失进程内状态,任何不能从 API 或外部持久记录重建的预留都不适合作为生产事实源。
插件供应链与 kube-scheduler 二进制绑定。Go 插件通常需要与目标 Kubernetes 源码、依赖和编译选项严格匹配,升级一个 minor 都应重新构建、扫描和回放。若需求只是不同内置插件权重,Profile 更轻;若需要故障隔离或独立升级,独立 scheduler 更合适;若要求跨集群队列、公平共享或 gang 语义,应评估专门批调度系统,避免把主调度器扩展成不可维护的平台。
容量与成本不能只算调度器副本
多 Profile 的直接成本较低,但共享缓存、队列和 CPU,一个昂贵插件仍会拖慢其他 Profile。独立调度器增加 Deployment、leader election、日志、指标、镜像与升级流水线,也会为 Pod、Node、PV 等对象建立自己的 informer cache,并向 API Server 发起 list/watch。集群规模、对象 churn 和副本数上升时,控制面内存、watch 流量与审计日志都会增加。
容量评审应记录每秒新建 Pod 峰值、pending queue 最大年龄、节点数、Filter/Score 调用量、插件 p99 时延、API 限流与故障恢复时间。HA 副本主要缩短进程故障恢复,并不会线性增加有效调度吞吐,因为同一 lease 通常只有一个 active leader。盲目增加副本会增加缓存和 watch 成本,却不解决慢插件或 API Server 限流。
业务容量还受策略分流影响。某个 scheduler 只能看到特定节点池时,即使全集群空闲,也可能没有可行节点;节点供应器必须能根据这些 Pod 的硬约束创建正确类型。调度策略、节点池模板和扩容器模拟应共同回放,否则“调度器按设计拒绝”可能被错误解释成供给不足并持续烧钱。
升级、回滚与退出围绕调度所有权展开
升级前冻结 Profile 名称和插件配置,导出代表性工作负载模板,在隔离集群或影子 scheduler 上回放 QueueSort、Filter、Score、抢占、卷绑定和 Permit 场景。比较升级前后的成功率、失败类型、节点分布、队列年龄和插件延迟。配置 API 从旧 beta 迁移到 v1 时,应先用目标二进制解码并输出有效配置;不兼容字段不能等到主调度器重启后才发现。
独立调度器采用双轨迁移:部署新名称与新 lease,只把 canary namespace 的少量工作负载改向新 schedulerName;验证后逐批迁移控制器模板。回滚先把模板切回旧名称,再确认新调度器队列归零,最后缩容新副本。直接删除旧 scheduler 会让仍引用旧名称的新建 Pod 无人处理;准入策略和仓库扫描应阻止已退役名称继续出现。
平台团队拥有调度器二进制、配置、lease、RBAC 和 SLO;应用团队拥有 Pod 模板中的 schedulerName、requests 与降级选择;节点平台拥有可供给节点类型;安全团队审查插件供应链、节点信息读取和高权限绑定;每个插件还需要明确代码 owner、兼容矩阵、性能预算、失败策略和退出替代方案。Profile 名称与权限应进入资产清单,离职、项目下线或供应商退出时同步撤销镜像仓库、签名密钥、ServiceAccount 与 ClusterRoleBinding。
实验完成后先删除测试 Pod,再缩容并移除独立调度器,最后删除权限和命名空间:
kubectl -n scheduler-lab delete pod scheduled-by-latency orphan-scheduler \
--ignore-not-found
kubectl -n scheduler-lab scale deploy latency-scheduler --replicas=0
kubectl -n scheduler-lab get pods,lease
kubectl delete clusterrolebinding latency-scheduler --ignore-not-found
kubectl delete namespace scheduler-lab --wait=true
kubectl get pods -A \
-o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,SCHEDULER:.spec.schedulerName \
| grep -E 'latency-scheduler|no-such-scheduler' || true如果改动过主 kube-scheduler 配置,应先恢复原配置并确认默认 scheduler 重新持有 lease、普通 Pod 可完成绑定,再删除备份。退出完成的证据是没有工作负载引用退役名称、没有遗留 lease 和高权限绑定、默认调度延迟回到基线,并且所有外部预留都能按 Pod UID 核销。
