Kubernetes 基础体系 · 第 10/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes 调度器:Queue、Filter、Score、Bind、插件和扩展
Kubernetes 调度器(kube-scheduler)负责为尚未绑定节点的 Pod 选择一个合适的节点,并把这个选择写回 Kubernetes API。它解决的不是“启动容器”,而是一个带约束的资源分配问题:
在当前集群状态下,为待调度 Pod 找到一个满足硬约束、并且在软约束下尽可能合适的节点,然后完成绑定。
调度器不负责创建 Pod 的容器。节点上的 kubelet 观察到 Pod 已经绑定到自己的节点后,才会负责拉取镜像、创建 Pod Sandbox 和容器。
本文使用的术语主要来自 Kubernetes Scheduling Framework 和稳定的 kube-scheduler 配置 API。具体插件集合、调度配置字段和实验性能力可能随 Kubernetes 版本变化;生产环境应以所部署版本的官方 API 文档和组件配置为准。
一、先建立全局模型:Pod 如何从等待变成已绑定
一个 Pod 的典型调度路径如下:
flowchart LR
A[Pod 创建<br/>spec.nodeName 为空] --> B[Scheduling Queue]
B --> C[Scheduling Cycle]
C --> D[PreFilter]
D --> E[Filter<br/>逐节点判断]
E -->|没有可行节点| F[失败记录/可能触发抢占]
F --> B
E -->|得到 feasible nodes| G[Score<br/>逐节点打分]
G --> H[Reserve/Permit]
H --> I[PreBind]
I --> J[Bind<br/>写入 Pod 绑定关系]
J --> K[PostBind]
K --> L[kubelet 在目标节点运行 Pod]
这张图需要区分两种“状态”:
- 调度器内部状态:队列、调度缓存、假设中的节点资源占用。
- API Server 中的实际状态:Pod 的
spec.nodeName、Binding 请求、Node 和其他对象的最新数据。
调度器为了性能不会在每次调度前都从 API Server 完整读取全部对象。它通过 Informer 监听对象变化,维护本地缓存;调度过程中又会暂时“假设”某个 Pod 已经占用目标节点资源。真正绑定时,如果 API Server 或其他调度周期已经改变了集群状态,绑定可能失败或后续 kubelet 可能无法正常运行该 Pod。
一个更形式化的表达是:
给定待调度 Pod 和候选节点集合 ,调度器先计算可行集合:
如果 为空,当前调度周期不能直接绑定。若 非空,则对每个可行节点计算软约束得分:
其中:
- 是第 个 Score 插件对节点的归一化得分,通常为 0 到 100;
- 是该插件的权重;
- 是启用的 Score 插件数量。
最终选择得分最高的节点。得分相同并不表示存在逻辑错误,调度器会使用内部的打破平局逻辑选择其中一个节点。
二、Queue:调度器为什么不是看到 Pod 就立即调度
2.1 Scheduling Queue 的职责
调度队列保存“当前需要尝试调度”的 Pod。一个 Pod 通常在以下情况下进入队列:
- Pod 创建时没有设置
spec.nodeName; - Pod 因资源不足、亲和性冲突等原因调度失败后被重新排队;
- Node、Pod、PVC、StorageClass、Taint 等相关对象发生变化,可能使它重新变得可调度;
- 发生抢占或调度器内部状态变化,需要再次尝试。
队列不是简单的先进先出队列。默认调度器会根据 Pod 优先级等信息决定取出顺序。高优先级 Pod 通常会先于低优先级 Pod 尝试调度,但这不代表高优先级 Pod 一定可以抢占已经运行的 Pod,也不代表它可以绕过所有硬约束。
Pod 的优先级来自 PriorityClass。例如:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-high
value: 100000
globalDefault: false
description: "示例:较高优先级的批处理 Pod"
---
apiVersion: v1
kind: Pod
metadata:
name: batch-job
spec:
priorityClassName: batch-high
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
创建后可以检查 Pod 的优先级:
kubectl apply -f priority-pod.yaml
kubectl get pod batch-job -o jsonpath='{.spec.priorityClassName}{"\n"}'
kubectl get pod batch-job -o wide
如果没有足够资源,Pod 可能长期处于 Pending。这时 kubectl describe pod batch-job 的 Events 通常会显示类似:
0/3 nodes are available: 3 Insufficient cpu.
具体文字会因插件、版本和集群状态不同而变化,不能把事件文本当作稳定 API。
2.2 三类队列状态
调度器内部通常将 Pod 分为三类队列状态:
- ActiveQ:当前可以被调度循环取出的 Pod;
- BackoffQ:刚刚调度失败,经过短暂退避后再尝试;
- UnschedulableQ:当前没有明显机会成功,需要等待相关集群事件或周期性重新检查。
这样设计的原因是避免失败 Pod 忙等并反复占用调度器 CPU。例如,一个 Pod 因为所有节点都缺少 2 GiB 内存而失败,如果集群没有任何资源变化,让它每毫秒重试一次没有意义。
当 Node 增加资源、节点标签变化、Pod 被删除释放资源,或相关对象发生变化时,调度器会尝试把受影响的 Pod 从不可调度状态重新移回可调度队列。较新的调度器还支持基于事件的 Queueing Hint,以减少无关事件导致的重复重试;这一能力的默认行为和可用扩展点需要结合具体 Kubernetes 版本确认。
2.3 队列顺序不等于最终节点选择
Queue 只决定“先调度哪个 Pod”,不决定“这个 Pod 使用哪个节点”。
例如:
- Pod A 优先级高,但所有节点都没有足够内存;
- Pod B 优先级低,但有一个节点满足它的约束。
Pod A 可能先被取出,但仍然会调度失败;随后 Pod B 可能成功。优先级影响调度顺序和抢占候选,不能把不满足条件的节点变成可用节点。
三、Filter:硬约束如何把节点变成可行集合
3.1 Filter 是合取条件
Filter 阶段针对候选节点逐个判断。一个节点只有通过所有启用的 Filter 插件,才会进入可行节点集合。
如果有三个硬约束:
- 节点必须有
disktype=ssd; - 节点不能带有未被 Pod 容忍的
dedicated=gpu:NoSchedule污点; - 节点必须有足够的 CPU 和内存;
那么节点可行的条件是:
任何一项为假,节点就会被排除。Filter 不是加权投票:一个节点即使 CPU 资源非常充足,只要不满足必需的节点亲和性,也不能靠“高分”挽救。
3.2 常见内置 Filter 插件与 API 约束的关系
常见内置插件包括:
NodeUnschedulable:排除标记为不可调度的节点;NodeName:处理spec.nodeName约束;NodeAffinity:处理nodeSelector和节点亲和性;TaintToleration:检查污点和容忍;NodePorts:检查主机端口冲突;NodeResourcesFit:检查 CPU、内存、扩展资源等资源请求;VolumeBinding:结合 PVC、PV 和拓扑完成卷绑定约束;InterPodAffinity:处理 Pod 间亲和性和反亲和性;PodTopologySpread:处理拓扑域分布约束。
例如,下面的 Pod 要求节点同时满足标签和容忍条件:
apiVersion: v1
kind: Pod
metadata:
name: ssd-workload
spec:
nodeSelector:
disktype: ssd
tolerations:
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
containers:
- name: app
image: nginx:1.27
resources:
requests:
cpu: "500m"
memory: "256Mi"
对应节点至少需要:
kubectl label node worker-1 disktype=ssd
kubectl taint node worker-1 dedicated=gpu:NoSchedule
此时:
nodeSelector由节点标签匹配插件处理;tolerations只允许该 Pod 忽略指定污点;- 容忍污点并不会自动让 Pod 获得 GPU,也不会增加 CPU 或内存;
- 如果 Pod 没有声明足够的资源请求,
NodeResourcesFit可能依据较小的请求判断它可放置,但运行时资源消耗仍可能超过请求,造成资源争用。
3.3 Filter 的完整算例
假设有三个节点:
| 节点 | 标签 | 污点 | 可分配 CPU | 可分配内存 |
|---|---|---|---|---|
| n1 | zone=a, disktype=ssd |
无 | 2 | 4 GiB |
| n2 | zone=b, disktype=hdd |
无 | 8 | 16 GiB |
| n3 | zone=a, disktype=ssd |
dedicated=gpu:NoSchedule |
8 | 16 GiB |
Pod 的约束:
nodeSelector:
disktype: ssd
resources:
requests:
cpu: "4"
memory: "8Gi"
且没有容忍 dedicated=gpu。
逐步判断:
n1的disktype=ssd满足,但 CPU 2 小于请求 4,Filter 失败;n2的 CPU 和内存满足,但标签是hdd,Filter 失败;n3的标签和资源满足,但存在未容忍的NoSchedule污点,Filter 失败。
因此:
Score 阶段根本不会运行,因为没有可评分的节点。
一个常见误解是“给节点加上高分标签可以解决调度失败”。不能。标签通常只是让某些节点进入或离开可行集合;如果所有节点都被硬约束排除,Score 没有输入。
3.4 全量检查与提前结束
调度器可以配置 percentageOfNodesToScore,允许在找到足够多的可行节点后停止继续扫描。它的目的在大规模集群中减少一次调度周期检查的节点数量。
这会带来一个重要边界:
- Filter 逻辑仍然必须正确;
- 但一次调度周期可能只检查部分节点;
- Score 只在本次找到的可行节点上运行;
- 如果可行节点分布不均,提前结束可能影响最终选择质量。
因此,这个参数是性能与搜索完整性的取舍,不是改变硬约束语义的开关。小型集群通常不必为了追求极限性能而随意降低它;大规模集群需要结合调度延迟、节点分布和工作负载测试。
四、Score:可行节点之间如何比较
4.1 Score 只处理软偏好
Filter 得到可行节点后,Score 插件对这些节点打分。例如:
- 更倾向于把 Pod 放到剩余资源较多的节点;
- 更倾向于使用已经存在镜像的节点;
- 更倾向于满足 preferred node affinity;
- 更倾向于让副本分散到不同可用区;
- 更倾向于减少 Pod 之间的冲突。
Score 不应该承担必须满足的条件。若某节点“绝不能使用”,应该使用硬约束,例如 requiredDuringSchedulingIgnoredDuringExecution、污点或资源限制,而不是给该节点一个低分。
4.2 单个插件的归一化与权重
不同插件的原始计算方式不同,因此框架会将插件得分转换到统一范围,通常是 0 到 100。然后乘以权重:
例如两个可行节点:
| 节点 | 资源插件得分 | 拓扑插件得分 |
|---|---|---|
| n1 | 90 | 20 |
| n2 | 60 | 80 |
若两个插件权重都是 1:
n1 = 90 + 20 = 110n2 = 60 + 80 = 140
选择 n2。
若资源插件权重为 3,拓扑插件权重为 1:
n1 = 3×90 + 20 = 290n2 = 3×60 + 80 = 260
选择变成 n1。这说明权重不是装饰性配置,它改变了偏好之间的相对优先级。
但这仍然不能违反 Filter 的硬约束。若 n1 不满足必要污点容忍条件,它不会进入上述计算。
4.3 NodeResourcesFit 的打分策略
NodeResourcesFit 不仅可以做资源可行性检查,也可以参与打分。常见资源打分策略包括:
LeastAllocated:倾向于选择剩余资源较多的节点;MostAllocated:倾向于把工作负载集中到已有负载较高的节点;RequestedToCapacityRatio:根据请求量与容量的比例使用自定义函数。
一个调度配置示例:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: resource-aware
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 2
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
这里的含义是:
NodeResourcesFit仍然可以参与资源 Filter;- 在 Score 阶段使用
MostAllocated; - 该插件最终得分乘以权重 2;
- 这只是调度器配置片段,必须由 kube-scheduler 进程加载,不能通过创建一个普通 Pod 动态改变集群调度器。
MostAllocated 可能提高节点利用率,但也可能让少数节点更快耗尽,降低故障域冗余。LeastAllocated 可能改善资源分散,却增加跨节点扩散和镜像拉取成本。没有脱离工作负载和集群拓扑的普遍最优策略。
4.4 Spread 不是绝对平均
Pod Topology Spread 约束通常表达的是“尽量均匀”或“不能超过最大偏差”,而不是保证每个拓扑域恰好拥有相同数量的 Pod。
例如三个可用区当前副本数为:
再调度一个副本时,如果 maxSkew=1 且约束为硬约束,目标通常只能选择当前数量为 1 的区域,使结果成为:
不能选择数量为 3 的区域,因为偏差会继续增大。若约束是软约束,插件可能只对更均匀的区域给高分,但在资源或其他高权重偏好作用下仍可能选择次优位置。
拓扑分布还依赖节点是否正确标注拓扑标签、哪些节点被纳入统计,以及 nodeAffinityPolicy、nodeTaintsPolicy 等版本相关配置。云厂商提供的区域和故障域标签也可能存在命名或语义差异,不能假设所有集群完全一致。
五、Bind:选择节点不等于绑定完成
5.1 Bind 的实际动作
当调度器完成节点选择后,默认绑定插件会通过 Kubernetes API 写入 Pod 与节点的绑定关系。概念上等价于设置:
spec:
nodeName: worker-2
实际实现通常使用 Pod 的 binding 子资源,而不是简单地由客户端直接更新整个 Pod 对象。API Server 接受绑定后,kubelet 通过监听 Pod 变化发现它属于自己的节点。
绑定成功后,可以观察:
kubectl get pod ssd-workload -o wide
kubectl get pod ssd-workload -o jsonpath='{.spec.nodeName}{"\n"}'
spec.nodeName 为空表示尚未绑定;非空表示调度器已经完成节点绑定,但不表示容器一定已经启动。还需要检查:
kubectl get pod ssd-workload
kubectl describe pod ssd-workload
kubectl get events --sort-by=.lastTimestamp
5.2 Reserve、Permit 和 Bind 之间的保护
在真正 Bind 前,调度框架还可能经过以下扩展点:
- Reserve:在调度器内部预留资源或记录状态;
- Permit:允许插件暂缓绑定,例如等待一组 Pod 一起决定;
- PreBind:绑定前执行额外检查或准备;
- Bind:执行实际绑定;
- PostBind:绑定成功后执行通知或清理。
“假设”是调度器避免并发冲突的重要机制。假设某个 Pod 要放到节点 n1 后,后续调度周期会把它视为已经占用对应资源,即使 API Server 中的绑定请求尚未完成。
如果后续阶段失败,框架需要调用相应的取消或清理逻辑,例如:
- Reserve 成功但 PreBind 失败,应执行 Unreserve;
- Permit 等待超时,应释放相关状态;
- Bind API 请求失败,应重新入队或按错误类型处理。
一个插件如果只修改了外部系统,却没有在失败路径中撤销修改,就可能产生“调度器认为资源已释放、外部系统认为资源仍被占用”的状态泄漏。
5.3 并发和陈旧信息
调度器本地缓存可能与 API Server 存在短暂延迟,多个调度周期也可能交错运行。调度器通过缓存、假设状态和 API Server 的最终写入来降低冲突,但插件不能把本地观察结果当作强一致锁。
例如:
- 调度器根据缓存认为节点
n1还有 4 CPU; - 调度 Pod A,假设它占用 3 CPU;
- 另一个调度路径或控制器同时改变了相关对象;
- Pod A 的绑定或后续启动可能失败;
- 调度器重新观察状态并重试,或把问题交给 kubelet、控制器和事件系统处理。
这也是为什么自定义插件不能只在内存中做一个永久资源分配表。该表必须能够处理重启、绑定失败、对象删除和事件延迟。
六、插件:调度器框架把流程拆成了哪些扩展点
Scheduling Framework 将调度流程暴露为插件扩展点。常见生命周期如下:
| 扩展点 | 作用 |
|---|---|
QueueSort |
决定队列中 Pod 的排序 |
PreEnqueue |
Pod 入队前判断是否允许进入调度队列 |
PreFilter |
在遍历节点前计算或校验 Pod 级状态 |
Filter |
判断单个节点是否满足硬约束 |
PostFilter |
没有可行节点时处理,例如抢占 |
PreScore |
评分前准备共享数据 |
Score |
对单个可行节点打分 |
NormalizeScore |
将同一插件的得分归一化 |
Reserve |
在调度器内部预留资源或状态 |
Permit |
暂停、拒绝或允许继续绑定 |
PreBind |
绑定前的检查或外部准备 |
Bind |
执行绑定 |
PostBind |
绑定成功后的处理 |
Unreserve |
后续失败时撤销 Reserve |
PreFilter 和 PreScore 的价值在于共享计算结果。假设 Pod 的反亲和性规则很复杂,如果每个节点都重新解析一次规则,成本会很高;插件可以在 PreFilter 中先计算 Pod 级状态,再由 Filter 快速判断每个节点。
6.1 Filter 插件的伪代码模型
一个 Filter 插件的逻辑类似:
func (p *Plugin) Filter(
ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeInfo *framework.NodeInfo,
) *framework.Status {
if !matchesLabels(pod, nodeInfo.Node().Labels) {
return framework.NewStatus(
framework.Unschedulable,
"required node labels are not matched",
)
}
if !fitsResources(pod, nodeInfo) {
return framework.NewStatus(
framework.Unschedulable,
"requested resources do not fit",
)
}
return nil // Success
}
真实插件还要处理:
- 节点对象为空或缓存信息不完整;
- Pod 使用扩展资源;
- PVC 和拓扑绑定;
- 预留状态;
- 插件内部错误与普通“不适合”之间的区别。
Unschedulable 表示这个节点不适合,但可能在未来重试;Error 表示插件执行本身发生异常,通常需要更谨慎地处理和观测。不能把所有异常都伪装成普通不适合,否则会掩盖插件故障。
6.2 Score 插件的伪代码模型
func (p *Plugin) Score(
ctx context.Context,
state *framework.CycleState,
pod *v1.Pod,
nodeName string,
) (int64, *framework.Status) {
nodeInfo, err := p.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
if err != nil {
return 0, framework.AsStatus(err)
}
score := calculatePreference(pod, nodeInfo)
return score, nil
}
插件应返回框架要求范围内的分数,并正确实现归一化。若插件依赖外部系统,例如 IPAM、机架库存或 GPU 编排服务,还必须定义超时、不可达和重试行为。一个阻塞整个 Score 阶段的外部 HTTP 请求,会把外部系统延迟放大为所有 Pod 的调度延迟。
6.3 插件如何获得集群信息
插件通常通过 Framework Handle 获取:
- SharedInformer 相关缓存;
- 调度器快照;
- Kubernetes Client;
- EventRecorder;
- Framework 配置和周期状态。
插件不应在每次 Filter 调用中直接向 API Server 发请求查询节点。因为一次调度周期可能对大量节点调用 Filter,这会形成 API Server 请求风暴,并且使调度结果受网络延迟影响。
七、Profile:一套调度器如何提供多种调度策略
7.1 Profile 与 schedulerName
一个 kube-scheduler 进程可以配置多个 Scheduling Profile。Pod 通过 spec.schedulerName 选择 Profile:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
- schedulerName: resource-aware
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 2
pluginConfig:
- name: NodeResourcesFit
args:
scoringStrategy:
type: MostAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
使用第二个 Profile 的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: profile-demo
spec:
schedulerName: resource-aware
containers:
- name: app
image: nginx:1.27
resources:
requests:
cpu: "100m"
memory: "128Mi"
检查:
kubectl apply -f scheduler-config.yaml
kubectl apply -f profile-demo.yaml
kubectl get pod profile-demo -o jsonpath='{.spec.schedulerName}{"\n"}'
kubectl get pod profile-demo -o wide
但 scheduler-config.yaml 不会被 kubectl apply 自动应用到正在运行的 kube-scheduler。实际部署时必须让 kube-scheduler 进程以该配置文件启动,或修改静态 Pod、控制平面部署或托管平台提供的调度器配置入口。不同发行版的启动方式差异很大。
7.2 多 Profile 的限制
多 Profile 并不是多个完全隔离的调度器:
- 它们通常共享同一个调度器进程及其缓存;
- Pod 仍然写入同一个集群状态;
- 一个 Profile 的策略不能保证其他 Profile 不把 Pod 放到相同节点;
QueueSort对队列有全局约束,多个 Profile 不能任意定义互相冲突的队列排序逻辑;- 一个 Pod 只能通过一个
schedulerName选择一个 Profile。
如果真正需要独立的调度队列、独立故障域或独立生命周期,可以运行多个 scheduler 实例,但这会增加 leader election、资源竞争、监控和故障排查复杂度。多调度器还必须明确各自处理哪些 schedulerName,避免配置错误导致 Pod 永远无人调度。
八、扩展方式:内置插件、自定义插件和 Extender
8.1 修改配置不等于写代码
内置插件可以通过 KubeSchedulerConfiguration 启用、禁用和配置。它们随 kube-scheduler 二进制发布,通常具有较好的版本兼容性。
如果需要新的调度逻辑,主要有两种路线:
-
Scheduling Framework 插件
使用 Go 实现,编译进 kube-scheduler 或基于带插件的调度器二进制。它可以接入 Filter、Score、Reserve、Bind 等生命周期,功能最完整、延迟也通常更可控。 -
Scheduler Extender
通过 HTTP 与外部服务交互。它不需要把代码编译进调度器,但边界较老,适合已有 extender 集成或某些外部资源决策场景。新设计通常应优先评估 Framework 插件。
Kubernetes 没有一个可以把任意二进制“热插入”正在运行的官方动态调度插件机制。所谓“安装调度插件”,通常意味着更换或构建调度器镜像、修改启动参数,并进行版本测试。
8.2 Extender 的基本模型
Extender 由 kube-scheduler 调用外部 HTTP 服务,传统上可以参与:
- 过滤节点;
- 给节点打分;
- 提供抢占相关决策;
- 绑定动作。
一个概念性的配置片段如下:
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
extenders:
- urlPrefix: "http://scheduler-extender.example.svc:8080"
filterVerb: "filter"
prioritizeVerb: "prioritize"
weight: 5
nodeCacheCapable: true
ignorable: false
字段是否可用、请求格式和具体版本行为必须以目标 Kubernetes 版本为准。Extender 的关键风险是:
- 调度器依赖外部网络服务;
- HTTP 延迟直接进入调度路径;
- 外部服务不可达时,
ignorable决定是忽略还是影响调度; - 失败重试和超时可能造成大量 Pending Pod;
- 外部服务看到的集群状态也可能陈旧;
- Extender 与 Framework 插件同时启用时,约束和得分叠加关系需要实际验证。
ignorable: true 并不等于“故障无影响”。它可能让调度器在外部策略失效时继续放置 Pod,从而绕过原本依赖 Extender 的资源安全约束。
九、没有可行节点时:调度失败与抢占
Filter 结束后没有可行节点时,调度器会记录失败原因并让 Pod 保持 Pending。常见原因包括:
Insufficient cpu或Insufficient memory;- 节点标签不满足;
- 未容忍污点;
- Pod 间反亲和性冲突;
- PVC 无法绑定到满足拓扑条件的 PV;
- 拓扑分布硬约束无法满足。
如果 Pod 优先级允许,调度器可能进入 PostFilter 的抢占流程。抢占的思路是:
- 在候选节点上寻找可以被低优先级 Pod 让出的受害者;
- 模拟删除这些受害者后,判断待调度 Pod 是否满足约束;
- 选择代价较小的节点和受害者集合;
- 设置相关状态并删除受害者;
- 等待资源真正释放后重新调度高优先级 Pod。
抢占不是强制成功机制。以下情况仍可能抢占失败:
- 受害者被删除后,Pod 亲和性、反亲和性或拓扑约束仍不满足;
- 资源释放不够;
- Pod 有
preemptionPolicy: Never; - 其他调度周期先占用了资源;
- PDB、控制器重建或拓扑条件使候选方案无效。
抢占还可能导致低优先级工作负载被驱逐、重新创建,并增加业务抖动。因此优先级设计应反映真实业务等级,而不是简单给所有关键服务设置极高数值。
十、一个可验证的调度约束实验
下面的实验不需要修改 scheduler 配置,可以直接观察 Queue、Filter 和 Bind 的结果。
先查看节点标签和资源:
kubectl get nodes --show-labels
kubectl describe node <node-name>
给一个节点添加标签:
kubectl label node <node-name> topology-demo=enabled
创建 Pod:
apiVersion: v1
kind: Pod
metadata:
name: scheduling-lifecycle-demo
spec:
nodeSelector:
topology-demo: enabled
containers:
- name: nginx
image: nginx:1.27
resources:
requests:
cpu: "100m"
memory: "128Mi"
执行:
kubectl apply -f scheduling-lifecycle-demo.yaml
kubectl get pod scheduling-lifecycle-demo -w
kubectl describe pod scheduling-lifecycle-demo
kubectl get pod scheduling-lifecycle-demo \
-o jsonpath='{.spec.nodeName}{"\n"}'
预期过程是:
- Pod 创建后
spec.nodeName为空,进入调度队列; - Filter 排除没有
topology-demo=enabled的节点; - 如果至少有一个节点满足资源等其他条件,Score 在这些节点中进行选择;
- Bind 成功后
spec.nodeName出现目标节点名称; - kubelet 开始创建容器。
删除标签后再创建同样的 Pod:
kubectl label node <node-name> topology-demo-
kubectl delete pod scheduling-lifecycle-demo
kubectl apply -f scheduling-lifecycle-demo.yaml
kubectl describe pod scheduling-lifecycle-demo
此时可能看到:
0/N nodes are available: N node(s) didn't match Pod's node affinity/selector.
该 Pod 不会因为等待时间变长而自动放宽 nodeSelector。只有节点标签变化、约束变化或其他相关事件使条件重新满足,它才可能成功;否则会一直 Pending。
十一、诊断:从 Pending 事件回到调度阶段
11.1 先确认 Pod 是否真的需要调度
kubectl get pod <pod> -o jsonpath='
schedulerName={.spec.schedulerName}
nodeName={.spec.nodeName}
phase={.status.phase}{"\n"}'
以下情况不应归因于调度器:
spec.nodeName已经被手动设置;- Pod 已绑定节点但容器处于
ImagePullBackOff; - Pod 因 CNI、挂载、探针或应用进程失败而未 Ready;
- Pod 使用了不存在的
schedulerName,导致没有匹配的 Profile 处理它。
11.2 查看调度事件和节点事实
kubectl describe pod <pod>
kubectl get events --field-selector involvedObject.name=<pod> \
--sort-by=.lastTimestamp
kubectl get nodes
kubectl describe node <node>
重点对照:
- Pod 的 CPU、内存和扩展资源
requests; - Node 的
allocatable,而不是只看物理内存; nodeSelector、required/preferred affinity;- 污点和容忍;
- PVC、PV 与 StorageClass 的拓扑;
- 现有 Pod 的反亲和性和拓扑分布;
- 节点是否
SchedulingDisabled。
11.3 判断是硬约束失败还是软偏好结果
如果事件显示所有节点都因为同一硬约束失败,应修改标签、污点、资源或 Pod 约束,而不是调整 Score 权重。
如果多个节点都可行,但 Pod 总是去某一类节点,才适合检查:
- Profile 是否正确;
- Score 插件是否启用;
- 插件权重;
MostAllocated与LeastAllocated等策略;- 拓扑分布约束是否为硬约束或软约束;
- 是否启用了
percentageOfNodesToScore; - 多个 scheduler 或 Profile 是否共同处理 Pod。
生产环境还应观察 kube-scheduler 的调度延迟、队列长度、失败原因和插件耗时指标。不能只看 Pod 最终落在哪个节点,因为最终位置无法单独证明每个插件的判断过程。
十二、生产边界与常见误解
12.1 requests 决定调度视图,实际使用可能不同
NodeResourcesFit 主要依据 Pod 的资源请求与节点可分配资源判断。容器实际使用量可能高于请求,也可能长期低于请求。
因此:
- 请求过低,调度器会高估可用容量;
- 请求过高,Pod 可能长时间 Pending;
- Limit 不等于调度器可用容量;
- 内存超用可能导致 OOM,CPU 超用可能导致 throttling,但这些不是 Score 阶段自动修正的结果。
12.2 Taint 和 Toleration 不是双向绑定
Taint 表示节点排斥某类 Pod,Toleration 表示 Pod 可以接受该排斥。容忍污点并不表示调度器偏好该节点,也不表示该 Pod 拥有节点上的专用硬件。
对于 GPU 等资源,通常还需要:
- 节点扩展资源;
- Pod 的资源请求;
- 设备插件;
- 可能的节点标签、污点和容忍。
12.3 调度成功不等于业务可用
Bind 成功只说明 Pod 已分配节点。后续仍可能因为以下原因失败:
- 镜像不存在或无法拉取;
- CNI 无法配置网络;
- CSI 无法挂载卷;
- 容器启动命令错误;
- 节点 kubelet 异常;
- 应用探针失败。
调度器负责的是放置决策,不是整个 Pod 生命周期。
12.4 nodeName 会绕过调度器
直接设置:
spec:
nodeName: worker-1
会让 Pod 被指定到节点,通常不经过正常调度选择。这样可能绕过资源选择、亲和性、污点策略和自定义插件,除非有明确的底层控制器或特殊工作负载需求,否则不应把它当作普通调度约束替代品。
12.5 自定义调度策略必须测试失败路径
调度插件至少要测试:
- 没有可行节点;
- 节点对象变化;
- Pod 删除和重建;
- Reserve 成功后 Bind 失败;
- 外部依赖超时;
- 调度器重启;
- 多个调度周期并发;
- 节点数量和 Pod 数量扩大后的耗时;
- 版本升级后的框架接口和默认插件变化。
尤其是 Bind、Reserve 和外部系统整合。只验证“正常时能把 Pod 放到目标节点”远远不够;生产故障往往发生在取消、超时、重试和状态恢复路径。
Kubernetes 调度器可以概括为一条有明确边界的流水线:
Queue 决定先处理哪个 Pod,Filter 决定哪些节点根本不合格,Score 在合格节点中表达偏好,Bind 把选择写入集群状态;插件和 Profile 则允许在这些阶段改变或扩展决策。理解这几个阶段之间的因果关系,才能正确判断一个问题究竟是队列等待、硬约束冲突、软偏好差异、绑定失败,还是绑定成功后的节点运行故障。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes etcd 数据层:Revision、Watch、事务、Compaction 和备份
- 下一篇:Kubelet 与容器运行时:CRI、Pod Sync、PLEG、Probe 和资源状态
- 延伸:Kubernetes 调度约束:NodeSelector、Affinity、Taint、Topology 和 Spread
- 延伸:Kubernetes 调度性能与扩展:Profile、Plugin、Queue、规模和测试
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论