Netflix 如何用 Kueue 简化批计算:一次百万级任务的换心手术
- 约 11 分钟读完 - 全文 4308 字Netflix 如何用 Kueue 简化批计算:一次百万级任务的换心手术
原文:How Netflix Simplified Batch Compute with Kueue 目标读者:维护过或正在维护自研任务调度/排队系统的工程师;以及所有纠结过「自研轮子 vs 换开源方案」的人。 阅读目标:搞清楚 Netflix 为什么敢把百万级批任务的排队调度层整体换成 Kueue、怎么换的、换完拿到了什么。重点不在 Kueue 的用法,在这次迁移的姿势。
〇、先把背景摊开:CMB 和 Titus
Netflix 的容器平台叫 Titus,跑批任务用的是自建方案 CMB(Compute Managed Batch),2018 年建的。CMB 负责的事:用户提交跑到结束的批工作负载,按租户层级排队、按优先级有序执行、按租户管理容量;真正跑任务的活交给 Titus。
Titus 对 CMB 有两个关键能力:跨 cell 联邦(一个 cell 就是一个 K8s 集群)和联邦容量预留。所以 CMB 只需要对 Titus 的一个端点说话,不用关心底下集群的拓扑——这一层抽象是后面整次迁移能「无感」的地基。
租户层级与两种容量
CMB 的租户是一棵树,分两类:
- 内部租户(Internal Tenant):只做组织,不接收任务,没有队列;子节点可以是内部或叶子租户。
- 叶子租户(Leaf Tenant):能接收任务,挂队列,不能再有子节点。
容量也分两种:
- 预留容量(Reserved):内部租户的预留在其子树内公平共享;叶子租户的预留则是独占切分,别的租户碰不了,保证吞吐。
- 共享容量(Shared):Compute 团队维护一个全局池子,谁都可以 burst 进去。池子按租户公平共享——但注意,CMB 的公平只在准入(admission)那一刻生效:它没有抢占,任务一旦放行就跑到结束,哪怕公平份额已经变了。这个语义缺口后面会被 Kueue 填上。
一、为什么是 Kueue:自研轮子的折旧时刻
CMB 2018 年建的时候,开源界还没有像样的批计算方案。但 K8s 生态滚了几年之后,CMB 有的和想要的功能——公平共享、层级租户、容量管理、优先级队列——开源项目全都有了。反过来,CMB 想加新功能(比如抢占)越来越费劲,因为它离底层的 K8s 集群隔得太远。这就是自研系统的经典折旧曲线:当年它填补空白,现在它阻碍你吃到生态红利。
团队评估了怎么把批抽象现代化,最后选了 Kueue,理由五条,第一条最值得琢磨:
- Kueue 不替换 kube-scheduler。 和 YuniKorn、Volcano 不同,Kueue 只管排队和准入,Pod 调度还是 kube-scheduler 干,所以能和 Titus 已有的调度画像(scheduling profiles)集成。换掉 Titus 的调度画像会让任务摆放(placement)碎片化,损害效率。这条的工程含义:选组件时,「它不管哪一段」和「它管哪一段」同样重要——Kueue 把调度和排队拆开,只拿走排队这一段,接入面就小得多。
- 社区采用势头和创新速度。
- 支持异构硬件上的多租户配额管理。
- 能操作
v1.Pod、batch/v1.Job这类原生原语,也支持 RayJob / RayCluster 这类高层抽象,留好了扩展空间。 - 自带团队本来想在 CMB 里自己造的功能:抢占、all-or-nothing 调度、拓扑感知调度。
二、迁移:Netflix Batch 的三条军规
这次把 CMB 工作负载迁到 Kueue 的项目,内部叫 Netflix Batch。军规三条:
- 对 CMB 终端用户零负担、完全透明。
- 容器启动速率和整体最大吞吐不允许回退。
- 用 Kueue 替换 CMB 的排队与调度。
新旧提交流程的关键差别:排队和调度被下放给每个启用了 Kueue 的 Titus cell 里的 Kueue,Titus 联邦层通过一个自研 Kueue 路由器把任务路由到 Kueue cell。注意这里没说换成 K8s 的通用调度入口——联邦这层壳还在,换的是壳里面的引擎。
租户管理这边,对运维来说迁移简单到「在 UI 上给租户点一个按钮」,出问题也能一键回滚。按钮底下的映射关系是:
- 内部租户 → Cohort
- 叶子租户 → ClusterQueue + LocalQueue
- 租户上的容量配置 → resource flavor + 名义配额(nominal quota)
flowchart LR
subgraph OLD["迁移前 CMB"]
U1["用户与应用"] -->|"提交批任务"| CMBQ["CMB 自研排队与调度<br/>租户层级加公平共享"]
CMBQ --> FED["Titus 联邦层"]
FED --> CELL["Titus cell 各 K8s 集群"]
end
subgraph NEW["迁移后 Netflix Batch"]
U2["用户与应用 同一 API"] -->|"提交批任务 无感"| ROUTER["自研 Kueue 路由器"]
ROUTER --> KQ["各 cell 内的 Kueue<br/>ClusterQueue 排队与准入"]
KQ --> KS["kube-scheduler 负责 Pod 摆放"]
end
OLD -.->|"只换排队调度引擎<br/>API 与联邦层不动"| NEW
style KQ fill:#fff3cd
这张图是整篇文章的骨架:对上一寸不动,对下一寸不动,只把中间自研的那层换成开源组件。 用户侧的 API、Titus 的联邦与调度画像都是旧资产,全保留了。
三条经验教训(全文最有信息量的部分)
- 先保 API 平价,先换底层组件。 不给用户暴露新 API 面,而是把底下组件换掉作为第一步。原文的说法是 unstacking bets——把赌注拆开:不让「用户迁新 API」和「底层换引擎」两件事耦合成一次豪赌。项目风险降了,客户体验也没断。
- 别把最复杂的客户留到最后。 他们一上来就迁最大最复杂的那个客户。信心是先难后易建起来的,后面其他客户的迁移就顺了——结果整个生产迁移只花了 4 周。多数迁移的直觉是「先易后难、小步快跑」,Netflix 反着来:最难的客户先过,因为它把所有边角情况一次性暴露完,剩下的全是子集。这和「先打最硬的仗」是一个道理——前提是第 1 条保证了输得起。
- Kueue 的默认参数顶不住 Netflix 的量。 必须把 QPS、Burst、groupKindConcurrency 调得比默认高很多才能满足吞吐。这个坑是提前在仿 Titus 的开发环境跑负载测试趟出来的,不是生产上撞出来的。
三、换完之后:抢占式公平共享补上了语义缺口
Kueue 已在生产全量铺开,管理着数百万级批工作负载。前面说的 CMB「公平只在准入时生效、无抢占」的语义缺口,现在用**基于抢占的公平共享(Preemption-based Fair Sharing)**补上了:
- 预留容量没人用的时候可以借给其他租户,主人回来再抢回来——预留语义保住,闲置不再浪费。
- 高优先级任务可以抢占低优先级任务——关键业务的周转时间变快,且不怕饿死。
原文给的 ClusterQueue 抢占配置示例:
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: "team-a-cq"
spec:
preemption:
reclaimWithinCohort: Any
withinClusterQueue: LowerPriorityreclaimWithinCohort: Any 意思是:同 Cohort 里谁借了我的份额,我随时可以抢回来(不限优先级);withinClusterQueue: LowerPriority 意思是:队列内部只抢优先级比我低的。两条规则分别对应「租户之间」和「租户之内」两个维度的公平。
效果:Compute 团队观察到平均资源利用率显著提升(原文没给具体数字,只有这个定性结论)。另外这套经验正在外溢到其他内部团队,包括搭 K8s 原生训练基础设施的团队——批计算和 ML 训练的调度问题,本质上是同一个。
flowchart TD
subgraph COHORT["Cohort 对应内部租户 子树"]
CQA["ClusterQueue 团队 A<br/>有预留配额"]
CQB["ClusterQueue 团队 B<br/>有预留配额"]
end
CQA -->|"配额闲置时<br/>借给同 Cohort 成员"| CQB
CQB -->|"主人回来 reclaimWithinCohort Any<br/>抢占借出去的资源"| CQA
CQA -->|"队列内部<br/>高优先级抢占低优先级"| PRE["withinClusterQueue LowerPriority"]
PRE --> R["预留语义不丢<br/>闲置容量不浪费<br/>利用率上升"]
style R fill:#e8f5e9
四、工程实践联想
- 迁移的黄金姿势:API 不动,引擎换心。 Netflix 把「用户迁新 API」和「底层换引擎」拆成两个独立的赌注,先只赌后者。回到我们自己的项目:存储换引擎、队列换实现、框架换内核,第一件事永远是把门面焊死。门面动了,你就同时在做两个项目。
- 先迁最难的客户,但要先有回滚按钮。 「最难的先来」之所以成立,是因为他们有 UI 一键切换加一键回滚托底。没有廉价回滚能力之前,先难后易是鲁莽,不是勇气。顺序是:先造回滚,再挑硬骨头。
- 调度与排队分层,是值得抄的架构决策。 Kueue 只做排队准入,Pod 摆放留给 kube-scheduler。这个切分让 Netflix 保住了 Titus 多年的调度调优积累。评估任何「大而全」的调度系统(YuniKorn、Volcano 都是全替换派)时,先问:它接管的那一层,我原有的积累值多少钱?
- 开源组件的默认参数是按「示例集群」调的,不是按你的量。 QPS、Burst、并发数这类旋钮,上生产前必须在仿真环境里压出来。Netflix 的负载测试环境「模仿 Titus」,这笔钱花在了迁移前,不是事故后。
- 自研系统要定期做折旧审计。 CMB 不是烂系统,是时代变了:它独有的功能被生态追平,它缺失的功能(抢占)自研成本却越来越高。每两年问一次「这轮子如果今天立项,我还会自己造吗」,答案变了就该动手了。顺带一提,做 AI 训练平台的可以直接抄作业:批任务的排队、配额、抢占、多租户公平,和 GPU 训练集群的调度问题是同构的,Kueue 支持 RayJob/RayCluster 就是 Netflix 明说的扩展点。
本篇小结
- 背景:CMB(2018 年自研托管批处理)跑在 Titus 上,租户分内部/叶子两级,容量分预留/共享两种;共享容量只在准入时公平,无抢占。
- 动机:K8s 生态追平了 CMB 的功能,而自研新功能(如抢占)成本越来越高;Kueue 不替换 kube-scheduler、支持异构配额和 Pod/Job/RayJob,且有抢占、all-or-nothing、拓扑感知调度等原生能力。
- 迁移(Netflix Batch):用户零感知、吞吐不回退、排队调度换 Kueue;内部租户映射为 Cohort,叶子租户映射为 ClusterQueue + LocalQueue,容量配置映射为 resource flavor + nominal quota;UI 一键切换、可回滚。
- 经验:API 平价先行拆分赌注;最大最复杂的客户最先迁,生产迁移仅 4 周;Kueue 需调高 QPS/Burst/groupKindConcurrency,靠仿真环境负载测试提前摸底。
- 现状:生产全量、数百万批工作负载;基于抢占的公平共享(
reclaimWithinCohort: Any+withinClusterQueue: LowerPriority)让预留容量可出借可回收,平均资源利用率显著提升。
一句话收束:这篇文章表面讲 Kueue,实际讲的是一个成熟组织怎么给自研系统办一场体面的退休——不办追悼会(大 rewrite),办的是工作交接(API 不变、逐层替换、最难的先交)。
参考资料
- How Netflix Simplified Batch Compute with Kueue(原文,作者 Alvin Bao、Alex Petrov、Jennifer Lai、Aidan Sherr、Samartha Chandrashekar,2026-06-22)
- Kueue 官方文档(K8s 原生的批任务排队系统,原文提到的 Cohort、ClusterQueue、LocalQueue、resource flavor、抢占语义的权威定义)
- YuniKorn 与 Volcano(原文对比中提到的全替换派调度器)
- Netflix Titus(Netflix 容器平台,CMB 与 Netflix Batch 的运行底座)