AI Infra 全景复盘:GPU 性能、分布式训练与调度层深度解析
AI Infra 全景复盘:GPU 性能、分布式训练与调度层深度解析
本文以一次 AI Infra 岗位一面为线索,围绕 GPU 性能分析、分布式训练、RayJob 弹性伸缩、GPU 混部与调度可行性判断五大模块展开;再把面试中经常被追问的调度层作为独立章节补齐——抢占、队列、调度器框架、DRF、调度器与 Node 的解耦关系、多调度器冲突、backfill/preempt/reclaim 的跨队列语义,一次讲透。
核心观点先抛出来:Infra 面试考的不是名词背诵,而是"瓶颈定位 → 决策依据 → 工程权衡"这条完整链路的推理能力。调度层的所有机制,本质上都在"分布式一致性"和"资源效率"之间做权衡。
一、GPU 性能与分布式训练:Nsight 与并行策略
1.1 Nsight 是什么、怎么用
Nsight 是 NVIDIA 提供的 GPU 性能分析工具套件,其中最核心的是两把"不同倍率的放大镜":
| 工具 | 定位 | 分析粒度 | 典型命令 |
|---|---|---|---|
| Nsight Systems (nsys) | 全系统时间线分析 | CPU 线程、CUDA API、kernel 启动、内存拷贝、流活动 | nsys profile -o out ./app |
| Nsight Compute (ncu) | 单 kernel 深度分析 | Occupancy、SM 利用率、各级存储带宽、warp stall 原因 | ncu --set full -o report ./app |
| 两者的关系是递进式的定位流程,不是二选一。 | |||
| 标准的瓶颈定位三步走: |
- 先用 Nsight Systems 看时间线:判断 GPU 到底是在算还是在等。典型信号包括 kernel 之间的大段空白(GPU 饥饿,说明 CPU 侧数据准备或 Python 逻辑是瓶颈)、
cudaMemcpy占比过高(PCIe 传输成为瓶颈)、以及本可以重叠却串行执行的 H2D 拷贝与 kernel。 - 加 NVTX 打标:在训练循环里用
nvtx.range_push("forward")等标记关键阶段,让时间线可读性大幅提升,这是生产环境必备技巧。 - 再用 Nsight Compute 深挖热点 kernel:对 nsys 里最耗时的几个 kernel 做 SpeedOfLight、Occupancy、Memory Workload 分析,判断到底是 compute-bound、memory-bound 还是 latency-bound,再对症下药。
一个容易被忽视的细节:ncu会把同一个 kernel 多次重放以采集完整指标,对训练任务侵入性很强,生产环境通常要用-k <regex>和--launch-count限定采集范围。
1.2 分布式训练:显存、通信与拓扑的三角关系
面试官问"对分布式训练有哪些了解",本质上是在考察能否把并行策略 → 通信模式 → 显存占用 → 网络拓扑这条因果链讲清楚。当前主流的并行维度有六种,关键差异如下:
| 并行方式 | 切分维度 | 核心通信操作 | 通信频次 | 显存收益 | 拓扑要求 |
|---|---|---|---|---|---|
| DP(数据并行) | Batch 维度 | AllReduce 梯度 | 每步 1 次,可与 backward 重叠 | 无(每卡完整模型) | 对延迟容忍度最高 |
| TP(张量并行) | 层内权重矩阵 | AllReduce | 每层 forward+backward 都要做 | 参数/激活均分 | 必须放在 NVLink 域内,通常节点内 |
| PP(流水线并行) | 层间切分 | P2P 传激活 | 每阶段边界 1 次 | 按层数均分 | 对带宽要求最低,适合跨节点 |
| EP(专家并行) | MoE 专家 | All-to-All | 每层 router 分发时 | 专家参数均分 | 需网络拓扑支持大带宽 |
| CP(上下文并行) | 序列维度 | Ring Attention | 注意力内部 | 激活按序列均分 | 长上下文必备 |
| SP(序列并行) | 序列+LayerNorm/Dropout | 配合 TP 的通信 | 与 TP 相当 | 减少 TP 激活冗余 | 与 TP 同域 |
| 几个典型的"题眼": |
- 为什么 TP 通常限制在 8-16 卡(节点内)? TP 的 AllReduce 发生在每层 forward 和 backward,消息小、频次极高,一旦跨节点走 IB,小消息的延迟开销会被急剧放大。实测显示 TP 跨 IB 比域内 NVLink 慢 7-50 倍,小消息场景接近 50 倍。
- 为什么 DP 不能单独使用? DP 不减少每卡显存,每张卡都要装下完整模型、梯度和优化器状态,所以大模型必须配合 TP/PP,或用 ZeRO 把优化器状态、梯度、参数分片。
- 拓扑对 AllReduce 的影响:同一个 8 卡节点内,GPU0-GPU1 走 NVLink(450 GB/s 级)与 GPU0-GPU4 跨 NUMA 走 QPI 的带宽差一个量级;调度器若不感知拓扑,会把一个 NVL4 域的作业拆到两个 NUMA 上,等效带宽直接腰斩。
回答套路总结:讲清每种并行"切什么、怎么通信、省多少显存、对拓扑什么要求",再给一个组合案例(例如:TP 在节点内 + PP 跨节点 + DP 外层挂 ZeRO-1,MoE 再叠 EP),就能体现体系化理解。
二、弹性扩缩容:RayJob 弹性的原理与业务权衡
2.1 为什么支持 RayJob 弹性
RayJob 的弹性由 Ray Autoscaler 驱动,关键机制是:它依据逻辑资源请求(@ray.remote 里声明的 CPU/GPU 需求)而非物理机利用率来扩缩。工作流程是:
- 用户提交 Ray task/actor/placement group;
- 资源不足时请求入队,Autoscaler 侧感知到 pending demand;
- Autoscaler 增加 RayCluster CR 的
replicas字段,KubeRay Operator 创建新的 Worker Pod; - Pod 就绪后 Ray 调度器把工作负载放置上去;
- 节点空闲超过
idleTimeoutSeconds后被移除。
在 KubeRay 里实际存在三层弹性,理解分层很重要:
- Ray actor/task 层:Ray Serve 根据请求量自动调整副本数;
- Ray 节点层:Autoscaler 调整 Ray Worker Pod 数量;
- Kubernetes 节点层:K8s Cluster Autoscaler 为新 Pod 供给物理节点。
2.2 什么时候扩、什么时候缩
扩容时机:出现 pending task/actor/placement group 时。这是逻辑层需求,不代表 CPU 真的满了,可能只是资源碎片化或亲和性约束导致调度不进去。
缩容时机:节点进入"空闲"状态(没有活跃 task/actor/object)且持续超过 idle 超时。Autoscaler 会把该 Pod 加进 workersToDelete 由 KubeRay Operator 删除。
推理服务为什么夜间缩容? 背后的业务逻辑是夜间流量低谷 + 离线训练任务潮汐需求:
- 在线推理有明显的日周期,白天峰值、深夜谷值,峰均差可达 30 个百分点;
- 离线训练任务全天需求旺盛,夜间是它抢到卡的最佳窗口;
- 缩容后释放的 GPU 可以让离线训练/批处理任务上卡,实现"潮汐混部",整体提升资源效率。
面试常见误区:只看 GPU 利用率决定扩缩。正确做法是从业务需求和阶段瓶颈出发,说清楚扩缩对象(是 Ray Worker 还是 K8s Node?是推理副本还是训练 Worker?)、扩缩的安全条件(推理缩容要考虑请求排空、连接排水、模型加载成本)以及冷启动代价(新 Pod 拉镜像、模型加载、Ray Worker 注册可能要几分钟,频繁扩缩反而放大成本)。
三、混部:资源共享的层级、隔离与回收
混部的本质是让在线服务(延迟敏感、潮汐性强、平时利用率低)与离线任务(容错性好、吞吐型、全天可跑)共享同一批物理资源,用隔离和调度机制把互相干扰压到可接受范围。
3.1 共享资源的层级
以快手的生产实践为例,典型分层是:
| QoS 等级 | 供给保障 | 适用负载 |
|---|---|---|
| LSE / LSR(在线高优) | 独占 CPU 核、严格保障 | 在线核心服务、延迟敏感推理 |
| LS / Mid | 基于服务画像和 AI 资源预测的天级别保障 | 近线任务、Flink、特征抽取 |
| BE / Free | 基于单机水位超发,允许压缩与退避 | 离线批处理、异步转码、测试任务 |
3.2 隔离方式
GPU 混部的隔离粒度比 CPU 复杂得多,核心手段有三种,各自作用于不同层面:
| 隔离技术 | 作用层面 | 隔离强度 | 典型用途 |
|---|---|---|---|
| MIG(Multi-Instance GPU) | 硬件级,显存+SM 硬切 | 最强,物理隔离 | A100/H100 上的多租户严格隔离 |
| MPS(Multi-Process Service) | 软件级,SM 份额限制 | 中等,算力软隔离 | 训练与推理同卡混跑的算力配比 |
| Time-Slicing | 时间片轮转 | 最弱,无显存隔离 | 低成本共享,接受延迟毛刺 |
| 需要注意两个硬约束:显存是硬边界,算力是软边界。MIG 只支持 A100/H100 等特定型号,4090、L40S 等卡只能靠 MPS/时间片做软隔离,且 MPS 的显存隔离并不完美,仍有 OOM 风险。CPU 侧则靠 cgroups v2 做细粒度控制,配合 NUMA 绑定避免跨 socket 访问拖慢关键路径。 |
3.3 优先级与回收策略
- PriorityClass + Preemption:定义 P0 Training / P1 Serving / P2 Batch / P3 Interactive 四档,高优可抢占低优;关键是要给交互类任务设置
preemptionPolicy: Never,避免级联驱逐。 - 资源紧张时的退避:压力状态触发时,离线任务按 QoS 优先级顺序自动退避,超阈值即被压制甚至驱逐;前提是底层任务支持 checkpoint 恢复,否则驱逐代价过高。
- 保障关键负载:高优服务必须有 PDB(PodDisruptionBudget)保护,同时给低优任务加弹性边界(降级而非驱逐),训练任务被抢占后从 checkpoint 继续而非从头重跑。
回答套路:讲清"哪几层资源可以共享 → 每层用什么隔离 → 优先级怎么定义 → 资源紧张时谁让路、怎么让路 → 让路后怎么恢复",这四段逻辑缺一不可。
四、扩容可行性:Quota、DryRun 与放置约束
这是最容易被面试者"答得太浅"的一道题。核心结论先行:Quota 通过 ≠ 能调度成功,DryRun 通过 ≠ 有资源预留。需要区分三层概念:额度(Quota)→ 容量(Cluster Capacity)→ 实际放置(Placement)。
4.1 Quota 校验做什么
Kubernetes 的 ResourceQuota 工作在 Admission 阶段,在 namespace 级别限制 requests.cpu/memory、limits.cpu/memory、Pod 数量、PVC 数量等聚合值。它只回答一个问题:“这个 namespace 的累计申请量会不会超配额?”
关键局限:Quota 校验完全不看节点层面有没有真实资源。namespace 配额没用完,不代表集群里能找到一台满足需求的节点。
4.2 DryRun 做什么
kubectl apply --dry-run=server 会在 API Server 上执行完整的准入链(认证 → 授权 → Mutating Webhook → Schema 校验 → Validating Webhook → ResourceQuota),但不持久化对象、不触发 controller。
一个重要的坑:Deployment 的 dry-run 不会创建 ReplicaSet 和 Pod,所以它只验证 Deployment 对象本身合法,后续 Pod 是否能通过 Quota、是否能被调度,DryRun 完全不保证。要做真正的扩容前校验,需要把 Pod 模板单独提取出来做一次 Pod 级别的 server dry-run,再叠加 Quota 剩余量聚合计算。
4.3 RayJob 是否有亲和约束
有,而且比一般工作负载更严格。关键约束有四类:
① Gang Scheduling(成组调度)。Ray 的 head 和 worker 构成一个整体,若只调度一部分 Pod 会造成两个作业各占一半资源但都无法运行。通过 Volcano 的 PodGroup 机制,一组 Pod 要么全部放置、要么全部等待,minMember 和 minResources 定义了最低门槛。KubeRay 通过 --batch-scheduler=volcano 启用该能力。
② 拓扑亲和(Topology Affinity)。TP 组必须放在同一个 NVLink 域内,DP 组可跨 IB。调度器需要感知节点内 NVLink/PCIe/NUMA 拓扑与节点间 RDMA 网络,通过 GPU Feature Discovery 标签 + kubelet Topology Manager + RDMA device plugin 组合实现。
③ 资源碎片。8 卡作业要找一台 8 卡连续空闲的节点,若集群里只剩 4 台各有 2 卡空闲的节点,即便总量够也无法放置;GPU 尤其敏感,因为原生 K8s 把 GPU 当作不可分割的整数资源。
④ 卡型匹配。H100 与 A800 不能混用,MIG 切分后的实例规格要匹配,这些都要通过 nodeSelector、taint/toleration 或自定义调度插件表达。
4.4 资源状态变化怎么办
扩容决策通过后,集群状态仍在变化:新 Pod 不断加入可能挤压后续放置、节点故障会让原本可行的方案失效、低优任务被抢占可能释放资源。工程上的做法是:
- 扩容走声明式 API + 控制器 reconcile,而不是一次性决策;
- 批量扩容时使用 quota reservation(如 Kueue、YuniKorn 的 reserve 机制)避免多个作业同时判定"能放下"却互相冲突;
- 对长周期任务做周期性 re-validate,状态变化时重新走一遍预选。
回答套路:先把"额度够 ≠ 容量够 ≠ 能放下"三层拆开,再按"Quota → DryRun → Gang → 亲和 → 碎片 → 拓扑"顺序展开,最后提一下状态变化的处理,基本能把这道题答满。
五、检查成本:检查一大堆会不会很慢?
这是一道典型的工程权衡题。面试官想听的不是"会/不会",而是:检查覆盖哪些维度、每项耗时多少、是否并行、如何兜底、总体权衡在"提前发现问题"和"启动延迟"之间。
5.1 检查会覆盖什么
扩容或调度前的检查链路通常包括:
- API 层面:Schema 校验、Mutating/Validating Webhook;
- 配额层面:ResourceQuota、LimitRange、PDB 冲突;
- 调度层面:资源余量、亲和/反亲和、污点容忍、拓扑约束、Gang 成员;
- 运行时层面:镜像是否存在、存储是否可挂载、网络策略是否放通。
5.2 耗时与超时怎么定
单次检查的耗时分布大致是:API 准入毫秒级、Quota 校验毫秒级、调度预选/优选在大集群下可达百毫秒级、Webhook 调用视实现可能到几十毫秒。真正的瓶颈往往在:
- 大规模集群的调度器预选:每个 Pod 要对所有节点跑一遍 filter + score,10000 节点集群即使单节点评估只要几十微秒,累计也可观;
- Gang 调度的等待:PodGroup 需要所有成员资源满足才放行,minMember 很大时等待时间可能到分钟级;
- 镜像拉取与模型加载:一个 40GB 的大模型镜像冷启动可能要几分钟,这部分不是"检查"但占启动大头。
超时设置需要分级:单次检查给秒级超时快速失败,整体扩容流程给分钟级总预算,避免某一个检查点卡死整个链路。
5.3 并行与优化手段
- 调度器并行预选:K8s 1.33 起默认对 500+ 节点集群启用并行 filtering,把节点池分批并发评估;
- 百分比预选:
percentageOfNodesToScore让调度器在找到足够可行节点后提前结束,10 万节点集群设置评估 30% 就能显著降低延迟; - 插件短路:让明显不匹配的节点尽早返回失败,避免浪费完整评估;
- Webhook 轻量化:混部/配额类 Webhook 要严格控制 P99 延迟,否则会拖慢整个准入链;
- 检查项分层:必检项(Quota、Schema)与可异步项(镜像预热、网络预检)分开,同步路径只保留最小集合。
5.4 故障归因与兜底
检查失败要有清晰的归因:是 Quota 不足、无可用节点、亲和性冲突还是 Webhook 拒绝?每种原因对应不同处置(扩 Quota、扩节点池、调整作业规格、修 Webhook)。检查通过后还要有事后观测——如果实际放置仍然失败,要有事件与指标能快速定位到是哪个 plugin 在哪个扩展点拒绝的。
回答套路:按"覆盖范围 → 单项耗时 → 总体预算 → 并行手段 → 故障归因 → 权衡(提前发现 vs 启动延迟)“这条链路展开,最后给一个具体数字(例如"一次完整的扩容预检控制在 3 秒内,镜像预热走异步”)会显得非常扎实。
六、调度层深潜:抢占、队列、调度器、DRF 与跨队列语义
前五章是面试的"基础盘",而调度层才是拉开差距的地方。这一章把调度层的核心机制一次讲透。
6.1 抢占机制:谁让路、怎么让、代价谁付
PriorityClass:抢占的语言基础
抢占的前提是 Pod 有优先级,而优先级通过 PriorityClass(集群级资源)表达。它本质上是一个"名字 → 32 位整数"的映射,用户可定义的范围是 -2147483648 ~ 1000000000,超过 10 亿的值预留给系统组件(system-node-critical = 2000001000、system-cluster-critical = 2000000000)。
一个完整的 PriorityClass 有四个关键字段,每个都对应一个工程决策:
| 字段 | 作用 | 工程含义 |
|---|---|---|
value |
优先级数值,越大越优先 | 决定队列排序顺序和抢占资格 |
globalDefault |
未指定 priorityClassName 时的兜底 | 生产环境必须设一个默认类,避免优先级 0 |
preemptionPolicy |
PreemptLowerPriority / Never |
交互类任务通常设 Never,防止抢占风暴 |
preemptionPolicy 缺省 |
默认 PreemptLowerPriority |
所有 Pod 默认都能抢占别人 |
| 一个常被忽略的细节:ResourceQuota 可以限定每个 namespace 能创建多高优先级的 Pod,这是防止多租户场景下"用户把自己的作业全标成 P0"的关键治理手段——Ai2 的案例中,正是因为缺乏这种限制,最终"集群内 100% 的提交任务全部变成了紧急状态,多级优先级彻底失效"。 |
kube-scheduler 的抢占流程
抢占在调度框架里由 PostFilter 扩展点触发——只有当 Filter 阶段所有节点都被淘汰时,DefaultPreemption 插件才会启动,去问一个问题:"如果驱逐掉某些低优 Pod,这个 Pod 能不能放进去?"整个流程分四步:
- 筛选候选节点:找出"假如移除若干低优 Pod 就能放下目标 Pod"的节点集合;
- 选择 victims:在候选节点集合里计算"驱逐代价"最小的组合,优先考虑优先级最低、最近被调度、打断的 PDB 最少的组合;
- 删除 victims:scheduler 只是把 victim Pod 打上删除标记(通过 API Server 走 deletion + graceful termination),真正终止由 kubelet 完成;
- 下一轮重试:victims 优雅退出后,原本 pending 的高优 Pod 在下一个调度周期成功放置。
关于 victim selection 的三个细节,面试里很容易被追问:
- 优先级不低于待调度 Pod 的 Pod 永远不会被驱逐,这是抢占的底线规则;
- PDB(PodDisruptionBudget)只是"best-effort"——scheduler 会尽量避免打破 PDB,但为了放一个系统级关键 Pod,它允许打破;
- 跨 namespace 的抢占要求:只有当 victim Pod 的优先级低于抢占者、且抢占者有足够优先级跨过 namespace 边界时,才会发生跨 ns 抢占,这是防止多租户互相踩踏的护栏。
抢占的三种形态:抢占 vs 回收 vs 驱逐
很多面试题把这三个概念混为一谈,其实在 AI Infra 场景下它们对应不同的调度器动作,严格区分才能答对:
| 机制 | 触发条件 | 调用方 | 典型场景 |
|---|---|---|---|
| 抢占 | 同队列内高优 Pod 放不下 | Volcano 的 preempt action / Kueue 的 InClusterQueue preemption |
同一团队内 P0 训练任务挤掉 P2 实验 |
| 回收 | 队列使用量超过 deserved,其他队列资源紧张 | Volcano 的 reclaim action / Kueue 的 reclaimWithinCohort |
借了别队 GPU 的作业被"还回去" |
| 驱逐 | 节点压力、taint 变化、PDB、硬件故障 | kubelet eviction / node controller | 内存压力下的节点级驱逐 |
| Kueue 的抢占算法尤其值得展开。它有"经典抢占"和"公平共享"两种模式,核心区别在于能否跨 ClusterQueue 抢占: |
- 经典抢占:只有当抢占方的 ClusterQueue 用量(在接纳后)低于 nominalQuota,或者所有候选都属于同一个 ClusterQueue 时才允许抢占。换句话说,ClusterQueue 不能靠抢占别人来维持自己的"借出状态",这是"先到先得"语义;
- 公平共享:允许有 pending 工作负载的 ClusterQueue 去抢占 cohort 内其他 ClusterQueue 的工作负载,直到自己拿到与权重相称的公平份额。
抢占候选的排序规则是:先挑"借了配额的队列里的工作负载"→ 再挑"优先级最低的" → 最后挑"最近被接纳的",这个顺序背后的逻辑是尽量让借出去的资源先回家、让最早被误接纳的先退场。
AI 场景的抢占治理:比机制更重要的问题
在 AI 集群里,抢占的成本远高于普通无状态服务——一次抢占可能意味着要写几十 GB 的 checkpoint、重拉模型、重建 NCCL 通信组。Ai2 在数千张 H100 上的实践给出了一个反直觉的结论:与其堆优先级,不如用"时间预算 + 分时契约":
- 给每个任务声明"最短受保护运行时长",保护窗口内不可被抢占;
- 窗口结束后系统有权将其挂起重排;
- 声明"最短时长为零"的任务不扣预算,代价是随时可被驱逐;
- 配合"抢占退避惩罚",频繁触发抢占的作业会被临时降级。
这套设计的核心洞察是:抢占风暴的本质不是技术问题,是博弈问题——当抢占零成本时,每个人都会把自己的任务标成最高优,多级优先级就形同虚设。治理抢占必须在机制之外叠加经济激励(预算扣减)和时间约束(保护窗口)。
6.2 队列机制:Pod 排队与作业排队的两层世界
kube-scheduler 的三队列模型
调度器内部的队列不是 FIFO,而是三个子队列 + 两个后台刷写协程构成的状态机:
| 队列 | 作用 | 排序规则 |
|---|---|---|
| ActiveQ | 待调度的 Pod,scheduler 每次从这里 pop | 默认按优先级降序,同级按创建时间升序;可通过 QueueSort 插件自定义 |
| PodBackoffQ | 调度失败、处于指数退避期的 Pod | 每次失败退避时长翻倍(1s、2s、4s……),退避结束自动回 ActiveQ |
| UnschedulableQ | 反复调度失败、等待集群状态变化的 Pod | 等 flushUnschedulableQLeftover 每 30s 刷一次,或等特定事件触发唤醒 |
| 两个后台协程的节奏值得记住:flushBackoffQCompleted 每秒跑一次,把退避完成的 Pod 挪回 ActiveQ;flushUnschedulableQLeftover 每 30 秒跑一次,兜底把长期无人问津的 Pod 重新尝试(最坏情况一个 Pod 要在 UnschedulableQ 里等 60 秒)。 | ||
事件驱动的队列迁移是这套机制最精巧的部分:当节点加入、节点更新、Pod 删除、PV 创建等事件发生时,scheduler 会调用 MoveAllToActiveOrBackoffQueue 把相关 Pod 从 UnschedulableQ 批量搬回 ActiveQ——这就是为什么"删掉一个别的 Pod 之后,之前一直 Pending 的任务突然就跑起来了"。 |
QueueSort:队列的排序规则
QueueSort 是调度框架的一个扩展点,决定 ActiveQ 内 Pod 的相对顺序。默认实现是优先级降序、创建时间升序,高优先调度、同级 FIFO。对于需要 Gang 语义的调度器,QueueSort 会被重写为按 PodGroup 的优先级和创建时间排序,保证同一 Gang 的 Pod 在队列里相邻,配合 Permit 扩展点实现"凑齐再放行"。
从 Pod 队列到作业队列:为什么要升级
kube-scheduler 的队列粒度是单个 Pod,而 AI 场景需要作业级队列。这个差异在多团队共享 GPU 集群时会爆炸性地放大:
- 没有配额:一个团队可以吃光整个集群;
- 没有排队语义:新提交的作业直接抢资源,早提交的作业可能饿死;
- 没有公平性:先来先服务不等于公平,DRF(Dominant Resource Fairness)才是多资源维度的公平解。
所以 Volcano / Kueue / YuniKorn 的本质是把队列从"Pod 排队"升级到"Workload 排队",每个 Workload 带着完整的资源请求被纳入队列治理。三者的队列模型对比如下:
| 维度 | Volcano | Kueue | YuniKorn |
| ------------------------------------------------------------ | -------------------------------------- | -------------------------------------------- | --------------------------------------- |
| 核心 CRD | Queue + PodGroup | ClusterQueue + LocalQueue + Workload | QueueInfo + hierarchical Queue |
| 队列结构 | 多级队列(root → 子队列 → 叶子) | ClusterQueue + Cohort(共享配额池) | 严格的层级树,YARN 风格 |
| 配额语义 | guarantee ≤ deserved ≤ capability 三级 | nominalQuota + borrowingLimit + lendingLimit | guaranteed + max |
| 公平算法 | proportion(weight)/ capacity 插件 | Fair Sharing(cohort 内加权) | DRF + StateAware / FIFO / Priority 可选 |
| Gang 调度 | 原生支持(PodGroup + minAvailable) | 原生支持(Workload Admission) | 支持(App 级调度) |
| 准入门槛 | enqueue action 判断 | Admission Check | placement rules + ACLs |
| Volcano 的 deserved 三级模型特别值得记住,因为它把"资源保障"和"资源共享"的关系表达得最清楚: | | | | guarantee:独占预留,其他队列碰不到,用于保护核心业务;deserved:应得资源,空闲时可以被别的队列借走,忙时可以 reclaim 回来,这是弹性共享的关键;capability:硬上限,无论集群多空闲,本队列都不能超过。
这套模型一句话总结就是"预留保底、应得公平、上限封顶"。
Volcano 的调度动作流水线
Volcano 的调度器不走 kube-scheduler 的 Filter/Score 流程,而是用 action + plugin 的可插拔架构——action 定义"走哪几步",plugin 定义"每一步用什么逻辑":
1 | enqueue → 判断基本资源够不够,PodGroup 从 Pending → Inqueue |
调度循环的关键设计:Volcano 每隔约 1 秒 OpenSession(快照整个集群的资源和任务),按序执行 actions,最后 CloseSession 真正 bind。单 session 内串行处理所有 job,每分配一个就立即在内存快照里扣减资源——这个设计天然不会 over-commit,不会出现"两个 gang 任务都判断资源够、却各自只分到一半"的写倾斜问题。这一点和 kube-scheduler 的"每 Pod 一次完整调度周期"是本质不同的架构选择。
6.3 调度器框架:一个 Pod 的一生
调度周期 vs 绑定周期
kube-scheduler 把每个 Pod 的调度拆成两个周期:
- Scheduling Cycle(调度周期):选择节点,串行执行——因为它会修改 scheduler 的共享状态(比如 Reserve 阶段占住的资源),并发会导致竞态;
- Binding Cycle(绑定周期):把决策落地,可以并发执行——只是调 API Server 写 nodeName,不同 Pod 之间互不影响。
这个设计的启示是:调度器是吞吐瓶颈,因为它必须串行做决策。在大规模集群里,调度延迟往往不是 Filter/Score 慢,而是 Scheduling Cycle 排队过长。
十三个扩展点:完整流水线
调度框架定义了十多个扩展点,一个 Pod 从进入队列到被绑定,要完整走一遍:
| 扩展点 | 作用 | 默认实现 |
|---|---|---|
| PreEnqueue | 决定 Pod 是否允许进入 ActiveQ | SchedulingGates 在这里卡 Pod |
| QueueSort | 决定 ActiveQ 内排序 | 按 PriorityClass value 降序 |
| PreFilter | 预计算 Pod 的需求,存入 CycleState | NodeResourcesFit 预算资源 |
| Filter | 对每个候选节点判断"能不能放" | 14 个插件并行评估 |
| PostFilter | 所有节点都失败时触发 | DefaultPreemption 找 victim |
| PreScore | 打分前的预处理 | 初始化打分状态 |
| Score | 对每个可行节点打分 | 9 个插件加权求和 |
| Reserve | 在调度器内存里"占住"资源 | NodeResourcesFit 占位 |
| Permit | 批准 / 拒绝 / 等待 | Gang 插件在这里凑齐成员 |
| PreBind | 绑定前最后一步 | 处理 PV 绑定等 |
| Bind | 调 API Server 写 nodeName | DefaultBinder |
| PostBind | 绑定后通知 | 一般为空 |
| 两个特别值得展开的扩展点: | ||
Permit 是 Gang 调度的实现基石。插件在这里可以返回 Wait,把 Pod 暂时挂起,直到同 Gang 的所有 Pod 都到达 Permit 点才统一放行——这就实现了"要么全成功,要么都不调度"。 |
||
| Reserve 是防止写倾斜的关键。在 Scheduling Cycle 结束前,scheduler 把这个 Pod 需要的资源在内存里占住,这样下一个 Pod 进来时看到的"可用资源"已经扣掉了前面 Pod 的量,避免两个 Pod 同时判断某个节点"够放"却实际只能放一个。 |
Filter 与 Score 的默认插件
Filter 阶段有 14 个内置插件,任何一个返回 Unschedulable 都会淘汰该节点,没有申诉机制:
NodeUnschedulable、NodeName、TaintToleration、NodeAffinity、NodePorts、NodeResourcesFit、VolumeRestrictions、NodeVolumeLimits、VolumeBinding、VolumeZone、PodTopologySpread、InterPodAffinity、DynamicResources、NodeDeclaredFeatures。
Score 阶段有 9 个内置插件,按权重加权求和:ImageLocality、NodeAffinity、NodeResourcesFit、NodeResourcesBalancedAllocation、PodTopologySpread、InterPodAffinity、VolumeBinding、TaintToleration、SelectorSpread。
硬规则 vs 软规则是 Filter/Score 对应的语义边界:硬规则是 Filter(nodeSelector、必须的亲和性),不满足就 Pending;软规则是 Score(weight、preferred),不满足也能调度。几乎所有的调度事故都源于 hard/soft 用错——比如把"必须跑在同一 NUMA"写成了 preferred,结果跨 NUMA 部署导致训练吞吐暴跌。
调度器性能:大规模集群的调优三板斧
调度器在大规模集群下的瓶颈主要是 Filter/Score 的 O(N × M) 复杂度(N 个 Pod × M 个节点)。核心调优手段有三个:
① percentageOfNodesToScore。允许 scheduler 在找到足够可行节点后就停止 Filter,提前进入 Score。默认按集群规模线性计算:100 节点取 50%,5000 节点取 10%,最低 5%。内部还有个硬编码的最小 50 节点——集群小于 50 个节点时该参数不生效。
② 节点轮询遍历。为了让每个节点都有公平机会被选中,scheduler 用"起点轮询"的方式遍历节点数组——第一个 Pod 从头开始扫,第二个 Pod 从上次结束的位置继续扫,避免"永远只评估前 50% 节点"的偏差。
③ 并行 Filter/Score。从 Kubernetes 1.33 起,500+ 节点的集群默认启用并行 filtering/scoring,把节点池分批并发评估,大幅提升吞吐。
6.4 DRF:多资源维度的公平算法
为什么 max-min fairness 不够用
max-min fairness 的经典假设是只有一种资源——比如纯 CPU 的调度,谁分到的 CPU 份额最少就优先满足谁。但在共享集群里,用户的需求向量是异构的:A 跑 CPU 密集型任务,B 跑内存密集型任务,C 跑 GPU 密集型任务。此时"对谁公平"没有统一答案——按 CPU 公平、按内存公平、按 GPU 公平会得出三种完全不同的分配。
传统做法(如 Hadoop 早期)把节点切成固定规格的 slot(例如每 slot = 4 CPU + 32 MB),强行把多资源问题压回单资源——但这会让 CPU 密集型任务浪费内存、内存密集型任务浪费 CPU,整体利用率反而下降。
DRF 的算法核心
DRF(Dominant Resource Fairness,主资源公平)由 Ghodsi 等人在 2011 年 NSDI 论文提出,后来成为 Mesos 和 YARN 的默认策略。定义非常简洁:
主资源 = 作业需求向量中,相对于集群总容量占比最大的那个资源维度。计算步骤是:
- 对每个作业,把它的需求向量除以集群总容量,得到份额向量;
- 取份额向量中最大的那个分量作为该作业的 dominant share;
- 每次调度,优先选择 dominant share 最小的作业。
用经典例子验证一下:集群 9 CPU / 18 GB,用户 A 每任务需 ⟨1 CPU, 4 GB⟩,用户 B 每任务需 ⟨3 CPU, 1 GB⟩。
- A 每任务份额 = ⟨1/9, 4/18⟩ = ⟨11%, 22%⟩ → dominant = 内存 22%
- B 每任务份额 = ⟨3/9, 1/18⟩ = ⟨33%, 6%⟩ → dominant = CPU 33%
- DRF 的目标是让 A 的内存份额与 B 的 CPU 份额趋近相等,最终 A 跑 3 个任务、B 跑 2 个任务,双方 dominant share 均为 67%。
Volcano DRF 插件的实现
Volcano 把 DRF 做成一个独立插件,与 proportion/capacity 队列插件是叠加关系而非替代——队列级公平用 proportion 按权重分,作业级公平用 DRF 排序:
| 关键函数 | 作用 | 语义 |
|---|---|---|
JobOrderFn |
作业排序 | dominant share 小者优先出队 |
PreemptableFn |
抢占判断 | 计算抢占前后双方的份额变化,只允许朝公平方向抢占 |
| PreemptableFn 的逻辑是 DRF 最精妙的地方:它不是简单看优先级,而是算"抢占完成后,抢占方的份额会不会仍然低于被抢占方"——如果抢占会让不公平加剧,就拒绝执行。 |
DRF 的四大数学性质
DRF 之所以成为多资源公平的事实标准,是因为它同时满足四个理想性质,这是之前所有算法都无法联合保证的:
- Sharing Incentive(共享激励):每个用户拿到的份额不比独占 1/n 集群差;
- Envy-Freeness(无嫉妒):没有用户会愿意拿别人的分配方案;
- Pareto Efficiency(帕累托最优):无法在不损害他人的情况下让某人更好;
- Strategy Proofness(策略无欺):谎报需求不会让自己得到更多。
常见误区
- DRF ≠ max-min:max-min 只适用单资源,DRF 是它的多资源泛化;
- DRF 不是节点级放置算法:它决定"哪个作业先出队",不决定"放到哪台机器";真正落节点仍然要经过 predicates/node filtering;
- DRF 不能与 quota 治理混用:Volcano 中如果队列已启用 proportion/capacity,DRF 通常作用于队列内部的作业排序,而不是替代队列级配额。
6.5 调度器跟 Node 的资源是解耦的吗?
一句话结论:逻辑上解耦,时序上有细微不一致——调度器不直接读 node 的实时状态,而是通过 Informer List-Watch 把状态同步到本地 Scheduler Cache,基于这份内存快照做决策,写入则走异步 bind 到 APIServer。
读路径:Informer + Scheduler Cache
kube-scheduler 的第一个控制循环叫 Informer Path,它的唯一任务是维护一份"足够新鲜的本地视图":
- 启动时通过 List 拉取全量 Pod/Node/PV/PVC/StorageClass 等 9 类资源;
- 之后通过 Watch 保持增量同步;
- 事件通过 Handler 回调更新 Scheduler Cache(NodeInfo 树 + Pod 三态:Assumed / Added / Forgotten)。
第二个控制循环 Scheduling Path 的所有 Filter/Score 计算,全部读本地缓存,从不实时访问 API Server 或 kubelet。这是 kube-scheduler 性能的根本保证——如果每个 Pod 的调度都要对 10000 个节点各做一次网络查询,吞吐会直接崩溃。
写路径:乐观绑定
调度器选定节点后,不会同步等 API Server 确认:
- Reserve:在本地 Cache 里 assume 这个 Pod 已经落在 node-1 上,把资源账目扣掉——这样下一个 Pod 的 Filter 就能看到这个"假设占用",防止 double-book;
- Bind:异步起 goroutine,把 Binding 对象写到 API Server;
- 失败回滚:Bind 失败 → cache 里 assume 被回滚,Pod 重新入队重试;
- kubelet 兜底:Pod 真正落到节点时,kubelet 会再跑一遍 Admit(GeneralPredicates)做二次校验,避免因为缓存过期导致的超卖。
这套设计的本质:牺牲强一致性换吞吐
| 一致性风险 | 具体场景 | 兜底机制 |
|---|---|---|
| 缓存滞后 | node-1 刚刚好被别人占满,调度器还以为有空 | 乐观锁冲突 → bind 失败 → 重试 |
| 假设失效 | assume 后 node-1 被 kubelet 驱逐 Pod,资源被别人拿走 | kubelet Admit 拒绝 → Pod 回 Pending |
| 并发 bind | 两个调度器同时 bind 同一个 Pod | APIServer 的 ResourceVersion 乐观锁,后到者失败 |
| 资源超卖 | 调度器假设成功,实际 kubelet 发现节点资源不足 | kubelet Admit + 事件回传 → 调度器重新调度 |
| 面试要点:回答"是否解耦"时,关键是把"读路径解耦(本地缓存)+ 写路径异步(乐观绑定)+ 兜底(kubelet Admit + 乐观锁)“这三层都说清楚,而不是简单答"是"或"否”。 |
6.6 多调度器同时调度产生冲突怎么办?
一句话结论:Kubernetes 用 schedulerName 做"Pod 级路由隔离"——每个 Pod 只归一个调度器管;但不同调度器对同一 Node 的资源竞争依然存在,靠 ResourceVersion 乐观锁、kubelet Admit 兜底,以及工程上的节点池物理隔离来彻底规避。
第一道防线:schedulerName 路由隔离
每个 Pod 通过 spec.schedulerName 指定由哪个调度器调度,不写就默认 default-scheduler。多个调度器都 watch 同一个 Pod 资源,但只认自己名字的 Pod,其他调度器会主动忽略——这是最基础也最有效的"抢 Pod 不冲突"机制。
这意味着"两个调度器同时给同一个 Pod 选节点"这种冲突在路由层就不存在。
真正的冲突:节点资源的并发占用
但问题没完。假设 Scheduler A(kube-scheduler)和 Scheduler B(Volcano)各自 watch 到自己的 Pod 队列,都看到 node-1 有 8 张 GPU 空闲:
- Scheduler A 给 Pod-A1 决策"放 node-1",在 A 的本地 cache 里 assume,异步 bind;
- 与此同时 Scheduler B 给 Pod-B1 决策"也放 node-1",在 B 的本地 cache 里 assume,异步 bind;
- 两个 bind 请求都成功——因为 APIServer 不会做节点资源校验,它只校验 schema 和 RBAC。
结果:node-1 被超卖。Pod-A1 和 Pod-B1 都被绑定,等 kubelet 真正启动时才会发现资源不够。
三层兜底机制
| 兜底层 | 机制 | 拦截什么 |
|---|---|---|
| APIServer 乐观锁 | ResourceVersion 校验 | 同一个 Pod 的并发 bind(而不是同一个 Node 的并发 bind) |
| kubelet Admit | GeneralPredicates 二次校验 | 节点资源超卖、端口冲突——最后一道防线 |
| 调度器 cache 同步 | kubelet 上报的 Pod 状态通过 Informer 回流 | 发现节点实际资源与本地快照不符 |
| 注意:Reserve/Assume 是进程内缓存,别的调度器完全看不到。所以多调度器共存时,"Reserve 防 double-book"只对同一调度器内部有效,跨调度器必须靠 kubelet 兜底。 |
工程上的三种解法
| 解法 | 做法 | 优缺点 |
|---|---|---|
| Pod 级分工(默认) | schedulerName 路由,各自管各的 Pod | 实现简单,但节点资源仍可能互相踩踏 |
| 节点池物理隔离 | 给每个调度器分独立的节点池(taint/toleration/label),互不共享 | 最干净彻底,牺牲一定弹性 |
| 统一调度器 | 干脆只用一个调度器(如 Volcano 管所有),避免多调度器共存 | 抛弃原生调度器的生态优势 |
| 面试追问点:为什么 kubelet Admit 是必要防线?因为多调度器场景下,调度器的"乐观假设"必然存在失效窗口——跨调度器的 assume 状态无法互通,只能靠最终执行者(kubelet)做权威校验。 |
6.7 backfill、preempt、reclaim 是怎么跨 ns 跨队列的?
一句话结论:三者作用域完全不同——backfill 只吃空闲边角资源不触发任何抢占;preempt 是同一 Queue 内的优先级抢占;reclaim 才是跨 Queue 的资源回收,且要求"被回收方超额使用、回收方有缺口"。三者真正的边界是队列而不是 namespace。
三种 action 的语义对比
| 机制 | 作用域 | 触发条件 | 关键约束 | 是否跨队列 |
|---|---|---|---|---|
| backfill | 集群空闲资源 | 有 BestEffort / 小任务待调度 | 不能触发任何抢占,只吃真正空闲的部分 | 否——它根本不抢资源 |
| preempt | 同一 Queue 内 | 高优先级作业资源不足 | victim 必须是同队列、优先级更低、标记 preemptable | 否——队列内 |
| reclaim | 跨 Queue | 目标队列 allocated > deserved 且回收方 allocated ≤ deserved | victim 标记 preemptable,尊重 reclaimable 字段 | 是——队列间 |
| 这是最容易混淆的三个概念。Volcano 的 preempt 是队列内抢占,reclaim 才是队列间回收——很多面试者会把 preempt 说成跨队列,这是典型错误。 |
为什么说边界是队列而不是 namespace
Volcano 的 Queue 是全局资源池,PodGroup 通过 spec.queue 字段挂到指定队列,而 PodGroup 本身可以来自任何 namespace。所以:
- 同一个 Queue 里可以混着 namespace-A 和 namespace-B 的 PodGroup;
- preempt 抢的是"同队列内优先级更低的 Pod",无论它在哪个 namespace;
- reclaim 收的是"队列超额的部分",无论那些超额的 Pod 来自哪个 namespace。
namespace 在这里只是 K8s 的对象隔离边界,不是调度公平的边界。如果想做 ns 级隔离,得把 Queue 与 ns 一一对应,这是治理层的决定,不是机制层的。
reclaim 的精确语义:借出-归还
reclaim 的判断条件是双向的:
回收方(要去抢资源的队列)必须满足:
- 自己的 allocated ≤ 自己的 deserved(它应得但没拿到)
- 存在 pending 的 PodGroup 作业
出借方(被抢资源的队列)必须满足: - 自己的 allocated > 自己的 deserved(超额使用)
- 超额部分足以覆盖回收方的缺口
- 目标 Pod 标记
preemptable: true - 队列的
reclaimable: true(默认就是 true)
反例验证一下:队列 first 配置 deserved=20 核,实际 allocated=40 核(超额借了 20 核);队列 second 配置相同。当 first 想再去抢资源时,日志显示:
1 | Queue <first> can not reclaim, deserved <cpu 20000.00>, allocated <cpu 40000.00> |
first 自己已经超额了,它没有资格去 reclaim 其他队列的资源——这是"借出去要先还回来"的语义。
层级队列下的 reclaim 顺序
从 Volcano 的 Hierarchical Queue 设计看,reclaim/preempt 有明确的"就近优先"原则:
- 优先从同父队列的兄弟队列里找 victim;
- 再按层级自底向上考虑更远的祖先下的其他队列;
- 由
ancestorReclaimLevel控制回收层级深度。
这个设计的动机是业务上的亲和——同一团队内的资源争夺最接近真实需求,跨部门回收会带来更大的政治成本。
Statement 推演机制:所有驱逐先在内存里彩排
Volcano 在执行 preempt/reclaim 时用 Statement 对象做推演:
- 在当前调度 session 的内存快照里"假设性地"驱逐 victim,更新队列/节点的资源账目;
- 验证抢占方 Pod 能否真的放下(gang、节点约束、亲和性等重新检查一遍);
- 只有推演通过,才 commit 真正的删除请求;
- 驱逐后资源不是立即可用——要等 victim 优雅退出(terminationGracePeriodSeconds)+ 缓存观察到变化后,才能给新 Pod 用。
一个容易被忽略的坑:驱逐 Ray/AI 训练 worker 的代价远不止资源回收本身——worker 上的任务、actor、内存对象可能无法无损恢复,要依赖 Ray 的重试和 checkpoint 机制。这也是为什么生产上经常把 AI 训练队列的reclaimable设为 false。
常见错误认知清单
| 错误认知 | 正确理解 |
|---|---|
| “preempt 是跨队列的” | preempt 是队列内,reclaim 才是队列间 |
| “allocated = 实时 CPU/内存利用率” | allocated 是 Pod 请求的记账值,业务空闲但 Pod 存在就算占用 |
| “deserved 变化会立即改变运行中的 Pod” | 需要等资源释放或 reclaim 触发,Pod 不会自动迁移 |
| “reclaim 驱逐后资源立即可用” | 要等 victim 优雅退出,这期间新 Pod 仍然 Pending |
| “backfill 会触发抢占” | backfill 只吃空闲部分,绝不驱逐任何人 |
| “DRF 能替代 quota” | DRF 只做作业排序,队列级配额仍需要 proportion/capacity |
6.8 多调度器共存与 AI 场景落地
为什么需要多个调度器
kube-scheduler 的设计假设是"每个 Pod 独立、随时可调度",这与 AI/批处理负载的诉求根本冲突:
- Gang Scheduling 冲突:8 卡训练作业被逐个调度,前 5 个占资源、后 3 个永远放不下,形成死锁;
- 装箱偏好冲突:原生调度器默认 LeastAllocated(打散),对 Web 合理,对 GPU 是灾难——会人为制造资源碎片;
- 公平性缺失:没有 DRF、没有队列配额、没有"长期占用回收"。
Kubernetes 通过 schedulerName 字段原生支持多调度器共存:Pod 的spec.schedulerName指定由哪个调度器调度,不指定就用 default-scheduler。不同调度器通过 watch 同一个 Pod 资源、只认自己名字的 Pod 来避免冲突。
三大批处理调度器的选型决策
| 场景 | 推荐 | 理由 |
|---|---|---|
| 大规模 AI 训练 + HPC 混部 | Volcano | 生态最成熟,CNCF 项目,与 Training Operator 深度集成 |
| 追求作业排队与配额治理、不想换调度器 | Kueue | 官方项目,用 Admission 机制在 kube-scheduler 之上叠加排队 |
| YARN 迁移、强层级队列治理、多用户 ACL | YuniKorn | 最接近 YARN 语义,层级队列和 placement rule 最强 |
| 商业化平台(fractional GPU、fair-share) | NVIDIA Run:ai / KAI Scheduler | 支持显存切片、动态借出、闭环治理 |
| 三者的架构差异很本质:Volcano 是替换调度器(自己的 scheduler 二进制,自己的 action/plugin 流水线);Kueue 是调度器之上的排队层(不改 kube-scheduler,只做 Admission,Pod 被准入后才被 kube-scheduler 调度);YuniKorn 是独立调度器 + 层级队列治理。 |
GPU 感知调度:gang、拓扑、亲和的三重叠加
AI 场景下,调度器还需要理解 GPU 的物理特性,三重约束叠加后才能做到"把 8 卡训练放到正确的 8 张卡上":
① Gang 约束:PodGroup 的 minAvailable 必须一起满足,避免部分启动死锁。
② 拓扑约束:TP 组必须落在同一 NVLink 域内,DP 组可跨节点但要走 IB。实测显示 TP 跨 IB 比域内 NVLink 慢 7-50 倍,小消息场景接近 50 倍——调度器如果不感知拓扑,等于让法拉利跑乡道。GB200 NVL72 这种机架级系统更是把拓扑约束推到极致:一个作业段最多跨 18 个节点,但必须整体对齐 NVL72 域边界。
③ 亲和与反亲和:同一个 PodGroup 的 Pod 之间用 podAntiAffinity 打散到不同故障域;跨作业用 podAffinity 聚合到同一拓扑域复用 NCCL 通信组。这些约束都要进入 Filter 插件,在预选阶段就淘汰不合规节点。
从 Ai2 案例看调度器治理的边界
Ai2 在数千张 H100 上的改造经验给出了三个清醒的判断:
- 调度器解决不了组织博弈:优先级通胀、占座空转、挂机霸卡,本质都是激励问题,必须在调度之外叠加预算和审计;
- 高出租率不等于高吞吐:频繁抢占触发大规模 checkpoint 读写,SSD 打满、计算等待,"账面 98% 出租率"背后真实计算效率可能只有 70%;
- 拓扑碎片化是隐性代价:频繁抢占重排后,原本对齐的 NVLink 域被拆散,通信带宽骤降,模型越大损失越大。
这三条是 AI Infra 面试里区分"会用调度器"和"理解调度器局限"的分水岭。
七、面试答题套路速查
GPU 性能类:讲清 nsys(时间线)→ NVTX 打标 → ncu(kernel 深挖)三步定位法,并给一个 compute-bound / memory-bound / latency-bound 的具体判断例子。
分布式训练类:按"切什么 → 怎么通信 → 省多少显存 → 对拓扑什么要求"逐个展开,给一个 TP+PP+DP+ZeRO 的组合案例。
弹性伸缩类:三层弹性(actor / Ray 节点 / K8s 节点)+ 扩缩时机(pending demand / idle timeout)+ 夜间缩容的业务逻辑(流量低谷让卡给离线任务)。
混部类:资源共享层级 → 隔离方式(MIG/MPS/时间片)→ 优先级与回收 → 关键负载保障,四段缺一不可。
扩容可行性类:额度 ≠ 容量 ≠ 放置三层拆开 → Quota 是 Admission 层 → DryRun 不创建 Pod → Gang + 拓扑 + 碎片 + 卡型四类约束 → 状态变化的 reconcile 处理。
抢占类:PriorityClass 四字段 → kube-scheduler 四步抢占流程 → PDB 是 best-effort → 抢占/回收/驱逐三形态 → AI 场景的时间预算治理。
队列类:kube-scheduler 三队列 + 两个后台协程 + 事件驱动迁移 → QueueSort 自定义 → Volcano 的 guarantee/deserved/capability 三级模型。
DRF 类:max-min 为什么不够 → dominant share 计算 → Volcano 的 JobOrderFn/PreemptableFn → 四大数学性质。
解耦类:读路径(Informer 本地缓存)+ 写路径(乐观绑定)+ 兜底(kubelet Admit)三层。
多调度器冲突类:schedulerName 路由隔离 → 真正冲突在节点资源 → ResourceVersion + kubelet Admit + cache 回流三层兜底 → 节点池物理隔离的工程解法。
backfill/preempt/reclaim 类:作用域三层对齐→ 队列而非 namespace 是边界 → reclaim 的双向条件 → Statement 推演 + 就近优先。
八、结语
把所有这些问题串起来看,AI Infra 岗位考察的能力模型其实很清晰:
| 模块 | 考察点 | 底层能力 |
|---|---|---|
| GPU 性能 | Nsight + 并行策略 | 瓶颈定位 + 系统性建模 |
| 弹性伸缩 | RayJob + 业务权衡 | 技术机制 + 成本意识 |
| 混部 | 隔离 + 优先级 + 回收 | 多目标约束下的工程取舍 |
| 扩容可行性 | Quota + DryRun + 亲和 | 概念分层 + 状态机思维 |
| 检查成本 | 覆盖 + 耗时 + 并行 | 系统化拆解 + 量化意识 |
| 调度层 | 抢占 + 队列 + DRF + 解耦 | 分布式一致性 + 资源效率权衡 |
| 这些问题的共同规律是:没有非黑即白的答案,全都在"保障关键负载"和"提升整体效率"之间找平衡点。调度层尤其如此——DRF 用份额算法换多维公平,调度器用本地快照换吞吐,多调度器用路由隔离换扩展性,preempt/reclaim 用语义分层换治理灵活性。 | ||
| 准备 Infra 面试时,与其背名词,不如把每个问题都按"是什么 → 为什么 → 怎么做 → 有哪些权衡 → 实际踩过什么坑"的框架练一遍,这条链路能完整走下来的候选人,在任何一轮技术面里都很难被问倒。 |