Kubernetes Scheduler:从 Pod Placement 到 AI 工作负载调度
Kubernetes Scheduler 最核心的问题看起来很简单:
这个 Pod 应该放到哪个 Node 上?
在普通 Web Service 中,这个问题通常可以被简化为 CPU、Memory、亲和性和可用区。但当工作负载变成 GPU 推理、分布式训练或大规模 Batch Job 后,调度对象就不再只是一个孤立的 Pod:它可能需要一组协同启动的 Worker,需要特定型号的 GPU,需要同一台机器或同一条高速网络,还要和其他 Team 公平地分享昂贵资源。
先看两个表面上相同、实际上完全不同的请求:
100 个相互独立的 1-GPU Inference Task
vs
1 个需要 100 GPU 协同运行的 Training Job
前者找到一张卡就能推进一个任务,后者可能必须一次性找到足够的 GPU,并让 Worker 在合适的拓扑中协同启动。后文会反复回到这个例子。
因此,AI 场景真正要回答的问题变成了:
哪个 Job 应该先运行?
它什么时候可以拿到资源?
需要的资源能否作为一个整体满足?
这些 GPU 的型号、拓扑和互联关系是否合适?
资源释放后,怎样避免碎片和长期饥饿?
这篇文章先建立 kube-scheduler 的基本模型,再沿着 CPU/Memory → GPU → 分布式 AI 的路径,说明为什么原生 Pod 放置(Pod placement)最终会演进成一个多层调度系统。

kube-scheduler 负责什么
执行 kubectl apply -f pod.yaml 时,API Server 会把 Pod 写入集群状态。刚创建的 Pod 通常还没有 spec.nodeName,kube-scheduler 观察到它处于 unscheduled 状态后,选择一个 Node,再通过 API Server 写回绑定结果。
kubectl apply
↓
API Server → etcd
↓
Pending Pod(nodeName 为空)
↓
kube-scheduler 选择 Node
↓
Pod.spec.nodeName = node-3
↓
node-3 上的 kubelet 通过 CRI 启动 Pod
这里必须区分几个组件:
API Server
→ 保存并提供 Kubernetes API 与集群状态
kube-scheduler
→ 决定 Pod 的 Placement
kubelet
→ 在指定 Node 上落实 PodSpec
Container Runtime
→ 创建并运行 Container
所以 Scheduler 不会创建容器,也不会直接启动 Pod。它做的是一个控制平面决策:把尚未放置的 Pod 绑定到一个合适的 Node。 Scheduler 会读取 Pod、Node、已绑定 Pod 和各种约束,把待调度 Pod 放入调度队列;一次 scheduling cycle 找到候选 Node 后,再通过 Binding API 写回绑定结果。
Web 应用与 AI Job:调度对象已经变了
在传统 Web 应用中,Deployment Controller 和 kube-scheduler 通常已经足够完成主要工作:Deployment 维持副本数,滚动升级替换旧版本,kube-scheduler 为新 Pod 找到合适的 Node。
Deployment
↓
ReplicaSet / Pod
↓
kube-scheduler 选择 Node
↓
kubelet 启动服务
这类应用通常是长期运行的 Service。它们关心副本数、可用性、滚动升级和故障恢复;只要某个 Pod 能找到一台满足约束的机器,其他副本还可以继续提供服务。
AI Training 和大规模 Batch Job 则往往需要更高一层的 Job Scheduler 或 Queue Controller。它们面对的不是一个孤立 Pod,而是一个需要排队、配额、公平性和协同资源的 Job:
Job Scheduler / Queue Controller
→ 哪个 Job 先运行?
→ 是否满足 Team / Quota / Fairness?
→ 是否需要一组 Pod 同时获得资源?
→ 需要多少 GPU、什么型号和什么拓扑?
kube-scheduler
→ 这些已经获准运行的 Pod 分别放到哪个 Node?
Job Scheduler 通常不会直接调用 kube-scheduler。它通过 Kubernetes API、CRD、Scheduling Gates 或 Job 的 suspend/resume 等机制创建、挂起或释放 Pod;Pod 进入可调度状态后,kube-scheduler 仍然自动负责最终的 Pod-to-Node Placement。
因此可以把 Kubernetes 看成 Job Scheduler 使用的资源执行底座:
Job Scheduler
│ Kubernetes API / CRD
▼
Kubernetes Cluster
├── CPU / Memory
├── GPU
├── Node
└── Network / Storage
并不是所有 AI 应用都需要额外的 Job Scheduler。GPU Inference Deployment 通常仍然是 Service,可以沿用 Deployment;多租户 GPU Batch、分布式 Training、需要 Gang Scheduling 或跨队列借用资源的场景,才更容易需要 Job Scheduler。关键区别不在于 Pod 是否使用 GPU,而在于工作负载是否需要 Job 级别的排队、准入和协同调度。
Filter → Score → Bind
可以把 kube-scheduler 的基本流程先简化成三步:
所有 Nodes
↓ Filter
可行 Nodes
↓ Score
优选 Node
↓ Bind
Pod 与 Node 建立绑定
这是理解 Placement 的教学模型,不是 kube-scheduler 的完整内部流程。真实实现还有 scheduling queue、失败后的 backoff 和重新入队,以及 Scheduling Framework 提供的 Reserve、Permit、PreBind、Bind 等扩展阶段。插件可以在这些阶段预留资源、等待一组条件满足,或执行绑定前后的动作。
Filter:这个 Node 能不能运行
假设 Pod 声明:
resources:
requests:
cpu: "4"
memory: "16Gi"
三个 Node 的剩余可分配资源如下:
Node A: 8 CPU, 32 GiB → 可行
Node B: 2 CPU, 64 GiB → CPU 不足
Node C: 16 CPU, 8 GiB → Memory 不足
required 类型约束通常在 Filter 阶段淘汰节点。除了资源,还包括 NodeSelector、required Node Affinity、Taint/Toleration、Volume 拓扑、Pod 间亲和性等。任何一个硬约束不满足,Node 就会在这个阶段被淘汰;但不是所有约束都是硬约束,部分拓扑规则会根据 whenUnsatisfiable 配置表现为硬约束或软约束。
Score:可行的 Node 中哪个更好
Filter 之后可能还剩下多个 Node:
Node A → 72
Node D → 91
Node F → 65
Node H → 84
Scheduler 根据一组评分插件和权重选择得分更高的 Node。preferred Node Affinity、资源装箱或分散、拓扑偏好,都可以影响 Score;具体行为取决于启用的插件和 scheduler 配置。例如 NodeResourcesFit 可以使用 LeastAllocated、MostAllocated 或 RequestedToCapacityRatio 等资源评分策略。
Filter
→ Can it run?
Score
→ Which feasible Node is preferable?
最后的 Bind 是把决策写回 API Server。之后 kubelet 发现这个 Pod 属于自己的 Node,才会调用 CRI、CNI、CSI 等组件把它真正运行起来。
Scheduler 看的是 Request,不是瞬时 Usage
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
可以先用一个实用的区分来理解:
Request → 调度和 QoS 计算使用的资源声明
Limit → 运行时允许使用的上限
这里的“预留”是调度模型中的记账,不代表 CPU 或内存已经被物理独占。CPU 可以在运行时超卖;内存超出可用容量时则可能触发驱逐或 OOM。若只设置 limit,admission 配置可能把它补成 request,具体行为取决于集群配置。
如果 Node 的可分配 CPU 是 32,而已经放置了三个 request=8 的 Pod,那么 Scheduler 通常会把可调度余量视为约 8 CPU。即使这三个 Pod 此刻的实际使用量加起来只有 3 CPU,也不能仅凭瞬时 Usage 把剩余容量当成 29 CPU。
这意味着 Kubernetes 原生调度主要是基于声明资源的 Placement,不是实时利用率驱动的调度。Request 写得过大,会造成资源看起来被占满;写得过小,则可能导致节点过载。AI 集群里,这个区别尤其重要,因为 GPU 往往是不可超卖、价格高且难以临时补充的资源。
CPU 与 Memory 通常是每个 Node 都有的连续资源:
resources:
requests:
cpu: "4"
memory: "16Gi"
GPU 传统上通过 Device Plugin 暴露为 Extended Resource:
resources:
limits:
nvidia.com/gpu: 1
对扩展设备资源,常见写法是只设置 limits;Kubernetes 会把它作为调度需求使用。GPU 通常要求 request 与 limit 相等,不能像 CPU 一样随意声明不同的 request 和 limit。
Node 可能报告:
capacity:
cpu: 64
memory: 512Gi
nvidia.com/gpu: 8
对 kube-scheduler 来说,传统 GPU 资源首先只是一个带名字的容量。典型链路是:Device Plugin 向 kubelet 注册资源,Node 状态把容量提供给 kube-scheduler,Pod 被绑定后 kubelet 再调用 Device Plugin 的 Allocate,最后由 Runtime、NVIDIA Container Toolkit 或 Operator 等组件完成设备注入。
这也解释了一个容易混淆的边界:Scheduler 根据 Node 上报的资源做放置决策,但它并不直接决定“具体哪一张 GPU”或把 CUDA 设备塞进容器。对于需要更复杂设备请求、设备选择和生命周期管理的场景,还可以使用 Dynamic Resource Allocation(DRA)与 ResourceClaim。
这正是 Kubernetes 资源模型的价值:新的硬件可以通过插件接入,而不必把每一种加速器硬编码进 Scheduler。不过,“能分配一张 GPU”并不等于“这个 AI Job 一定能高效运行”,后面的问题会越来越超出简单的数量统计。
GPU Node:Taint 与 Affinity 的组合
GPU Node 成本高,通常不希望普通 nginx、Redis 或 Backend 因为 CPU 恰好有空闲就被放上去。可以给 GPU Node 加 Taint:
kubectl taint nodes gpu-node accelerator=nvidia:NoSchedule
AI Pod 再声明对应的 Toleration,并用 Affinity 选择目标节点:
tolerations:
- key: "accelerator"
operator: "Equal"
value: "nvidia"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: accelerator
operator: In
values: ["nvidia"]
两者解决的是不同方向的问题:
Taint
→ Node 排斥没有对应容忍的 Pod
Toleration
→ Pod 取消这种排斥,但不会主动选择这个 Node
Affinity
→ Pod 表达希望去哪些 Node
仅有 Toleration 只表示“我可以进去”,并不保证 Pod 会去 GPU Node;仅有 Affinity 也无法阻止其他没有 GPU 需求的 Pod 进入。因此专用硬件节点经常同时使用两套机制。
Bin Packing、Spreading 与 GPU Fragmentation
对三个副本的无状态服务,Spreading 往往更可靠:
Node A: replica-1
Node B: replica-2
Node C: replica-3
这样可以降低单节点故障造成的相关影响。对昂贵的 GPU,Bin Packing 也很重要:尽量把任务装进少数节点,留下完整的空闲节点,既提高利用率,也减少资源碎片。
Spreading
→ 故障隔离、避免热点、提高可用性
Bin Packing
→ 提高利用率、释放整机、降低成本
考虑四台各有 8 张 GPU 的 Node。如果四个任务各请求 2 张 GPU,并且平均分布:
Node A: ██░░░░░░
Node B: ██░░░░░░
Node C: ██░░░░░░
Node D: ██░░░░░░
集群总共空闲 24 张 GPU,但每台机器都没有足够空间运行一个需要 8 张 GPU 的 Job。这就是 Fragmentation:总资源足够,但资源的分布无法满足某个工作负载的形状。
在合适的场景中,可以通过 NodeResourcesFit 的 MostAllocated 或 RequestedToCapacityRatio 等评分策略偏向 Bin Packing;这不是所有集群的默认行为,也不是完整的 GPU 碎片治理方案。若优先装箱,则可能得到:
Node A: ████████
Node B: ████████
Node C: ░░░░░░░░
Node D: ░░░░░░░░
它为大 Job 留出了完整节点,但也不是所有任务都应该装箱。推理副本可能需要跨节点分散,训练任务可能需要同机 GPU,最终策略必须由工作负载的语义决定。
从 Pod 优先级到 Job 优先级
当资源不足时,Scheduler 需要决定谁先获得资源。Kubernetes 可以通过 PriorityClass 表达 Pod 的优先级,并在必要时进行 Preemption:高优先级 Pod 可能驱逐低优先级 Pod,为自己腾出可行位置。
但 AI 场景的抢占代价通常比 Web Service 高:
Web Pod 被驱逐
→ 重新拉起即可继续服务
Training Worker 被驱逐
→ 可能丢失计算进度
→ 其他 Worker 等待或失败
→ 需要 checkpoint 与恢复策略
而且 Pod 优先级不等于 Job 优先级。PriorityClass 首先是 Pod 级属性;高优先级 Pod 也不一定能成功抢占,仍要存在满足约束的候选节点,并受到被驱逐 Pod、PodDisruptionBudget 和终止宽限期等因素影响。一个分布式训练 Job 可能有 16 个 Worker;只抢到其中 8 个并不能让 Job 有效运行。Job 级优先级、公平性、等待时间和队列借用,通常需要上层 Job Controller 或 Batch Scheduler 为一组 Pod 提供一致的生命周期语义。
例如,下面的配置只定义了一个 Pod 优先级:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: training-high
value: 1000
globalDefault: false
description: "Priority for recoverable training jobs"
训练 Job 的每个 Worker 可以引用这个 PriorityClass,但它不会因此自动获得“整组准入”、公平排队或抢占恢复能力。
Gang Scheduling:先把一组 Pod 当成一个整体
分布式训练常见的约束是:只有当一组 Worker 同时获得足够资源时,Job 才有意义。
Training Job
├── worker-0
├── worker-1
├── worker-2
└── worker-3
如果 Scheduler 逐个绑定 Pod,可能出现:
worker-0 → 已绑定
worker-1 → 已绑定
worker-2 → Pending
worker-3 → Pending
前两个 Worker 占住了资源,却无法开始训练;后两个 Pod 也许永远等不到位置。这种“部分成功”对普通 Replica 尚可接受,对同步训练则可能是纯粹的资源浪费。
Gang Scheduling 的核心语义是:
只有当 Job 至少能同时获得所需的最小资源集合时,才允许这一组 Pod 开始绑定或运行。
这要求系统具备 Job 级别的视角:维护 group、计算整体资源需求、检查整体可行性,并在资源不足时让整个 Job 等待。原生 kube-scheduler 的基本调度单元仍是 Pod,并不会自动把多个 Pod 聚合成一个 Job 级 Gang。现实中常见的实现路径包括:由上层 admission controller 先准入再释放 Pod;用 Scheduling Gates 暂缓 Pod 进入调度;或使用 Kueue、Volcano 等系统提供的 PodGroup、Gang 和调度插件能力。
“同时获得资源”也不意味着所有容器在同一时刻开始执行。Gang 通常保证整组 Pod 达到准入或调度条件后才释放;镜像拉取、设备初始化和 kubelet 状态仍可能让实际启动存在时间差。
GPU 不一定是同质资源
nvidia.com/gpu: 1 只表达数量,不一定表达 AI 应用真正关心的全部信息:
GPU 型号
显存大小
GPU 与 CPU 的 NUMA 关系
GPU 之间的 NVLink / PCIe 拓扑
网卡与 GPU 的距离
所在的可用区或机架
两个 Node 都有一张空闲 GPU,但一个可能是适合推理的型号,另一个才满足训练所需显存;两台机器都能放下 8 个 Worker,但只有其中一台的 GPU 与高速网卡拓扑适合通信密集型训练。
因此常见做法是通过 Node Feature Discovery、标签、扩展资源、设备分配策略或调度插件表达硬件特征。Node label 适合表达型号、区域等节点级属性;Device Plugin 的 preferred allocation、Topology Manager 可以进一步参与设备与 NUMA 协调;DRA 更适合表达复杂设备资源。不过,标签无法自然表达“这 8 张 GPU 中哪几张彼此相连”,也无法完整表达 GPU 与网卡的距离。
故障域拓扑与设备拓扑不是一回事
对分布式训练来说,资源的“数量”和“距离”同样重要。先区分两类拓扑问题。
故障域:Topology Spread Constraints
Topology Spread Constraints 主要用于让 Pod 按 region、zone、hostname 等拓扑域分布,降低单个故障域失效带来的影响:
它并不直接计算 NVLink、PCIe、RDMA 带宽或 GPU-NIC 距离。对普通 Web 副本,分散通常是好事;但对需要频繁 AllReduce 的训练 Worker,盲目分散到延迟更高的区域,可能让 GPU 利用率看起来很高,Job 的整体吞吐却很差。
设备与网络:NUMA、NVLink、PCIe、RDMA
这类拓扑关注的是设备之间的通信成本:
同一 GPU 节点 / 高速互联域
↓ 通常最低
同一机架或网络域
↓
同一可用区
↓
跨可用区
↓ 通常最高
GPU 与 CPU 的 NUMA 关系、GPU 之间的 NVLink/PCIe 连接、GPU 与网卡的距离,通常需要 Device Plugin、Topology Manager、DRA 或专用调度插件提供信息和选择逻辑。对这类问题,Pod Affinity 或专用拓扑感知调度器可能比 Topology Spread 更合适。
这也是 AI 调度和普通 Web 副本调度的一个分界:Web 服务常常希望副本分散,训练 Worker 却可能希望在满足容错的前提下尽量靠近。
ResourceQuota 与 Fairness
当多个 Team 共享 GPU 集群时,仅靠 Node 上的 Filter 和 Score 不够。还需要回答:
Team A 最多能用多少 GPU?
Team B 是否有保底配额?
某个 Team 长时间空闲时,其他 Team 能否借用?
高优先级是否意味着可以无限抢占?
等待很久的 Job 如何避免永久饥饿?
ResourceQuota 是 Namespace 级的 admission 约束,通常根据 requests/limits 限制 Namespace 可以创建或持有的资源总量;它本身不负责决定哪个 Job 先运行,也不提供跨 Namespace 的公平队列、借用、回收和优先级老化。公平性通常还需要队列、权重、借用、回收和 Job 级别的 admission 策略。
这说明调度其实包含两个不同阶段:
Admission / Queueing
→ 现在谁有资格获得资源?
Placement
→ 获得资格后,Pod 应该放在哪个 Node?
前者更像集群级资源管理,后者才是 kube-scheduler 最熟悉的 Pod Placement。Kueue、Volcano、YuniKorn 等系统可以在不同程度上提供队列、准入、公平性或 Batch 调度能力。
三层调度视角
在真实的 AI 平台里,可以把决策拆成三层:
业务控制器 / Job Orchestrator
→ 选择模型、租户、数据集、训练策略与资源规格
Batch Scheduler
→ 排队、配额、公平性、Gang、借用与抢占
kube-scheduler
→ 将具体 Pod 放到满足约束的 Node
这三层并不是 Kubernetes 固定的产品架构,也不一定由三个独立组件实现。业务控制器很多时候并不真正执行节点调度,而是创建 Job、选择队列、设置资源和约束;Batch 层知道 Job 之间如何竞争资源;kube-scheduler 知道 Kubernetes Node、Pod 约束和集群状态。
如果把所有逻辑都塞进单个 Pod 的 YAML,最终往往会得到大量难以维护的 Affinity、优先级和标签;如果完全绕开 kube-scheduler 自己分配节点,又会重复实现 Kubernetes 已经具备的状态管理和节点约束。更合理的系统通常是让高层做 Job 级决策,再把可执行的 Pod 交给 Kubernetes 落地。
回到两个“100 GPU”问题
“需要 100 张 GPU”并不能直接决定调度算法。至少要区分两种情况。
100 个独立 Inference Task
Task-1 → 1 GPU
Task-2 → 1 GPU
...
Task-100 → 1 GPU
任务彼此独立时,可以分批调度,优先考虑装箱、利用率、模型缓存、故障域和服务延迟。只要单个 Task 找到合适的 GPU,整体就能逐步前进。
一个需要 100 GPU 的 Distributed Training Job
Job
├── worker × 100
└── 需要协同启动与高速通信
此时更关心:
Gang Scheduling
拓扑与网络带宽
GPU 型号和显存一致性
整组资源是否能同时满足
Checkpoint 与抢占恢复
两者的总 GPU 数相同,调度模型(Scheduling Model)却完全不同。一个可以渐进地填充集群,另一个可能必须等待一个足够大的资源窗口。
这里假设 GPU 以整卡独占方式分配。如果使用 MIG、time-slicing、fractional GPU 或弹性训练,资源形状和 Gang 语义都会变化。
因此 AI 调度的第一原则是:
先理解工作负载语义(Workload Semantics),再决定调度模型。
总结
对普通 Kubernetes 工作负载,最重要的主线是:
Request / Constraint
↓
Filter → Score → Bind
↓
Node
这套模型解决了绝大多数 Pod Placement 问题:资源是否足够,Node 是否匹配,哪个可行 Node 更合适。
进入 AI / GPU 场景后,应该按下面的顺序判断:
先问:任务是否独立?
再问:资源是否同质?
再问:是否需要同时启动?
再问:是否依赖设备或网络拓扑?
最后问:是否需要跨租户队列与公平性?
所以 kube-scheduler 并没有失去价值;它仍然是把 Pod 落到具体 Node 的核心组件。只是对于 AI 平台,Pod Placement 只是最后一公里。更完整的系统还需要:
Resource Management + Queueing + Job Coordination + Topology-Aware Placement
理解这个演进,就能理解为什么 CPU/Memory 时代的“给 Pod 找一台机器”还不够回答今天的 AI 调度问题:现在要调度的,往往是一个具有资源形状、协同关系、通信拓扑和公平性要求的 Job。