Ray 为什么能每秒调度 180 万个任务:把三种系统揉进一个引擎
- 关联论文:1712.05889
2017 年的 AI 系统有个被长期忽略的难题:一个强化学习应用要同时跑仿真、训练、服务三种形态完全不同的负载——仿真要毫秒级、训练要跨节点同步、服务要延迟敏感、参数搜索还要边跑边生成新任务。当时的做法是 Spark 管批、Parameter Server 管训练、专用 RL 框架管仿真,同一个应用维护三到四套分布式系统,调度器与容错语义互不兼容。Ray 的赌注是:这些差异其实是编程抽象层面的差异,不是系统层面的差异。
为什么重要
它最值钱的不是"跑得快",而是证明了异构 AI 负载可以在同一个编程模型下共享同一个执行引擎。Task/Actor 这对原语足够小、足够正交,新负载类型通常不需要新的系统抽象。放到当下:Agent 系统的形态和论文描述的 RL 负载高度相似——大量短生命周期的工具调用(task)、跨多轮的会话状态(actor)、运行时才知道依赖的调用链(动态 task 图)。Ray 后来的 Ray Data / Serve / RLlib 正是沿这条路径展开的。
核心机制
两个原语。 Task 是无状态函数,关键设计是可在运行时动态派生新的 task,不要求事先画出完整 DAG。Actor 是有状态对象,a.method.remote(x) 异步返回 future,状态由框架放在某个节点上串行维护。Spark 的 RDD 抽象"不可变批量数据集",天然不适合在线服务与有状态对象;Celery 之类能表达动态任务图,但缺 actor 抽象,也没为 RL 仿真做毫秒级调度优化。
自底向上调度是全篇最有意思的决策。与中心化调度(心跳上报 + 中心分配)不同,Ray 的 worker 在本地空闲时广播自己可用,然后竞争性地"拉取"任务:从 GCS 拉一批待执行 task 的元数据,挑出依赖已满足、且本地有对应 object 的来跑。调度决策发生在 worker 侧,中心只维护依赖图。好处是中心不成为吞吐瓶颈,任务顺序能随"谁先算完"自然变化,正好契合运行时动态依赖。代价是调度非全局最优——保证吞吐与延迟优先,不保证任务放置最优。
控制面/数据面分离是高吞吐的关键。GCS 存元数据(节点、actor 状态位置、依赖关系),Plasma 存 payload(基于 Apache Arrow 的分布式内存对象存储,零拷贝共享)。小的、需一致性的元数据走 GCS,大的、可分片的 payload 走 Plasma。
证据边界
引用必须带口径,这是本文最容易出事的地方。
- 1.8M tasks/s 是 100 节点的集群级数字,不是单机能力。论文 Figure 8b:60 节点超过 1M/s,100 节点线性扩展到 1.8M+/s,右端点是 100M 任务耗时 54 秒。
- 这个头条数字来自空 task 压测,代表调度路径上限,不是端到端真实 AI 管线吞吐。论文自己也写明 task 时长增加会按比例拉低吞吐。
- object store 的 15 GB/s / 18K IOPS 是单客户端、单台 16 核 m4.4xlarge 的测量值,不是集群聚合吞吐。
- GCS 容错不是 Raft。论文原文是"在 Redis 之上做轻量级链式复制"(chain replication)。
- "比业界最好快约 3 倍"是二级来源(见于公开课程讲义),回查论文 PDF 未找到出处,不建议写进选型文档。
- "actor 状态 checkpoint 到 GCS"是实现层描述,论文摘要层面只明确 "transparent fault tolerance"。
工程局限
容错不是数据级的。 Ray 的容错主要覆盖 task 失败重试与 actor 状态 checkpoint,不提供通用 object 副本机制。actor 状态体积大时,checkpoint 成本 ≈ 频率 × 状态体积 × actor 数,可能吃掉容错收益。更隐蔽的坑是:重试对有副作用的 task(写库、发消息)会产生重复副作用,必须自带幂等键。
动态派生会撑爆控制面。 Agent 场景里让模型自由派生子 task,GCS 里的元数据与依赖图规模爆炸,故障恢复时的重放时间也变长。派生深度、并发度、每个 task 的最大时长都要硬限。
大对象是隐藏瓶颈。 Plasma 的零拷贝只在同机共享内存内成立,跨节点就是实打实的网络传输。论文还指出大对象的开销主要在 memcpy、小对象主要在序列化与 IPC——两侧都容易打满。跨节点只传引用与 delta,大对象落文件系统。
2018 年的组件图已经过时。 论文写的是 Ray v1 时代的单一 GCS,今天已拆出独立的 Ray Control Plane 服务。今天该读的是设计动因与权衡,而不是照抄组件图。API 迭代也很快,跨大版本迁移成本不低。
最后一条纪律:先用空 task 基准,再用自己的真实负载复测一遍——论文的图就是空 task 做的,直接引用就是坑。
标题变体
- Ray 为什么能每秒调度 180 万个任务:把三种系统揉进一个引擎
- 100 节点、100M 任务 54 秒:Ray 那个 1.8M tasks/s 是怎么来的
- 别用中心调度器管百万级短任务:Ray 的自底向上调度为什么快
小红书卡片文案
Ray 论文里最容易被引错的一个数字:每秒 180 万个 task。
三件事必须知道: ① 这是 100 节点集群的数字,而且来自空 task 压测——是调度路径上限,不是你业务管线的端到端吞吐。真实 RL 负载会低 1~2 个数量级。 ② object store 的 15 GB/s 是单客户端、单台 16 核机器,不是集群聚合能力。容量规划必须自己在目标硬件上实测。 ③ GCS 的容错是 Redis 上的链式复制,不是 Raft——这是论文原话。
真正值得抄的设计才三个:Task/Actor 双原语、自底向上调度、控制面(GCS)与数据面(Plasma)分离。这三条今天没过时。
如果你的系统里有三类以上形态不同的负载,"一套引擎统一"的收益通常大于"每类用一个最专精的框架"——省掉的是跨框架的状态搬运和运维,不是单点性能。