框架与多租户
为什么不能只用 K8S 默认调度器
K8S 默认调度器适合每个 Pod 独立调度的在线服务。但 GPU 训练任务通常需要一组 Pod 同时启动、长时间运行、拓扑敏感、多租户公平和抢占恢复。默认 scheduler 没有任务级队列、没有作业级配额、没有 Gang 语义,也不能表达“超过 10 张 GPU 后继续提交但排队”。
| 需求 | 默认 K8s 支持情况 | 需要补的能力 |
|---|---|---|
| 任务级排队 | 不支持,只是 Pod 调度队列 | Queue / Workload / AIJob 准入队列 |
| 租户 GPU 限额 | ResourceQuota 会直接拒绝超额创建 | 允许提交,超额任务排队等待 |
| Gang Scheduling | 不支持 job 级 all-or-nothing | PodGroup / Workload / Permit 等待 |
| 异构 GPU | 靠 label / extended resource 粗表达 | ResourceFlavor / DRA / 拓扑画像 |
| 公平共享 | 默认只按 Pod 优先级和资源匹配 | DRF、proportion、borrowing、reclaim |
框架选型地图
| 框架 | 定位 | 最适合 | 主要代价 |
|---|---|---|---|
| Volcano | K8s 原生批调度系统 | 训练/HPC/Spark/Flink 等需要 Gang 和 Queue 的场景 | 学习 CRD 和插件体系,和调度器耦合较深 |
| Kueue | K8s SIG 官方作业准入/排队系统 | 不想替换 scheduler,只想做队列和配额准入 | 不控制 Pod 级调度细节 |
| YuniKorn | 层级队列资源管理器 | 大型组织、多层部门/团队资源治理 | 架构和运维复杂度更高 |
| Run:ai | 商业 GPU 虚拟化平台 | 快速落地 GPU 共享、配额、可视化和成本治理 | 闭源、定制深度受限 |
| 自研队列 + Scheduler Plugin | 按业务定制 | 已有 K8s 基础但需要 AIJob、预测、干扰、代价抢占 | 研发和维护成本最高 |
面试回答套路
- 先说默认 K8s 只能解决 Pod 到 Node 的绑定,不解决任务级队列和 Gang。
- 再说 Volcano / Kueue / YuniKorn / Run:ai 分别补哪一层能力。
- 最后根据规模和需求选型:小规模用 Volcano/Kueue,组织层级复杂看 YuniKorn,需要 GPU 共享商业能力看 Run:ai,深度定制走自研。
Volcano 定位与架构
Volcano 是 Kubernetes 批处理调度的新范式:为 AI/ML、HPC 等高性能工作负载补齐默认调度器缺失的 Gang、队列与公平性能力。
Volcano 是 CNCF 孵化的 Kubernetes 批处理调度系统,面向 AI/ML、HPC、Spark、Flink、Ray 等高性能工作负载。它通过 CRD 扩展 K8s 对象,再由 scheduler、controller manager、admission 协同完成批调度。
Volcano 架构图:在 Kubernetes 之上增加 batch scheduler、controller、admission 和 Job / Queue / PodGroup 等 CRD。
Volcano 调度链路:通过 CRD 扩展 K8s 资源对象,再由 scheduler / controller / admission 协同完成批调度。
安装与最小验证
面试不需要背命令,但要知道 Volcano 部署后至少有 scheduler、controller、admission 组件,以及 Queue / PodGroup / Job CRD。
helm repo add volcano-sh https://volcano-sh.github.io/helm-charts
helm repo update
helm upgrade --install volcano volcano-sh/volcano \
--version 1.12.0 \
-n volcano-system \
--create-namespace
kubectl get all -n volcano-system
kubectl get crd | grep volcano
使用 Volcano 特性的 Job 要指定 schedulerName: volcano。如果改成 default-scheduler,就无法使用 Volcano 的 Gang、Queue、Fair-share、Preemption 等能力。
三大核心对象关系
Volcano 三大核心对象:Queue(资源池)、PodGroup(Gang 调度单元)、VolcanoJob(批作业抽象)。
Volcano 对象模型:Queue 管资源池,VolcanoJob 管用户作业,PodGroup 管 Gang 调度,TaskInfo 是 Pod 的调度内部包装。
Queue 是资源池,PodGroup 是 Gang 调度单元,VolcanoJob 是批作业抽象。
| 对象 | 核心含义 | 关键字段 | 面试重点 |
|---|---|---|---|
| Queue | 多租户资源队列 | weight、capability、deserved、reclaimable | 资源隔离、弹性借用、reclaim |
| PodGroup | 一组强关联 Pod 的 Gang 单元 | minMember、minResources、priorityClassName、queue | All-or-Nothing,避免 partial allocation |
| VolcanoJob | 批作业抽象,包含多个 task | schedulerName、minAvailable、tasks、policies、plugins、queue | 多角色训练任务、生命周期策略 |
Queue:多租户资源管理
| 字段 | 作用 | 面试解释 |
|---|---|---|
weight | 按比例分配资源 | 适合资源软约束,空闲时可以动态共享 |
deserved | 队列期望应得资源 | 表达 fair-share / deserved resource |
capability | 队列可用资源硬上限 | 防止某队列超用整个集群 |
reclaimable | 资源是否可被回收 | 空闲借用和高优队列回收的基础 |
PodGroup:Gang Scheduling 的落地对象
| 字段 | 作用 | 设错的后果 |
|---|---|---|
minMember | 至少多少个 Pod 同时满足才允许启动 | 太低会 partial allocation,太高会长期 Pending |
minResources | 整体最小资源需求 | 可以提前判断集群是否可能满足 |
priorityClassName | PodGroup 优先级 | 影响抢占和排队顺序 |
queue | 归属资源队列 | 影响配额和公平性 |
VolcanoJob:批作业与生命周期策略
VolcanoJob 状态包括 pending、running、restarting、completing、completed、failed、terminating 等。
| 字段 | 作用 | 面试关注点 |
|---|---|---|
schedulerName | 指定调度器 | 保持 volcano 才能使用高级策略 |
minAvailable | Job 正常运行所需最少 Pod 数 | 类似 Gang 的最低可运行条件 |
tasks | 定义多角色 Pod 模板 | PS / Worker / Master / Launcher 等角色 |
policies | 生命周期策略 | PodFailed、PodPending、TaskCompleted 等事件触发动作 |
plugins | 任务级插件 | 如 ssh、svc、env,为分布式任务提供互信和服务发现 |
maxRetry | 最大重试次数 | 故障恢复和失败终止的边界 |
源码视角:Action、Plugin、Session
Volcano 调度周期:OpenSession 建上下文,Plugin 注册算法函数,Action 顺序执行并调用这些函数,CloseSession 清理状态。
Volcano 调度器内部不是简单套 kube-scheduler 的扩展点,而是有自己的 Action + Plugin + Session 框架。理解这层,面试回答会明显更深入。
| 概念 | 作用 | 怎么理解 |
|---|---|---|
| Action | 调度周期里要执行的动作 | 例如 enqueue、allocate、backfill、preempt、reclaim、shuffle |
| Plugin | 给 Action 提供算法函数 | 例如 gang、drf、proportion、priority、binpack |
| Session | 一次调度周期的上下文 | 保存 Jobs、Queues、Nodes 以及插件注册的排序/过滤/抢占函数 |
关键机制:OpenSession 时插件把函数注册到 Session;随后 actions 按配置顺序执行,并调用 Session 里的算法函数;最后 CloseSession 清理和提交状态。
Volcano Actions:一轮调度做哪些动作
| Action | 做什么 | 面试抓手 |
|---|---|---|
enqueue | 把 Pending 的 PodGroup / Job 判断为可入队,更新为 Inqueue | 解决“作业能不能进入队列” |
allocate | 给 Inqueue 任务分配节点资源,选择最合适的 Node | 核心资源分配动作,类似 Filter + Score + Bind 的组合 |
backfill | 利用碎片资源调度适合插空的任务 | 提高利用率,但不能破坏主要调度目标 |
reclaim | 从超额使用队列回收资源 | 队列间公平性和资源借用回收 |
preempt | 同队列或跨队列中按优先级抢占低优任务 | 高优任务保障,注意抢占代价 |
shuffle | 打散或重排任务,缓解局部不优 | 较少面试深挖,知道存在即可 |
runOnce
→ OpenSession(cache, plugins, config)
→ action.Execute(session) // enqueue / allocate / backfill ...
→ plugins registered functions are called through session
→ CloseSession(session)
源码对象不要混:VolcanoJob、JobInfo、TaskInfo
| 对象 | 在哪一层 | 真实含义 |
|---|---|---|
| VolcanoJob | CRD / controller 层 | 用户提交的批作业对象,包含 tasks、policies、plugins 等 |
| PodGroup | CRD / scheduler 层 | Gang 调度单元,表达一组 Pod 的 all-or-nothing 语义 |
| JobInfo | scheduler cache / Session 层 | 调度器内部的 Job wrapper,本质更接近 PodGroup 的调度视角 |
| TaskInfo | scheduler cache / Session 层 | Pod 的 wrapper,一个 TaskInfo 基本对应一个 Pod |
| QueueInfo | scheduler cache / Session 层 | Queue 的调度视图,保存 allocated、deserved、capability 等状态 |
Volcano 高频追问
Queue 是租户资源视角,PodGroup 是调度原子性视角,VolcanoJob 是用户提交的批作业视角。
VolcanoJob 提交后会关联一个 Queue,并自动创建 PodGroup。Queue 决定这个 Job 属于哪个资源池;PodGroup 决定这组 Pod 是否满足 minMember / minResources,可以 all-or-nothing 启动;VolcanoJob 自己定义 tasks、policies、plugins、maxRetry 等批任务语义。
不要把 VolcanoJob 等同于 PodGroup。VolcanoJob 是用户作业;PodGroup 是调度器用于 Gang 的原子单元;Queue 是资源治理对象。
默认 kube-scheduler 逐 Pod 调度,可能只启动部分 worker,剩余 worker Pending。对 DDP / MPI / NCCL 这类强同步任务来说,部分 worker 启动没有意义,GPU 会空转,多个 Job 还可能互相占住部分资源形成死锁。
Volcano 用 PodGroup 的 minMember 和 minResources 表达 all-or-nothing 语义。资源不满足时整组等待;资源满足时整体进入运行。
Gang 会提高正确性,但可能增加等待时间。大任务需要凑齐一组资源,小碎片不能随便启动它的一部分。
Volcano 解决了默认 K8s 在批任务里的核心缺口:Gang、Queue、多角色 Job、队列公平和部分抢占。
它不天然解决运行时间预测、干扰感知共置、checkpoint-aware preemption、复杂异构 GPU 拓扑和超大规模全局优化。
当你需要 QAD 这类连续保障度、预测调度、共置干扰模型、代价感知抢占或千卡以上全局优化时,通常要自研 scheduler plugin 或独立调度层。
Action 是调度流程中的动作,例如 enqueue、allocate、backfill、reclaim、preempt。它决定“这一轮调度要做哪些步骤”。
Plugin 是算法提供者,例如 gang、drf、priority、binpack。它把排序、过滤、抢占、可回收判断等函数注册到 Session。
每轮调度先 OpenSession,插件在 OnSessionOpen 里注册函数;Action 执行时调用 Session 里的函数;最后 CloseSession 清理状态。
解决“这个 Job / PodGroup 能不能进入队列成为 Inqueue”。它更偏作业准入和队列状态更新。
解决“已经 Inqueue 的任务具体分配到哪些节点”。它会结合 predicate、node order、task order、queue order 等插件函数做资源分配。
从任务优先级出发,高优任务抢占低优任务,重点是“谁更重要”。
从队列公平性出发,从超额使用或可回收资源的队列里拿回资源,重点是“哪个队列超额了”。
Kueue 解决什么问题
Kueue 的核心不是“给 Pod 打分”,而是“作业能不能被准入”。它适合你不想替换默认 scheduler,但需要 LocalQueue、ClusterQueue、ResourceFlavor、borrowing 和公平共享的场景。
| 对象 | 作用 | 面试解释 |
|---|---|---|
| LocalQueue | namespace 内用户提交入口 | 用户只看到本 namespace 的队列 |
| ClusterQueue | 集群级资源池和配额 | 平台管理员配置 quota、borrowing、flavor |
| ResourceFlavor | 资源类型 / 节点池抽象 | A100、H100、spot、on-demand 等资源口味 |
| Workload | 一个待准入作业的抽象 | 包含 PodSet,Kueue 判断它是否可以开始 |
Kueue 调度链路
- 用户提交 Job / PyTorchJob / RayJob。
- Kueue webhook 或 controller 生成 Workload。
- Workload 进入 LocalQueue,再映射到 ClusterQueue。
- Kueue 判断某个 ResourceFlavor 下是否有足够 nominal quota 或可借用额度。
- 准入后给 PodSet 写入 flavor 相关 nodeSelector / toleration 等信息。
- 具体 Pod → Node 绑定仍由 kube-scheduler 完成。
Kueue vs Volcano
| 维度 | Kueue | Volcano |
|---|---|---|
| 架构 | 准入/排队层,复用 kube-scheduler | 批调度系统,深度控制调度过程 |
| 强项 | 队列、配额、ResourceFlavor、渐进式接入 | Gang、Queue、Job 生命周期、调度插件 |
| 升级风险 | 低,和 scheduler 解耦 | 相对高,和调度链路耦合更深 |
| 控制粒度 | 准入层控制,Pod 绑定交给默认 scheduler | 调度过程内控制,能做更细的 Permit / Reserve |
异构 GPU 集群里“1 张 GPU”不是等价资源。A100/H100/V100、spot/on-demand、不同拓扑和成本都不同。ResourceFlavor 把资源的质纳入队列配额,让 Workload 在准入阶段就知道自己被分到哪类资源。
YuniKorn:层级队列和 Application 级调度
YuniKorn 源自 YARN 的资源管理思想,核心优势是层级队列。它适合公司 / 部门 / 团队 / 项目多层资源治理场景。
| 能力 | 说明 | 适用场景 |
|---|---|---|
| 层级队列 | root → department → team → project | 大型组织资源治理 |
| Application 级调度 | 一组 Pod 作为一个应用管理 | Spark、Flink、训练任务 |
| 替换调度器 | 可以作为完整 scheduler 运行 | 需要强资源管理能力的集群 |
Run:ai:商业 GPU 共享和配额平台
Run:ai 提供 GPU 分时共享、配额、项目级资源治理、可视化和成本归因。它的优势是开箱即用,适合想快速落地 GPU 平台能力的团队。
| 能力 | 价值 | 局限 |
|---|---|---|
| GPU sharing | 提高 Notebook、小实验、推理任务利用率 | 闭源实现,深度定制受限 |
| Quota / borrowing | 项目级配额和空闲借用 | 策略能力取决于产品版本 |
| 可视化 | 降低平台运维门槛 | 大规模特殊需求可能仍需自研 |
层级队列把组织结构映射到资源治理:公司给部门配额,部门给团队配额,团队之间可以按规则借用和回收。扁平队列在团队少时够用,但组织层级多后很难维护公平和预算边界。
面试场景题(面试官口吻)
这类题面试官不会直接说“设计一个 CRD”,而是给业务场景让你识别原生能力缺口:
“我们有一个几千张卡的 GPU 训练集群,混合了 A100 80G、H100、V100 多种型号。资源按团队分配额度,而且是按卡型分别给:比如推荐团队分到 64 张 A100、16 张 H100。这个团队几十号算法工程师白天会密集提交训练任务,有人跑单卡调试,有人提 8 卡实验,还有人要 32 卡做大模型预训练。高峰期他们提交的任务加起来需要的 A100 远超 64 张。
我的诉求是:配额是硬上限不能突破(不然挤占别的团队),但又不能让工程师提交时直接报错——应该能正常提交,超额的自动排队,集群里有任务跑完释放卡,排队的自动顶上。同时 32 卡大任务不能被一堆小任务一直插队饿死,重要任务还得能优先。你用 Kubernetes 怎么设计?原生的 ResourceQuota、scheduler 能直接满足吗?哪里不够、你怎么补?”
| 这道题在考你 | 想确认你懂 |
|---|---|
| 识别原生能力边界 | ResourceQuota 是“拒绝”语义,不是“排队”语义 |
| 排队的位置 | 排队要发生在建 Pod 之前,不是让 Pod 涌进 scheduler |
| 异构配额 | 额度按 (team, gpuType) 分别算,不能合并成总卡数 |
| 两级队列 | 业务准入(配额/优先级/防饿死)vs 调度放置(gang/拓扑)分开 |
| 状态怎么存 | CRD + etcd,不另起数据库;账本靠 reconcile 重算 |
| 落地参照 | 知道 Volcano / Kueue 是这套结构,但 Volcano 只承担二级 |
为什么原生机制和单靠 Volcano 都不够
| 机制 | 能不能满足 | 原因 |
|---|---|---|
| ResourceQuota | 不满足 | 超 quota 直接 Forbidden 拒绝创建,不保留等待队列,违背“超额也能提交” |
| PriorityClass | 不满足 | 只表达优先级,不表达团队/卡型运行上限 |
| 默认 scheduler | 不满足 | 只消费已创建 Pod,不管理任务级准入队列 |
| 单靠 Volcano | 不建议完全依赖 | 它的 Queue 偏 scheduler 内部队列:能让 PodGroup Pending,但不承载“你排第几、是 A100 还是 H100 不足、预计何时启动”等业务语义 |
| 两级:自研业务队列 + Volcano | 满足 | 一级管业务准入与配额,二级管 gang 与放置 |
Volcano 文档明确:当 enqueue 判断某 PodGroup 不允许进入队列时,vc-controller 不会创建 pending pods,reclaim/preempt 也不执行。这会影响“超额任务排着、重要任务可触发抢占/回收”的业务语义,所以业务排队不能完全交给 Volcano。
推荐架构:两级队列
核心原则:用 Volcano 做二级调度器,不让它承担完整业务排队。自己实现一级 TrainingJob Queue,所有超额任务先进自研队列,只有拿到团队/卡型 quota token 后才创建 VolcanoJob。
两级队列:一级自研业务队列负责准入与配额(source of truth),二级 Volcano 负责 gang 调度与放置。超额任务停在 Queued,不创建 Pod。
| 层级 | 组件 | 职责 |
|---|---|---|
| 一级 自研平台 | Admission Webhook | 校验身份、team、优先级权限、卡型;拦截“单任务请求 > 团队 quota”这种永远无法满足的任务 |
| Platform Queue Controller | 接收所有合法 TrainingJob,永不因当前 quota 满而拒绝,进入 Queued | |
| Quota Manager | 维护 (team, gpuType) 的 hard / used / reserved 账本,是业务配额的 source of truth | |
| Admission Scheduler + Dispatcher | 按 priority / aging / 大任务 reservation / backfill 排序,reserve quota 后才创建 VolcanoJob | |
| 二级 Volcano | Volcano Queue capability | 按卡型配 capability,作为执行层 guardrail,即使平台 bug 多放也不会无限超发 |
| Volcano Scheduler | 只接收已 admission 的任务,做 PodGroup、gang、allocate、preempt、binpack、topology-aware 放置 |
异构配额:账本是二维表,不是一个数
A100 和 V100 不能 1:1 抵扣,所以配额账本必须按 (team, gpuType) 分格维护,准入判断也要先看任务要哪种卡,再查那一格:
recommend / a100-80g : hard=64 used=? reserved=?
recommend / h100 : hard=16 used=? reserved=?
search / a100-80g : hard=32 ...
准入判断:该格 used + reserved + 任务要的同型号卡数 <= 该格 hard
这正好对应 Kueue 的 ResourceFlavor(按卡型区分资源)和 Volcano Queue 的多维 capability。业务队列本身也按这个维度分层:
TeamQueue
└── GPUTypeQueue
└── PriorityQueue
└── FIFO / Aging / Backfill
recommend
├── a100 ├── P0 ├── P1 ├── P2 └── P3
└── h100 ├── P0 ├── P1 ├── P2 └── P3
具体走例:团队 20 张 A100,已用 15,连续提交
用一个小数字把准入逻辑走通(生产是 64,这里用 20 便于看)。team-a 配额 20,已跑 15(Job-A=8, Job-B=7),用户连续提交 Job-C(4)、Job-D(6)、Job-E(2):
第 1 轮 reconcile(提交后):
running = Σ(Running/Admitted) = 8 + 7 = 15 # 重新数,不是存的
pending = [C(4), D(6), E(2)] # 按入队顺序
C:15 + 4 = 19 <= 20 ✅ Admitted,running→19
D:19 + 6 = 25 > 20 ❌ break(不跳过去看更小的 E,防止大任务被饿死)
结果:只有 C 被准入并建 Pod;D、E 停在 Queued,一个 Pod 都不建
第 2 轮 reconcile(Job-A 跑完释放 8 张,触发重算):
running = 7 + 4 = 11 # B + C
D:11 + 6 = 17 <= 20 ✅ Admitted,running→17
E:17 + 2 = 19 <= 20 ✅ Admitted,running→19
结果:D、E 都被准入并建 Pod
状态存哪里:不用数据库,存在 CRD 的 spec / status
很多人第一反应是“再起一个 MySQL / Redis 存队列和配额”,但在 K8s 里更标准的做法是不引入外部数据库,把状态拆成两类,全部交给 etcd:
| 状态类型 | 存在哪 | 谁写 | 例子 |
|---|---|---|---|
| 用户期望(desired) | CRD 的 spec | 用户 / 提交端 | 要几张卡、哪种卡型、哪个队列、优先级 |
| 系统观测(observed) | CRD 的 status | 控制器 | TrainingJob 当前 phase、配额账本已用量、等待列表 |
etcd 是 K8s 的强一致 KV 存储,CRD 读写都走 API Server,自带 watch、乐观锁(resourceVersion)、RBAC 和审计。另搭数据库反而要自己解决一致性、备份、和 etcd 状态对不齐的问题,所以默认不这么做。
# TrainingJob:任务期望 + 状态机
apiVersion: scheduling.example.com/v1
kind: TrainingJob
metadata:
name: llm-pretrain-001
spec:
team: recommend
gpuType: a100-80g
gpuCount: 32
priority: p1
minAvailable: 32 # Gang:32 个 Pod 要么一起跑
status:
phase: Queued # Submitted->Queued->Admitting->SubmittedToVolcano->Running->Succeeded/Failed/Cancelled
reason: "waiting for a100 quota"
# 配额账本:按 (team, gpuType) 分格,存在账本 CRD 的 status
status:
quotas:
- team: recommend
gpuType: a100-80g
hard: 64
used: 60 # 在跑的同型号卡总数
reserved: 4 # 已 reserve、待 Volcano 拉起
- team: recommend
gpuType: h100
hard: 16
used: 8
reserved: 0
配额账本怎么算出来:reconcile 而不是手工加减
关键认知:used 这个数字不要靠“准入时 +N、结束时 -N”手工累加,因为控制器会重启、会漏事件,累加值会和真实情况飘移。正确做法是每次 reconcile 都从真相源全量重算:
reconcile(team, gpuType):
1. List 该 team 该卡型下 phase∈{Admitting,SubmittedToVolcano,Running} 的 TrainingJob
2. used = Σ(它们的 gpuCount) # 重新算,不依赖旧值
3. for job in pending(按 priority/aging/FIFO 排序):
if used + reserved + job.gpuCount <= hard and rough_cluster_fit(job):
reserve_quota(job); create_volcano_job(job)
job.phase = SubmittedToVolcano
else:
break # 队头算不动就停,保证顺序、防饿死
4. 写回 quota.status 和各 job.status
控制器重启、丢了内存队列后,下次 reconcile 也能从 etcd 里的 TrainingJob 列表把账本完整重建,状态是“可重算的”而不是“攒出来的”——这就是 K8s 控制器 level-triggered(看最终状态)而非 edge-triggered(依赖每个事件)的思想。
并发与一致性:多副本控制器怎么不互相打架
| 问题 | 处理方式 |
|---|---|
| 两个任务同时准入导致超额 | 同一个 (team,gpuType) 串行 reconcile(workqueue 按 key 去重,同 key 不并发),不会两个 goroutine 同改一个账本 |
| 写 status 时对象已被改过 | API Server 用 resourceVersion 乐观锁,update 冲突返回 409,控制器 requeue 重算再写 |
| 控制器跑多副本 | 用 Lease 做 leader election,同一时刻只有一个 leader 真正 reconcile |
| reserve 后 Volcano 长时间拉不起来 | SubmittedToVolcano 设超时,超时 Requeue 退回 Queued 并释放 reserved,避免名额被占死 |
宕机恢复:控制器无状态,靠 reconcile 重建
控制器内存里的队列只是缓存。崩溃 / 升级 / 重调度后:
- 新实例启动,通过 informer 从 API Server List + Watch 拉全量 TrainingJob 和账本 CRD。
- 对每个
(team,gpuType)触发 reconcile,重新算 used 和准入结果。 - 已经在跑的 Pod 不受影响(真相在 etcd 和节点上),队列顺序从 TrainingJob 的创建时间 / 优先级字段恢复。
所以“状态怎么保存”的答案是:任务和配额状态保存在 etcd 里的 CRD 对象上,控制器自己不持久化任何东西,靠 reconcile 随时重建。只有 etcd 不该放的东西才进外部数据库——跨集群全局配额、长期计费/用量审计、历史任务归档写 Postgres/ClickHouse,但准入决策的实时账本仍留在 etcd。
大任务防饿死与抢占:策略在平台层,执行在 Volcano
| 能力 | 放哪 | 为什么 |
|---|---|---|
| head-of-line reservation | 平台层 | 32 卡大任务等久了要占住名额,不让小任务无限插队 |
| conservative backfill | 平台层 | 只在不影响大任务预计启动时,才放声明短时长的小任务回填空隙 |
| aging / 用户级公平 | 平台层 | Volcano 不知道业务意图(等了几小时、debug 任务多久结束) |
| 选 victim(抢谁) | 平台层 | 挑低优先级、可抢占、有 checkpoint、释放卡数合适、损失最小的任务 |
| gang 调度 | Volcano | 一旦下发,保证 32 个 Pod 要么一起跑 |
| PodGroup/Pod 抢占执行 | Volcano | preempt action 执行同 queue 内实际驱逐与重调度 |
三个不建议的做法
| 不建议 | 问题 |
|---|---|
| 让用户直接提交 VolcanoJob | 平台无法稳定控排队顺序、业务状态不可解释、Pending 对象太多污染调度层、权限和防绕过难做、大任务防饿死难做 |
| 用 ResourceQuota 做硬限制 | 超额提交直接 Forbidden,不符合“正常提交、超额排队”的诉求 |
| 完全相信 Volcano Queue capability | 它只能当底线 guardrail,不能当唯一账本——pending 统计、队列位置、用户级公平、单任务合法性、业务优先级、审计计费都还得平台层做 |
多租户管理:为什么"配额"不是简单地"分一下 GPU"?
多租户管理的本质问题是:如何在保证每个租户基本权益的前提下,最大化集群利用率?
最简单的方案是"固定配额"——A 团队分 40 张 GPU,B 团队分 60 张 GPU,互不干扰。问题是:A 团队晚上不用 GPU,B 团队晚上要跑实验,但 B 不能用 A 闲置的 GPU——因为"那是 A 的配额"。结果集群利用率只有 40%。
所以多租户管理的核心矛盾是:保障性(guarantee)vs 弹性(elasticity)。保障性强 → 利用率低;弹性强 → 可能损害保障。所有配额方案都在这个光谱上找平衡。
怎么理解:像公司停车位。固定配额 = 每人一个固定车位,你的车位空着别人也不能停。弹性配额 = 每人有保底车位(来了肯定有位),但空着时别人可以临时用,你来了别人得让出来。
四种配额方案详解
1. 固定配额(Static Quota)
定义:每个租户分配固定的 GPU 数量,互不借用。K8S 原生的 ResourceQuota 就是这种模型。
怎么理解:买断制——你买了 40 张 GPU,不用也归你,别人不能用。
优点:(1) 强保障——你的 GPU 永远不会被别人抢走;(2) 实现最简单——ResourceQuota + Namespace 隔离就够了。
致命问题:利用率低。实测数据:固定配额的 GPU 集群平均利用率通常只有 30-40%,因为总有租户的配额闲置(夜间、周末、实验间隙)。
面试中怎么答:不要说"固定配额不好"——要说"固定配额在什么场景下够用":小型团队(GPU 少,每个租户都满载)、合规要求严格(不能混用资源的场景)。问题在于规模大了之后利用率不可接受。
2. ElasticQuota(弹性配额)
定义:每个租户有 min(保障量)和 max(上限)。日常使用 min 以内的资源有保障;闲置时可以借用其他租户的 min 值,但总量不超过 max;被借用方需要时可以抢占回收。
怎么理解:信用卡 + 信用额度——你有 5 万信用额度(min = 5 万保障),最多可以刷 10 万(max = 10 万上限),但超出 5 万的部分银行随时可以收回。
手动推演:100 GPU 集群,A 团队 min=30/max=60,B 团队 min=40/max=80
- T1:A 用 30,B 用 40,剩 30 闲置 → A 可以借用,用 60(到 max)
- T2:B 要用 70 → A 借用了 30 中的 30 需要让出 → 抢占 A 的 30 GPU → A 回到 30(min),B 用 70
- T3:B 只用 40 → 剩 60 闲置 → A 又可以借用,用 60
关键机制:抢占回收。当 min 拥有者需要资源但被借用者占用时,借用者的 Pod 会被抢占。抢占策略影响很大——简单的优先级抢占可能杀掉一个训练了 10 小时的任务。
局限:(1) min/max 是静态值,不能动态调整;(2) 抢占不考虑沉没成本;(3) 没有"保障度"的概念——min 只是名义保障,实际保障度取决于抢占的及时性。
3. 弹性保障(QAD 驱动)
定义:不设 max 上限,借用不受限,但通过 QAD(Quota Assurance Degree,配额保障度)来持续监控和保障每个租户的权益。
QAD 是什么:QAD = 实际获得资源时间 / 应获得资源时间。例如一个租户的 guarantee 是 30 GPU,过去 24 小时内他需要 30 GPU 的总时间是 20 小时,其中 18 小时确实获得了 30 GPU,则 QAD = 18/20 = 0.9。
怎么理解 QAD:像手机电池健康度。你的电池设计容量是 100%,实际充满只有 90%,电池健康度就是 90%。QAD 就是"配额健康度"——你的保障配额有多少时间是真正可用的。
为什么比 ElasticQuota 更好:
- 无 max 限制:借用没有硬上限,利用率更高。只要 QAD 不降低,你借多少都行。
- 信号更精细:ElasticQuota 只有"在 min 内 / 超出 min"两个状态。QAD 是连续值(0.0-1.0),能做更精细的调度决策——QAD=0.95 的队列和 QAD=0.6 的队列,优先级显然不同。
- 驱动回收:当某个租户的 QAD 低于阈值(如 0.95),调度器优先从借用者回收资源,而不是等到租户主动请求。
手动推演:100 GPU,A 团队 guarantee=30,B 团队 guarantee=40
- T1:A 用 30,B 用 40,剩 30 闲置 → A 借用 30,A 总共 60 GPU
- T2:A 的 QAD = 1.0(一直在 30 以上),B 的 QAD = 1.0 → 一切正常
- T3:B 需要用 60 但只有 40(A 借了 30)→ 调度器检测到 B 的 QAD 可能降到 0.67 → 触发回收 A 的 20 GPU → A 回到 40,B 获得 60
- 关键:A 不一定要退回 guarantee=30,只要 B 的需求被满足就行。A 保留了 40(比 guarantee 多 10),B 获得了 60(比 guarantee 多 20)。总量 100 刚好用满。
面试金句:"ElasticQuota 是离散的保障信号——要么在 min 内要么超出。QAD 是连续的保障信号——像监控'配额健康度'一样持续度量保障水平,驱动更精细的调度决策。"
4. DRF(主导资源公平)
定义:多维资源场景下的公平分配。每个租户的"主导资源"(占集群该资源比例最高的那个维度)获得相等份额。
在多租户中的角色:DRF 解决的是公平性,不是保障性。它没有 min/max 的概念,只是确保每个租户的"最大需求维度"被公平对待。适合无保障要求的共享集群。
配额方案对比
| 维度 | 固定配额 | ElasticQuota | QAD 驱动 | DRF |
|---|---|---|---|---|
| 保障性 | 强(100%) | 中(min 保障,抢占可能延迟) | 强(QAD ≥ 0.95) | 弱(无保障承诺) |
| 弹性 | 无 | 中(max 上限) | 强(无上限借用) | 中(按比例分配) |
| 回收机制 | 不需要 | 抢占(简单优先级) | QAD 驱动回收(更精细) | 不需要 |
| 公平度量 | — | — | QAD 连续值 | Dominant Share |
| 利用率 | 低(30-40%) | 中(60-70%) | 高(80%+) | 中高 |
| 实现复杂度 | 低 | 中 | 高 | 中 |
| 适用规模 | 小 | 中 | 大 | 共享研究集群 |
多租户隔离的四个层次
配额管理解决的是"每个租户能用多少",隔离解决的是"租户之间互相不影响"。隔离有四个层次,从粗到细:
第一层:Namespace 级别
机制:K8S 原生的 ResourceQuota + LimitRange。
ResourceQuota:限制一个 Namespace 的总资源量(如最多 16 GPU)。硬限制,超了 Pod 创建直接被拒绝。
LimitRange:限制单个 Pod 的资源范围(如每个 Pod 最多 4 GPU,最少 1 GPU)。防止一个 Pod 吃掉整个 Namespace 的配额。
隔离能力:资源量隔离。不同 Namespace 的资源预算独立。
局限:(1) 不支持弹性——ResourceQuota 是硬限制,空闲资源不能被其他 Namespace 用;(2) 不做性能隔离——同一节点上两个 Namespace 的 Pod 可能互相干扰。
怎么理解:公司报销制度——每个部门有固定报销额度(ResourceQuota),单次报销有上下限(LimitRange),但额度用不完不能转给其他部门。
第二层:Queue 级别
机制:Volcano Queue / Kueue ClusterQueue / Yunikorn Queue。
增强能力:(1) 弹性配额——支持 borrowing 和 reclaim;(2) 公平调度——Queue 内部可以按 DRF/proportion 调度;(3) 优先级——不同 Queue 可以有不同优先级;(4) 排队——配额不足时任务排队等待,而不是直接拒绝。
为什么比 Namespace 更好:ResourceQuota 是"硬墙"——超了就拒绝。Queue 是"弹性门"——超了可以借用,还可以排队等。后者更适合 GPU 集群的动态负载。
第三层:节点级别
机制:NodeSelector / NodeAffinity / Taint-Toleration。
做法:把特定节点(或节点组)专属于特定租户。例如给 A 团队的节点打上 tenant=A:NoSchedule taint,只有带 A 团队 toleration 的 Pod 才能调度上去。
为什么需要:(1) 合规要求:某些数据只能在特定机器上处理;(2) 性能隔离:训练任务独占节点,避免推理任务的延迟抖动;(3) 硬件差异:A 团队的模型需要 A100,B 团队 V100 就够了。
局限:降低利用率——专属于某个租户的节点即使闲置,其他租户也不能用。
第四层:GPU 级别
机制:MIG(Multi-Instance GPU)硬件切片 / MPS(Multi-Process Service)软件共享。
MIG:A100/H100 支持将一张 GPU 硬件切分为最多 7 个实例,每个实例有独立的 SM、L2 cache、显存带宽。硬件级隔离——一个实例的故障和性能波动不影响其他实例。
MPS:软件层面的 GPU 共享,多个进程共享同一 GPU 的 SM。轻量级,但没有硬件隔离——一个进程的 kernel 可能影响其他进程的延迟。
怎么理解:MIG 像"合租公寓的独立房间"——各有各的卧室和卫生间。MPS 像"合租公寓的公共空间"——共享客厅和厨房,一人做饭另一人可能要等。
选择建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 训练任务(需要强隔离) | MIG 或独占 | 性能波动不可接受 |
| 轻量推理(延迟不敏感) | MPS | 利用率高,隔离要求低 |
| 数据预处理 + 训练混跑 | MPS | 预处理是 I/O 密集,GPU 利用率低,可以和训练共享 |
| 合规要求(数据不能混) | MIG | 硬件级隔离满足合规 |
多租户管理面试问答
ElasticQuota 的抢占是简单的优先级抢占——当 guarantee 拥有者需要资源时,驱逐借用者中优先级最低的 Pod。三个问题:
- 不考虑沉没成本:一个训练了 20 小时的任务和一个刚启动 5 分钟的任务,如果优先级相同,可能抢占前者。前者的进度损失远大于后者。
- 抢占延迟:从触发抢占到实际释放资源可能需要几分钟(优雅终止期 + checkpoint + 资源清理),在此期间 guarantee 拥有者一直在等。
- 级联抢占:抢占 A 的资源可能不够,还需要抢占 B 的,B 又依赖 C 释放的节点……级联效应导致调度不可预测。
(1) 代价感知抢占:优先抢占沉没成本低的任务(运行时间短、checkpoint 新鲜)。参见调度理论模块的 checkpoint-aware preemption。(2) 预回收:基于 QAD 信号,在 QAD 接近阈值时提前触发回收,而不是等到 guarantee 拥有者来请求时才抢占。(3) 优雅终止:通知借用者"请 checkpoint 后退出",给 5 分钟优雅期,而不是直接杀 Pod。(4) 弹性缩容:如果任务支持弹性训练,缩减 world size 而非杀掉整个任务。
"抢占不是免费的——它有沉没成本、延迟和级联效应。好的配额系统应该让抢占尽量少发生、尽量低代价、尽量可预测。"
(1) 配额模型:选择 ElasticQuota(min/max)或 QAD 驱动。10 个团队 + 200 GPU 规模下,ElasticQuota 可以工作,但 QAD 驱动利用率更高。建议 QAD 驱动 + guarantee 保障。
(2) 保障量分配:根据团队历史使用量和业务优先级分配 guarantee。例如核心训练团队 guarantee=40,实验团队 guarantee=10。总和应 ≤ 集群总量(200),保证不超卖。
(3) 借用和回收:借用无上限(或设安全上限),回收由 QAD 驱动——任何团队 QAD < 0.95 时触发回收。回收策略用代价感知抢占。
(4) 隔离层次:Namespace 级别(ResourceQuota 兜底)+ Queue 级别(Volcano/Kueue 管理)+ 节点级别(大团队专属节点组)。
(5) 监控:实时 QAD 看板、借用/回收日志、每团队利用率趋势。
"配额系统设计的关键不是选哪个模型,而是说清楚 guarantee 怎么定、借用怎么管、回收怎么触发、代价怎么控制。这四个问题回答清楚了,配额系统就立住了。"
| 维度 | MIG | MPS |
|---|---|---|
| 隔离级别 | 硬件(独立 SM、L2、显存带宽) | 软件(共享 SM,MPS server 调度) |
| 实例数 | 最多 7 个(A100) | 无硬限制 |
| 性能隔离 | 强——一个实例不影响其他 | 弱——一个进程的 kernel 可能阻塞其他 |
| 故障隔离 | 强——一个实例故障不影响其他 | 弱——一个进程崩溃可能影响 MPS server |
| 利用率 | 中(静态切分,可能浪费) | 高(动态共享) |
| 灵活性 | 低(切分比例预设,运行中不能调) | 高(随时加/减共享进程) |
| GPU 型号 | A100/H100 | 大部分 NVIDIA GPU |
(1) 训练 + 训练共享 → MIG。训练任务对性能波动敏感,需要硬件隔离。(2) 推理 + 推理共享 → MPS。推理任务可以利用 MPS 的高利用率。(3) 训练 + 数据预处理 → MPS。预处理是 I/O 密集,GPU 空闲时间多,MPS 让训练利用这些空闲。(4) 合规/多租户隔离 → MIG。硬件隔离满足合规要求。
"MIG 用硬件换隔离,MPS 用共享换利用率。选择取决于你对性能确定性的要求——训练需要确定性,推理更看重利用率。"
"吵闹的邻居"(Noisy Neighbor)指同一物理资源上,一个租户的高负载影响其他租户的性能。在 GPU 集群中主要表现为:
- 共享节点上的 GPU 争抢:MPS 共享时,一个大 kernel 占满 SM,其他进程排队。
- 网络带宽争抢:训练任务的 AllReduce 占满 InfiniBand 带宽,推理任务的网络延迟飙升。
- 存储 I/O 争抢:checkpoint 写入占满存储带宽,其他任务的数据加载变慢。
(1) 隔离:节点级隔离(不同租户不同节点)或 GPU 级隔离(MIG 硬件切分)。这是最彻底但最浪费资源的方案。(2) 干扰感知调度:调度器感知任务间的性能干扰,避免把"吵闹"的任务和"敏感"的任务放一起。需要性能模型来预测干扰程度。(3) 资源限流:对 GPU 使用率、网络带宽、存储 I/O 设置 cgroup 限制,防止单个租户占满共享资源。(4) 监控 + 自动迁移:检测到干扰时,自动将"受害者"迁移到其他节点。
"吵闹邻居的本质是共享资源的争抢。解决路径从粗到细:隔离(不共享)→ 干扰感知(有选择地共享)→ 限流(共享但约束)→ 监控(出问题再处理)。"
Namespace 隔离是 K8S 原生的"硬墙"——ResourceQuota 限制总量,超了直接拒绝。Queue 隔离是批调度框架的"弹性门"——配额不足时排队等待或借用,而不是直接拒绝。
| 维度 | Namespace + ResourceQuota | Queue |
|---|---|---|
| 超配额行为 | 直接拒绝 Pod 创建 | 排队等待或借用 |
| 弹性 | 无(硬限制) | 有(borrowing/reclaim) |
| 公平性 | 无调度公平性 | DRF/proportion 公平调度 |
| Gang 支持 | 不支持 | 支持(PodGroup/Workload) |
| 多维度资源 | 支持(CPU/GPU/内存各自限制) | 支持(且更灵活) |
(1) 只用 Namespace:微服务场景,没有 Gang 需求,每个 Namespace 的负载相对稳定。(2) 只用 Queue:训练场景,需要 Gang + 弹性配额 + 公平调度。(3) 两者结合(推荐):Namespace 做身份隔离和 RBAC(谁能访问什么),Queue 做资源治理和调度。Namespace 是"权限边界",Queue 是"资源边界"。
"Namespace 解决'谁能做什么'(权限),Queue 解决'能用多少资源'(调度)。它们不是替代关系,而是互补——Namespace 做安全边界,Queue 做弹性治理。"