Kubernetes 基础体系 · 第 83/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes 调度性能与扩展:Profile、Plugin、Queue、规模和测试
Kubernetes 调度器(kube-scheduler)的职责,是为尚未绑定节点的 Pod 选择一个满足约束、资源和策略要求的节点,并将调度结果写回 Pod 的 spec.nodeName。它不是一个单纯的“遍历节点并打分”的函数,而是一个由缓存、队列、调度框架、插件、绑定流程和 API Server 交互组成的并发系统。
要讨论调度性能,必须同时回答五个问题:
- 一个 Pod 如何进入、停留和离开调度队列?
- 一个调度周期如何从候选节点中筛选并选择节点?
- Profile 和 Plugin 如何改变调度器行为?
- 节点、Pod、约束和并发规模如何影响复杂度?
- 如何证明扩展没有破坏正确性,并测量优化是否真的有效?
下文基于当前稳定的 Kubernetes 调度框架 API 和配置模型说明机制。具体插件参数、指标名称和默认值可能随 Kubernetes 版本变化;生产环境应以所运行版本的官方 API 文档和 /metrics 输出为准。
一、从 Pod 到 Node:调度器的完整数据流
一个典型的调度路径如下:
flowchart TD
A[API Server 中的 Pending Pod] --> B[Shared Informer / Scheduler Cache]
B --> C[Scheduling Queue]
C --> D[PreEnqueue]
D --> E[Scheduling Cycle]
E --> F[PreFilter]
F --> G[Filter: 过滤节点]
G --> H{存在可行节点?}
H -- 否 --> I[PostFilter / Unschedulable]
I --> J[UnschedulableQ 或 BackoffQ]
J --> C
H -- 是 --> K[PreScore]
K --> L[Score]
L --> M[NormalizeScore]
M --> N[选择最高分节点]
N --> O[Reserve]
O --> P[Permit]
P --> Q[PreBind]
Q --> R[Bind]
R --> S[PostBind]
S --> T[Pod 获得 spec.nodeName]
这个流程包含两类容易混淆的状态:
- 调度器缓存中的状态:来自 Node、Pod、PV、Service、存储拓扑等 Informer 事件。
- API Server 中的真实对象状态:调度器最终必须通过 API 请求完成绑定或更新。
缓存用于提高读取性能,但不是绝对实时的。因此调度器必须接受“缓存观察到的状态”和“API Server 当前状态”之间存在短暂差异。绑定阶段还可能因为资源版本冲突、节点消失或其他控制器修改对象而失败。
1. Scheduling Cycle 和 Binding Cycle
调度器通常一次为一个 Pod 执行调度周期(Scheduling Cycle),包括:
- 从队列取出 Pod;
- 运行过滤插件;
- 运行打分插件;
- 选择节点;
- 执行保留和许可逻辑。
绑定周期(Binding Cycle)负责:
- 执行
PreBind; - 执行
Bind; - 执行
PostBind; - 最终将 Pod 绑定到节点。
调度周期在逻辑上是串行的:同一时间只有一个 Pod 处于选择节点的主调度过程。这种串行化有利于一致性,尤其是插件需要依据调度器缓存执行资源假设时。绑定阶段可以异步执行,因此绑定延迟不一定阻塞下一个 Pod 的完整调度周期。
这也解释了一个常见现象:CPU 很空闲,但调度吞吐仍然不高。瓶颈可能在:
- 某个串行调度插件;
- 队列锁竞争;
- API Server 绑定请求;
- 缓存事件处理;
- 大量候选节点的过滤和打分;
- 插件等待
Permit; - 失败 Pod 的退避和重入队列。
二、Queue:调度器如何管理 Pending Pod
1. 三种核心队列状态
调度队列通常由三部分组成:
- ActiveQ:当前可被调度器取出的 Pod。
- BackoffQ:最近调度失败、处于退避窗口内的 Pod。
- UnschedulableQ:当前没有可行节点,且暂时没有明确事件触发重试的 Pod。
可以将一个 Pod 的典型状态抽象为:
New
└─> ActiveQ
├─> Scheduling Cycle 成功 ──> Bind
├─> 调度失败且需要退避 ─────> BackoffQ
└─> 无可行节点 ────────────> UnschedulableQ
ActiveQ 通常使用堆结构,堆的优先级由 QueueSort 插件决定。队列元素不是“越早创建越优先”这一种固定规则,而是可以由优先级、创建时间、PodGroup 信息或其他策略计算。
BackoffQ 的目的是防止一个必然失败的 Pod 在短时间内反复占用调度器。退避时间通常会随连续失败次数增加,但有上限。具体退避参数和默认值属于版本实现细节,不应在业务逻辑中假设固定数字。
UnschedulableQ 中的 Pod 并不一定要等到固定定时器才重试。节点资源、污点、PV、Pod 删除等事件可能通过插件注册的事件关系,将相关 Pod 移回 ActiveQ。
2. 为什么事件注册会影响性能和正确性
假设一个 Pod 因为没有满足 Node Affinity 的节点而不可调度:
- 如果后来创建了一个满足 Affinity 的 Node;
- 事件处理逻辑能够识别该 Node 变化与此类 Pod 相关;
- Pod 就可以被重新加入
ActiveQ。
如果插件错误地没有注册相关事件,Pod 可能长时间停留在 UnschedulableQ,直到周期性刷新或其他事件偶然触发。反过来,如果插件对所有 Node 事件都唤醒所有 Pending Pod,会造成大量无效重试。
因此,队列性能不仅取决于队列数据结构,还取决于插件对“什么变化会使一个失败结论失效”的建模质量。
3. QueueSort 的全局约束
QueueSort 决定 ActiveQ 中 Pod 的排序规则。一个重要限制是:同一个调度器实例中的不同 Profile 不能随意使用互不兼容的 QueueSort 逻辑。调度队列是共享的,调度器必须有一个一致的全局顺序。
因此,Profile 可以改变某个 Pod 的过滤、打分或绑定插件,但不能把同一共享队列变成多个互相矛盾的排序系统。需要多套完全不同队列语义时,通常应运行多个调度器实例,并让 Pod 使用不同的 schedulerName。
三、Profile:同一个调度器中的多套调度策略
1. Profile 的含义
一个调度器 Profile 是一组插件配置和插件参数的组合。Pod 通过:
spec:
schedulerName: batch-scheduler
选择某个 Profile。调度器通过 schedulerName 找到对应 Profile,然后使用该 Profile 的插件集合执行调度。
默认 Profile 的名称通常是 default-scheduler。如果 Pod 不设置 schedulerName,它会使用默认调度器名称;自定义 Profile 不会自动接管这些 Pod。
Profile 适合表达以下差异:
- 在线服务偏好均衡;
- 批处理任务偏好高装箱率;
- 特定租户启用额外的拓扑约束;
- 某类 Pod 需要许可插件;
- 不同工作负载使用不同的打分权重。
它不等于启动多个调度器进程。多个 Profile 通常共享:
- 调度器进程;
- Informer;
- Scheduler Cache;
- 调度队列;
- API 客户端;
- 进程资源和故障域。
2. 一个 Profile 配置示例
下面配置使用内置插件构造两个 Profile。字段和插件参数应以目标 Kubernetes 版本的 kubescheduler.config.k8s.io/v1 API 为准。
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
plugins:
score:
disabled:
- name: PodTopologySpread
enabled:
- name: NodeResourcesFit
weight: 1
- schedulerName: batch-scheduler
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 5
- name: PodTopologySpread
weight: 1
这里的含义不是“第二个 Profile 只运行列出的插件”。插件有默认启用集合,enabled 和 disabled 用于在默认集合基础上调整。具体启用关系由调度框架和版本默认配置决定;生产中应使用目标版本的默认配置生成结果核对,而不是只凭配置片段推断最终插件列表。
创建一个使用 batch-scheduler 的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: batch-example
spec:
schedulerName: batch-scheduler
restartPolicy: Never
containers:
- name: workload
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
resources:
requests:
cpu: "500m"
memory: "256Mi"
前置条件是:
- 调度器进程加载了上面的配置;
batch-schedulerProfile 存在;- 集群中有满足资源和其他默认约束的节点。
如果 Profile 不存在,或者调度器实例没有运行名为 batch-scheduler 的配置,Pod 会保持 Pending,并在事件或调度器日志中表现为找不到对应调度器处理路径。schedulerName 只是路由字段,不会自动创建调度器。
3. Profile 的隔离边界
Profile 并不提供进程级隔离。一个 Profile 中的插件发生 panic、死循环或严重内存泄漏,可能影响整个调度器进程。需要不同升级节奏、不同 RBAC、不同故障域或不同资源上限时,应考虑运行独立调度器实例。
多个调度器实例还会带来新的协调问题:
- 它们可能竞争处理同一个未绑定 Pod;
- Pod 必须明确设置
schedulerName; - 自定义调度器需要正确观察和更新对象;
- 绑定冲突和重复决策需要由 API Server 及调度器逻辑处理;
- 监控和排障复杂度增加。
四、Plugin:调度框架扩展点及其生命周期
1. Plugin 不是一个统一阶段
调度插件(Plugin)是实现调度框架接口的 Go 组件。不同扩展点表示不同生命周期阶段:
| 扩展点 | 作用 |
|---|---|
QueueSort |
决定队列中的 Pod 顺序 |
PreEnqueue |
Pod 进入 ActiveQ 前进行预检查 |
PreFilter |
调度节点前做一次性预处理 |
Filter |
判断单个节点是否可行 |
PostFilter |
没有可行节点时执行,例如抢占 |
PreScore |
打分前做一次性预处理 |
Score |
为每个可行节点计算分数 |
NormalizeScore |
将某插件的分数归一化 |
Reserve |
在调度器内部预留资源或状态 |
Permit |
允许、拒绝或等待调度继续 |
PreBind |
写入绑定前执行准备动作 |
Bind |
执行实际绑定 |
PostBind |
绑定完成后的处理 |
插件可以实现多个扩展点,但每个扩展点都应保持清晰的职责边界。
2. Filter 与 Score 的形式化模型
设节点集合为:
对一个待调度 Pod ,每个过滤插件 返回节点是否满足约束。定义:
可行节点集合为:
只有 中的节点会进入打分阶段。对某个 Score 插件 ,归一化后的得分记为 ,插件权重为 ,最终总分为:
调度器选择总分最高的节点。如果最高分相同,则使用确定性的 tie-break 规则或随机选择机制,具体行为属于实现细节,插件不能依赖某一个未公开保证的节点顺序。
3. 完整算例
假设有三个节点:
| 节点 | 剩余 CPU | Zone | 是否有污点 |
|---|---|---|---|
n1 |
2 CPU | zone-a |
无 |
n2 |
8 CPU | zone-b |
dedicated=batch:NoSchedule |
n3 |
4 CPU | zone-a |
无 |
Pod 请求 3 CPU,且没有容忍 dedicated=batch 污点。
先执行 Filter:
n1剩余 2 CPU,不满足 3 CPU,拒绝;n2资源满足,但污点没有对应容忍,拒绝;n3资源满足且污点满足,保留。
因此:
此时即使 n1 或 n2 的 Score 更高也没有意义,因为它们已经被过滤掉。Score 只在可行节点上排序,不负责挽救一个已经违反硬约束的节点。
如果 Pod 增加:
tolerations:
- key: dedicated
operator: Equal
value: batch
effect: NoSchedule
则 n2 也可能进入 。随后资源打分插件可能给 n2 和 n3 不同分数,拓扑插件再叠加另一组分数,最终由加权和决定节点。
4. Filter 与 Score 的常见误用
错误做法是把硬约束放入 Score:
不喜欢带污点的节点:打 0 分
这不能保证节点不会被使用。只要所有节点都被打分,某个带污点节点仍可能得到最高分。硬约束必须由 Filter、Admission 校验或其他具有拒绝语义的机制实现。
另一个错误是把高代价查询放在每个节点的 Filter 中。例如每次过滤都访问 API Server、读取远程数据库或执行磁盘扫描。若有 个候选节点,这类操作可能被执行 次,并且处于调度关键路径中。可行的插件通常会:
- 在
PreFilter中为当前 Pod 计算一次不随节点变化的数据; - 在
Filter中只进行轻量节点判断; - 使用 Informer 或调度器缓存读取共享状态;
- 对需要更新的内部状态通过
Reserve和Unreserve管理生命周期。
5. 插件状态和错误回滚
插件可以通过框架提供的 CycleState 在同一个调度周期内共享预计算结果。状态不能假设跨 Pod 持久有效,因为不同 Pod 的约束和缓存视图可能不同。
如果插件在 Reserve 阶段记录了资源预留,而后续 Permit、PreBind 或绑定失败,就必须实现对应的回滚逻辑,例如 Unreserve。否则插件内部可能认为资源已经被占用,实际却没有绑定成功,最终导致“虚假的资源不足”。
Permit 插件尤其容易造成隐性故障:
- 返回等待后,必须有明确的批准路径;
- 等待应有超时;
- 相关 Pod 删除、调度器重启和绑定失败都要清理等待状态;
- 不能把外部系统的不可用无限期转换成调度器线程阻塞。
五、如何编写一个插件
一个简单的 Filter 插件通常实现以下接口:
type FilterPlugin interface {
Plugin
Filter(
context.Context,
*CycleState,
*v1.Pod,
*NodeInfo,
*Status
}
以下示例表达“节点必须带有 workload=trusted 标签”的逻辑:
package trustednode
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
const Name = "TrustedNode"
type Plugin struct{}
var _ framework.FilterPlugin = Plugin{}
func (Plugin) Name() string {
return Name
}
func (Plugin) Filter(
_ context.Context,
_ *framework.CycleState,
_ *v1.Pod,
nodeInfo *framework.NodeInfo,
) *framework.Status {
if nodeInfo == nil || nodeInfo.Node() == nil {
return framework.NewStatus(
framework.Error,
"node information is unavailable",
)
}
if nodeInfo.Node().Labels["workload"] != "trusted" {
return framework.NewStatus(
framework.Unschedulable,
"node does not have workload=trusted",
)
}
return nil
}
这个代码片段可以说明接口和错误语义,但它不是一个可通过 YAML 动态加载的“插件文件”。Kubernetes 调度插件是编译进调度器二进制的 Go 代码,通常需要:
- 使用与目标 Kubernetes 版本匹配的源码和依赖;
- 在调度器初始化时注册插件工厂;
- 将插件名称加入 Profile;
- 构建自定义
kube-scheduler; - 以正确的配置启动它;
- 通过集群测试验证缓存、并发和失败回滚。
调度器内部包依赖较多,直接从独立 Go 模块导入 k8s.io/kubernetes/pkg/scheduler/framework 时,模块版本、replace 规则和构建方式必须与 Kubernetes 源码树匹配。不能把上面的代码复制到普通业务服务后,期待它自动改变集群调度行为。
如果不能维护自定义调度器二进制,可评估调度器 Extender 或外部控制器。但它们的能力不同:
- Extender 通常通过 HTTP 与调度器通信,存在序列化和网络延迟;
- 它不等价于原生 Framework Plugin;
- 外部控制器可以修改标签、污点或 Pod,但会引入异步一致性;
- 通过修改 Pod 来“间接调度”时,必须避免与调度器互相触发更新循环。
六、规模如何影响调度性能
1. 基本复杂度
如果一个调度周期需要检查 个节点,并运行 个 Filter 插件、 个 Score 插件,粗略工作量可以写为:
其中:
- :一次 Filter 的平均成本;
- :一次 Score 的平均成本;
- :过滤后的可行节点集合;
- :队列操作、锁和事件处理成本;
- :读取调度缓存和维护索引的成本;
- :绑定请求、重试和 API Server 交互成本。
这不是 Kubernetes 对所有实现的严格复杂度保证,而是分析瓶颈的模型。实际运行中还会受到 Filter 短路、节点并行度、缓存局部性、插件内部索引和绑定并发影响。
2. percentageOfNodesToScore
调度器可以配置每个 Pod 至少考虑多少比例的节点。示例:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
percentageOfNodesToScore: 20
在节点数为 10,000 时,直觉上 20% 对应约 2,000 个节点,但实际筛选过程还会结合调度器的逐节点扫描和提前停止逻辑,不能简单理解为每次恰好只检查 2,000 个节点。
该参数的基本取舍是:
- 比例较高:更可能找到全局更优节点,但每个 Pod 的筛选和打分成本更高;
- 比例较低:调度吞吐可能提高,但可能错过后续节点中的更优选择;
- 节点异构程度越高,过度降低比例越容易造成分配质量下降;
- 节点数量较少时,该参数的收益通常有限。
例子:假设前 2,000 个节点中只有一个可行节点,但第 2,001 个节点拥有更合适的拓扑位置。如果扫描在达到目标可行节点数量后停止,调度器可能不会看到后者。这不是“错误调度”,因为它仍满足硬约束;但它可能改变成本、均衡性或碎片率。
因此,不能只根据调度延迟调低该值,还要观察:
- 调度失败率;
- 抢占次数;
- 资源碎片;
- 跨 Zone 分布;
- Pod 的最终资源利用率;
- 大规模扩缩容期间的行为。
3. parallelism 的边界
调度框架可以并行执行部分节点级插件工作。并行度提高后,不一定线性提升吞吐,因为还会受到:
- 调度周期的串行边界;
- 插件共享状态的锁;
- CPU 缓存和内存带宽;
- GC;
- API Server 绑定吞吐;
- Cache 更新竞争。
插件必须假设节点级调用可能并发发生,不应写入未加锁的全局 Map,也不应复用可变的共享切片。一个插件在单元测试中串行通过,并不能证明它在实际调度器中线程安全。
七、调度性能的真正瓶颈通常在哪里
1. 节点数不是唯一变量
调度成本不仅由节点数决定,还与下列因素有关:
- Pending Pod 数量;
- 每个 Pod 的 Node Affinity、反亲和、拓扑分布约束;
- Pod 间亲和与反亲和;
- PV/PVC 和存储拓扑;
- 节点污点和容忍;
- 资源请求维度;
- 自定义插件的数据索引;
- 失败后重新入队频率;
- API Server 的写入延迟;
- 集群事件风暴。
例如,1,000 个简单 Pod 在 1,000 个同构节点上,可能比 100 个具有复杂 Pod Anti-Affinity 的 Pod 更容易调度。后者的每个节点判断可能需要读取和统计大量相关 Pod。
2. 缓存不是免费数据库
调度器使用 Informer 和本地 Cache,避免每次调度都从 API Server 查询节点和 Pod。这样降低了网络延迟和 API Server 压力,但代价是:
- Cache 需要处理初始 List;
- 需要持续处理 Watch 事件;
- 需要维护节点、Pod、PV 等索引;
- 大量对象会占用内存;
- 事件突发会造成缓存更新积压。
如果插件绕过 Cache,频繁调用 API Server,可能出现“调度器看起来 CPU 不高,但 API Server 被打满”的反模式。插件访问外部系统也会让调度延迟从本地内存操作变成网络尾延迟。
3. 调度器缓存与资源假设
在调度周期中,调度器会根据缓存中的已绑定 Pod 和当前周期内的假设,判断节点剩余资源。多个调度周期之间,API Server 和其他控制器可能改变对象,因此调度结果不是事务性地锁住整个集群。
一个正确的插件不能把“本次看到节点可用”解释为“绑定一定成功”。绑定失败必须被视为正常错误路径,而不是罕见异常。
八、失败路径:为什么 Pod 会一直 Pending
诊断 Pending Pod 时,应区分不同失败原因。
1. Filter 拒绝所有节点
典型事件可能包含:
- Insufficient CPU 或内存;
- 节点污点没有容忍;
- Node Affinity 不匹配;
- Pod Anti-Affinity 不满足;
- Volume 拓扑不满足;
- 端口已被占用。
此时 Pod 通常进入 UnschedulableQ 或退避队列。只有相关集群状态变化后,才有必要重新调度。
2. 没有匹配的 Profile
如果 Pod 指定了错误的 schedulerName,调度器可能根本不处理它。应检查:
kubectl get pod batch-example -o jsonpath='{.spec.schedulerName}{"\n"}'
kubectl describe pod batch-example
kubectl get pods -n kube-system -l component=kube-scheduler
还要确认调度器启动参数实际加载了哪一个配置文件。不同部署方式可能通过静态 Pod、Deployment、托管控制面或云厂商封装参数启动调度器,不能假设本地文件就是生效配置。
3. 插件等待或绑定失败
如果 Permit 等待没有批准,Pod 可能长期停留在等待状态。此时不能只查看 Filter 失败信息,还应查看:
- 调度器日志中的插件阶段;
- 插件等待对象是否有超时;
- 外部批准者是否运行;
- 调度器是否发生重启;
- 绑定请求是否返回冲突或权限错误。
4. API Server 或 RBAC 问题
调度器需要访问 Node、Pod、相关资源并执行绑定。若 ServiceAccount 缺少权限,可能出现:
- Cache 无法同步;
- 绑定请求被拒绝;
- 调度器反复重试;
- Pod 被选中但没有最终
spec.nodeName。
应查看调度器日志、事件和审计日志,而不能把所有 Pending 都归因于“节点不够”。
九、测试:从插件单测到集群级压测
性能优化必须建立在正确性测试之上。调度器测试至少分为四层。
1. 插件单元测试
单元测试验证插件本身的规则,例如:
- 带
workload=trusted的节点通过; - 没有该标签的节点返回
Unschedulable; - 缺失 NodeInfo 返回错误;
- Pod 删除或绑定失败后状态可以回滚;
- 并发调用不会产生数据竞争。
示意测试结构:
func TestFilter(t *testing.T) {
tests := []struct {
name string
labels map[string]string
wantStatus framework.Code
}{
{
name: "trusted node",
labels: map[string]string{"workload": "trusted"},
wantStatus: framework.Success,
},
{
name: "untrusted node",
labels: map[string]string{"workload": "general"},
wantStatus: framework.Unschedulable,
},
}
// 使用目标 Kubernetes 版本提供的 NodeInfo 和 Status 构造方式,
// 为每个 case 创建 NodeInfo,调用 Plugin.Filter 并断言状态。
}
实际代码应使用与目标版本一致的 framework.NodeInfo 构造 API。调度框架内部类型在版本间可能有小幅变化,因此测试代码应和自定义调度器源码一起锁定版本。
运行 Go 测试时,至少使用:
go test -race ./...
-race 可以发现插件全局状态的并发读写问题,但它不能证明插件逻辑正确,也不能模拟真实 API Server 延迟。
2. Framework 集成测试
集成测试应验证插件与其他扩展点的组合:
- 创建多个 Node;
- 创建具有不同标签、污点和资源的 Pod;
- 启动加载该 Profile 的调度器;
- 等待 Pod 被绑定;
- 检查
spec.nodeName; - 删除节点或修改标签;
- 检查 Pending Pod 是否重新入队;
- 注入绑定失败,检查
Unreserve是否执行。
尤其要测试“成功前的每一个失败点”:
Filter 失败
-> UnschedulableQ
-> Node 事件
-> ActiveQ
-> Reserve 成功
-> Permit 超时
-> Unreserve
-> BackoffQ
如果只测试最终成功路径,最容易漏掉资源预留泄漏和等待状态泄漏。
3. 端到端测试
端到端测试在真实 API Server、Informer、Scheduler Cache 和控制器环境中验证行为。一个基本场景可以是:
kubectl apply -f trusted-node.yaml
kubectl apply -f batch-example.yaml
kubectl get pod batch-example -o wide
kubectl describe pod batch-example
预期结果是:
- Pod 的
schedulerName与 Profile 名称一致; - 节点具有
workload=trusted标签; - Pod 最终进入 Running 或完成绑定;
spec.nodeName被写入;- 事件没有持续的调度失败信息。
然后移除标签:
kubectl label node <node-name> workload-
kubectl delete pod batch-example
kubectl apply -f batch-example.yaml
kubectl describe pod batch-example
预期是 Pod 不应被绑定到不满足条件的节点。这个测试同时验证了缓存事件、过滤逻辑和最终绑定结果。
4. 性能和容量测试
性能测试必须固定变量,否则结果无法解释。至少记录:
- 节点数量和节点类型;
- Pending Pod 数量;
- Pod 资源请求;
- Affinity、Anti-Affinity、拓扑约束;
- Profile 和插件集合;
- 调度器 CPU、内存和 GC;
- API Server 请求延迟和限流;
- 从 Pod 创建到绑定的端到端延迟;
- 单次调度周期耗时;
- 调度失败和重试次数;
- ActiveQ、BackoffQ、UnschedulableQ 的变化。
可将测试目标分为:
同时观察延迟分位数,例如 P50、P95 和 P99。只看平均值会掩盖少量 Pod 因复杂约束或 API Server 尾延迟而长期 Pending 的问题。
测试应至少包含三类负载:
- 容易成功:大多数节点满足约束,观察峰值吞吐;
- 大量失败:没有可行节点,观察队列退避和事件风暴;
- 动态变化:节点标签、污点、资源和 Pod 持续变化,观察重新入队正确性。
规模测试不能简单地用“节点数乘以 Pod 数”外推生产结果,因为不同插件的代价函数不同。应使用与生产约束分布相近的工作负载。
十、监控和诊断方法
调度器通常暴露 Prometheus 指标,包括调度延迟、调度尝试、Pending Pod、插件执行耗时和队列状态等类别。指标名称和标签会随版本变化,建议先查看实际端点:
kubectl -n kube-system port-forward pod/<scheduler-pod> 10259:10259
curl -k https://127.0.0.1:10259/metrics
前提是调度器安全端口可访问,并且本地证书和认证方式允许该请求。托管 Kubernetes 可能不允许直接访问控制面组件,此时应使用云厂商提供的监控入口。
诊断时应按阶段拆分:
Pod 创建到进入队列
-> 队列等待时间
-> Filter/Score 执行时间
-> Permit 等待时间
-> Bind 请求时间
-> API Server 观察到绑定的时间
如果总延迟高但插件执行时间低,问题可能在队列或 API Server;如果某个插件 P99 明显高于其他插件,应检查它是否:
- 在每个节点重复执行昂贵操作;
- 访问远程服务;
- 持有全局锁;
- 产生大量临时对象;
- 在失败 Pod 上反复计算相同结果。
日志级别调高有助于定位具体失败节点和插件,但高并发集群中详细日志可能造成自身 I/O 瓶颈。生产环境应在短时间诊断窗口内启用,并监控日志量。
十一、常见误解和生产取舍
1. “分数最高的节点就是最优节点”
不是。首先必须通过所有硬约束;Score 只是在可行节点中表达软偏好。一个拥有更高分数的节点,如果 Filter 已拒绝,就不会被选择。
2. “Profile 等于独立调度器”
不是。Profile 共享进程和队列。要获得独立故障域和资源边界,应使用独立调度器实例,但这会增加路由、权限、监控和运维成本。
3. “降低扫描节点比例一定能提升性能”
不一定。它可能减少单 Pod 的计算量,但也可能降低调度质量,造成更多资源碎片、抢占或后续迁移。必须同时观察吞吐、延迟和放置质量。
4. “自定义插件只要返回正确结果即可”
不完整。插件还必须处理:
- 并发调用;
- 缓存陈旧;
- 失败回滚;
- 事件驱动重试;
- API Server 错误;
- 调度器重启;
- 内存和 CPU 上界。
调度器插件是控制面关键路径代码,不应把不可控的外部 RPC 直接放入每个节点的 Filter 或 Score。
5. “Extender 和 Framework Plugin 可以互换”
不能简单互换。Framework Plugin 运行在调度器内部,能参与多个生命周期阶段并使用框架状态;Extender 通常需要网络通信,接口和可表达的生命周期不同。选择哪一种取决于扩展需求、延迟预算、升级方式和故障隔离要求。
十二、一个可执行的验证顺序
在生产启用自定义调度策略前,可以按以下顺序验证:
- 使用内置插件和固定 Profile 建立基线;
- 在小规模集群中验证 Pod 能否按
schedulerName路由; - 对每个 Filter 失败原因编写单元测试;
- 对
Reserve、Permit、PreBind失败编写回滚测试; - 使用
go test -race检查并发访问; - 在集成环境验证节点事件能否唤醒不可调度 Pod;
- 执行成功、失败和动态变化三类容量测试;
- 比较调度延迟、队列长度、绑定延迟和资源放置质量;
- 逐步扩大节点和 Pod 数量;
- 保留回滚到内置 Profile 或旧调度器二进制的路径。
调度性能优化的核心不是让某一个函数更快,而是减少关键路径上的无效工作:让队列只唤醒真正可能成功的 Pod,让 Filter 使用本地索引而不是远程查询,让 Score 只处理可行节点,让插件状态在失败时完整回滚,并用端到端指标证明吞吐提升没有换来错误放置和长期 Pending。
Kubernetes 官方的扩展模型、调度框架接口和 client-go 组件文档分别说明了扩展点、控制器客户端和 Informer/缓存等基础能力。实现自定义调度器或调度插件时,应以运行版本对应的官方 API 定义为准,尤其要核对配置版本、默认插件集合、指标名称和实验性字段。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes API Aggregation:Extension API Server、发现、认证和可用性
- 延伸:Kubernetes 调度器:Queue、Filter、Score、Bind、插件和扩展
- 延伸:Kubernetes Controller 开发:client-go、Cache、Queue、Reconcile 和幂等
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论