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
两者的关系是递进式的定位流程,不是二选一。
标准的瓶颈定位三步走:
  1. 先用 Nsight Systems 看时间线:判断 GPU 到底是在算还是在等。典型信号包括 kernel 之间的大段空白(GPU 饥饿,说明 CPU 侧数据准备或 Python 逻辑是瓶颈)、cudaMemcpy 占比过高(PCIe 传输成为瓶颈)、以及本可以重叠却串行执行的 H2D 拷贝与 kernel。
  2. 加 NVTX 打标:在训练循环里用 nvtx.range_push("forward") 等标记关键阶段,让时间线可读性大幅提升,这是生产环境必备技巧。
  3. 再用 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 需求)而非物理机利用率来扩缩。工作流程是:

  1. 用户提交 Ray task/actor/placement group;
  2. 资源不足时请求入队,Autoscaler 侧感知到 pending demand;
  3. Autoscaler 增加 RayCluster CR 的 replicas 字段,KubeRay Operator 创建新的 Worker Pod;
  4. Pod 就绪后 Ray 调度器把工作负载放置上去;
  5. 节点空闲超过 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 能不能放进去?"整个流程分四步:

  1. 筛选候选节点:找出"假如移除若干低优 Pod 就能放下目标 Pod"的节点集合;
  2. 选择 victims:在候选节点集合里计算"驱逐代价"最小的组合,优先考虑优先级最低、最近被调度、打断的 PDB 最少的组合;
  3. 删除 victims:scheduler 只是把 victim Pod 打上删除标记(通过 API Server 走 deletion + graceful termination),真正终止由 kubelet 完成;
  4. 下一轮重试: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
2
3
4
5
enqueue  →  判断基本资源够不够,PodGroup 从 Pending → Inqueue
allocate → 给有明确资源请求的 task 精确选节点
backfill → 把剩余边角资源喂给 BestEffort / 小任务
preempt → 队列内高优先级抢占低优先级
reclaim → 队列间回收超额占用的资源

调度循环的关键设计: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 的默认策略。定义非常简洁:
主资源 = 作业需求向量中,相对于集群总容量占比最大的那个资源维度。计算步骤是:

  1. 对每个作业,把它的需求向量除以集群总容量,得到份额向量;
  2. 取份额向量中最大的那个分量作为该作业的 dominant share;
  3. 每次调度,优先选择 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 确认:

  1. Reserve:在本地 Cache 里 assume 这个 Pod 已经落在 node-1 上,把资源账目扣掉——这样下一个 Pod 的 Filter 就能看到这个"假设占用",防止 double-book;
  2. Bind:异步起 goroutine,把 Binding 对象写到 API Server;
  3. 失败回滚:Bind 失败 → cache 里 assume 被回滚,Pod 重新入队重试;
  4. 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 有明确的"就近优先"原则:

  1. 优先从同父队列的兄弟队列里找 victim;
  2. 再按层级自底向上考虑更远的祖先下的其他队列;
  3. 由 ancestorReclaimLevel 控制回收层级深度。
    这个设计的动机是业务上的亲和——同一团队内的资源争夺最接近真实需求,跨部门回收会带来更大的政治成本。

Statement 推演机制:所有驱逐先在内存里彩排

Volcano 在执行 preempt/reclaim 时用 Statement 对象做推演:

  1. 在当前调度 session 的内存快照里"假设性地"驱逐 victim,更新队列/节点的资源账目;
  2. 验证抢占方 Pod 能否真的放下(gang、节点约束、亲和性等重新检查一遍);
  3. 只有推演通过,才 commit 真正的删除请求;
  4. 驱逐后资源不是立即可用——要等 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 面试时,与其背名词,不如把每个问题都按"是什么 → 为什么 → 怎么做 → 有哪些权衡 → 实际踩过什么坑"的框架练一遍,这条链路能完整走下来的候选人,在任何一轮技术面里都很难被问倒。