Skip to content

云知声工程实践:RL rollout 场景下的 CubeSandbox 密度边界压测

作者|云知声 Atlas 智算团队

编者按| Agent RL rollout 对沙箱的需求和普通 Agent 服务不同,比如每条轨迹都需要干净隔离的执行环境,生命周期分钟级,数量规模化并行,沙箱里可能运行的是模型生成的任意代码。当标准参数真正落到工程实践场景时,往往会受到负载特征、调度参数、资源超卖、运行时峰值等多重因素影响。

云知声团队在用 Cube 支撑 SWE Agent rollout 和 RL 环境训练的实践中,基于 128 核 251 GiB 单机配置、1 核 4096 MiB 沙箱规格,推导出调度上限 117、不超卖的安全边界 58、实测稳态 80-100 三组数值。本文记录了从规格配置、调度参数推导到实测瓶颈的完整过程,以及实际运行中遇到的问题和解决方案。

一、RL rollout 负载特征与选型考量

我们用 Cube Sandbox 支撑的是 Agent 轨迹 rollout,具体分为两类负载:

一类是 SWE Agent 数据合成,核心是批量让 agent 解决真实代码仓库里的问题,产出训练数据;另一类是 Agent RL 环境训练,强化学习阶段需要大量并行 episode 与环境交互。

这类负载有几个鲜明特征,也基本决定了技术选型方向:每条轨迹需要一个干净、隔离、可复现的执行环境;生命周期很短,通常分钟级;数量极大,属于规模化并行。更关键的一点在于,沙箱里跑的是模型生成的任意代码,行为不可预判——死循环、内存泄漏、fork 炸弹、异常网络调用都可能发生,这让"安全隔离"属性成为我们的硬指标。

对这类负载来说,吞吐比稳态存活数更值得关注。轨迹生命周期很短,沙箱创建、执行、销毁的频率远高于常驻服务,单节点每秒需要处理几十到上百个创建请求。对应的,Cubelet 的 workflow 并发配置为创建、销毁各 100,配合 Cube 毫秒级的启动能力,这个吞吐量级是能够支撑的。

我们选型阶段的核心判断在于隔离边界的差异。Cube Sandbox 的隔离边界在硬件虚拟化层,每个沙箱是一台独立的 KVM MicroVM,拥有自己的内核,这是它和我们评估过的其他方案最本质的差别。而 Docker 的隔离边界在 namespace 和 Cgroup,共享宿主内核。对于执行不可信代码的场景,这个差别决定了能不能上生产:namespace 和 Cgroup 的隔离强度,在 Agent 执行任意代码时存在逃逸风险,而 MicroVM 的硬件级边界能避免 Agent 影响宿主环境。

具体到几项核心需求,Cube 都有对应的能力设计。安全隔离上,MicroVM 的硬件级边界满足了不可信代码执行场景;快速启动上,大规模 rollout 需要秒级甚至毫秒级创建环境,Cube 把模板预先烤成不可变 rootfs,并在每个节点本地预存内存快照,起沙箱时走本地 reflink 克隆而非冷引导,官方标称 60ms 级冷启动;环境复现上,模板是一份不可变的 ext4 二进制产物,全节点分发同一份,避免了镜像层缓存或宿主差异导致的行为漂移;资源控制上,CPU、内存、网络都有约束手段,Cgroup 与 VM 边界双层生效,调度器统一记账。高并发上,Cube 单节点软件设计上限是 3000 个存活沙箱。Cube 原生也支持内存加磁盘的完整快照,MicroVM 快照可以从任意中间态分叉,这对 RL 训练的样本效率影响很直接。但当前快照这一层能力我们暂时还没有深度用起来。

业务侧完全通过 E2B SDK 接入。Cube 的沙箱 API 与 E2B 协议兼容,训练侧的调度代码无需特殊适配,这一点在我们选型时也是加分项。

不过,这套兼容也带来一个需要留意的行为细节:e2b SDK 默认的命令执行方式是 "bash -lc command",也就是先拉起一个全新的 Bash 登录 shell(login shell),再在这个新 shell 里执行指定命令。单次执行时这个默认行为没有问题,但如果任务本身分两阶段执行(比如先跑一次评测准备、再跑一次评测本体),问题就会在第二阶段暴露出来:每次命令执行都会重新拉起一个登录 shell,第一阶段设置的环境变量、PATH 路径等状态不会带到第二阶段,导致两阶段环境不一致。

这个问题的根因不在 Cube 本身,而在 E2B SDK 协议默认的执行方式——一旦确认命令执行走的是 login shell,现象就能解释清楚。我们的处理方式是,在两阶段任务之间把需要延续的环境变量显式重新设置一遍,而不是依赖 shell 状态自动延续下去。

二、控制面与计算面的分离架构

我们的部署形态是 1 个控制节点加 N 个计算节点,控制节点自身也参与计算。

控制节点承载了 Cube 完整的控制面:cube-api 监听 3000 端口,是 SDK 的接入入口;CubeMaster 监听 8089,负责调度、模板中心和生命周期管理;cube-lifecycle-manager 驱动 AutoPause;cube-proxy 和 coredns 负责沙箱端口暴露;webui 跑在 12088;另外还有 MySQL 和 Redis 两个依赖组件。控制节点自身也跑着 network-agent 和 cubelet,所以它不只是控制面,本机同样承载沙箱。相比之下,计算节点很轻,只跑 network-agent 和 cubelet 两个组件,通过 ONE_CLICK_CONTROL_PLANE_CUBEMASTER_ADDR=<控制节点 IP>:8089 注册到控制面,注册完成后,CubeMaster 通过 9999 端口的 gRPC 向它下发沙箱创建和销毁指令。

控制面 / 计算面节点职责划分

图 1:控制面 / 计算面节点职责划分

模板分发的设计是性能表现的关键:模板不是放在共享存储里,而是由 CubeMaster 主动分发到各计算节点的一份 replica。 CubeMaster 先把 OCI 镜像烤成 ext4 rootfs,再主动推送到每一个计算节点,各节点收到后,会在本地再跑一次 VM,取内存快照留存。正因为这份 replica 已经在本地,起沙箱时才能走纯本地的 FICLONE 克隆,零网络 IO,这是毫秒级启动能实现的前提,相应的代价是每个计算节点都需要一块足够大的本地盘来存放这些 replica。

同时,存储配置也要配合这套设计:/data/cubelet 必须用 XFS 并开启 reflink,因为 cubecow 存储引擎依赖 FICLONE,非 XFS 会直接拒绝启动;模板镜像和内存快照则放在另一块盘,那块盘没有文件系统限制。

三、调度参数到实测稳态密度推导

我们当前使用的 Cube 版本是 v0.6.0,跑的是两套双节点集群(每套一控一算),单机配置 128 核 251 GiB。沙箱规格按 SWE agent 的实际需要配置:1 核、4096 MiB 内存、10 GiB 可写层。

我们按沙箱规格和 CubeMaster 的调度参数推算出两个关键数字:单节点并发上限约 117 个,双节点约 234 个,瓶颈出现在内存侧。这个上限是怎么算出来的:调度参数设置为内存预留 10 GiB、内存超卖比 2 倍、CPU 超卖比 3 倍但利用率封顶 80%;单个沙箱按 1 核 4096 MiB 的规格,实际占用内存约 4202 MiB。用这两个数字相除,内存维度能支撑的沙箱数是 117 个,而 CPU 维度理论上能支撑到 306 个——两个数字取小的一方,最终卡在内存这一侧。

要理解 117 这个数字,需要先理解"超卖"在这里的含义。内存超卖比 2 倍,意思是调度器允许接单的总内存额度是物理内存的 2 倍:这台机器物理内存只有 241 GiB,但调度器按约 480 GiB 在分配配额。如果完全不做超卖,单节点能跑的沙箱数应该是物理内存直接除以单沙箱占用,即 241 GiB 除以 4.2 GiB,约等于 58 个。这个 58,是我们在不依赖任何假设、能保证不触发 OOM 的安全边界;而 117,是调度器允许我们冒险接单的账面上限。58 到 117 之间的差值,本质是用超卖去换密度,而不是机器真的能同时装下 117 个沙箱的内存需求。

117 这个账面上限能不能真正兑现,取决于一个关键假设:沙箱不会同时把 4 GiB 内存用满。这个假设在大多数时候是成立的,因为 Cube 的 MicroVM 采用惰性分页机制,沙箱在空转、或者在等待大模型返回结果的时候,实际占用的内存(RSS)远低于 4 GiB 的标称值。但一旦沙箱进入 SWE 场景里的编译、装依赖、跑测试这类阶段,内存占用会迅速涨到接近 4 GiB 上限,这个假设就会被打破。所以我们的实测结果是:稳态运行时,单节点能稳定撑住的并发数落在 80-100 个,双节点大约 160-200 个,比账面的 117/234 要低。原因很直接,只要有一批沙箱同时撞上编译阶段的内存高峰,宿主机的物理内存会先于调度器的账面数字被撑爆。

密度推导阶梯图

图 2:密度推导阶梯图

再看 CPU 侧的情况:117 个沙箱每个都要占用 1 核,加起来是 117 核,而这台机器物理只有 128 核,利用率已经达到 91%。也就是说,当内存侧顶到 117 这个上限时,CPU 侧几乎同时被顶满,两个资源维度几乎是同步触顶的。这说明当前给沙箱配的规格(1 核配 4096 MiB)和这台机器的物理配置(128 核配 251 GiB)是匹配的,不存在某一侧资源明显过剩、另一侧提前卡死的情况。

四、压测中观察到的瓶颈

压测过程中我们遇到过三类瓶颈,表现各不相同,需要分开识别,否则容易被笼统归为"资源不够"而错判方向。

第一类是内存超卖被真正兑现,也就是前面提到的"58 到 117 之间那段靠超卖换来的空间"最终落空的时候。它的表现是:宿主机的可用内存(available)持续下降直至耗尽,随后触发 OOM,操作系统会直接杀掉进程来释放内存。这种情况通常出现在并发数往 117 逼近、又恰好赶上一批沙箱同时进入编译高峰的时刻——也就是我们前面提到的"沙箱不会同时把 4 GiB 用满"这个假设被打破的场景。这里需要区分清楚:CPU 超卖顶满时,沙箱只是变慢,进程不会被杀;内存超卖顶满时,操作系统会直接杀进程。两者是性质完全不同的信号,不能混为一谈。

第二类是磁盘水位触发节点被调度器排除在外。CubeMaster 会持续监控节点上几块关键磁盘的使用率,只要任意一块超过 80%,这个节点就会被标记为不再接受新的沙箱创建请求,此时新建沙箱会失败,报错码 130597(no more resource)。这里容易造成误判的地方是:节点在监控面板上看起来仍然是 HEALTHY 状态,但实际上已经拒绝新沙箱。

所以看到 130597 报错时,第一反应应该是去查磁盘水位,而不是查节点健康状态。我们早期就遇到过这种情况:单节点上 cube-snapshot 组件把系统盘写到了 83%,触发的正是这个错误码。这里有一个容量数字需要澄清:每个沙箱的可写层上限是 10 GiB,117 个沙箱理论上最坏情况会占用 1.14 TiB,但这只是最坏估计,实际用到的写时复制(CoW)空间通常远小于这个数字。真正需要遵守的规则是:可写层必须放在数据盘上,不能放在系统所在的根盘,否则很容易复现我们踩过的这个坑。

第三类是创建请求突发时的并发竞争,这一类和沙箱存活数量是否打满完全无关,即使当前存活沙箱数远低于 80-100,也可能触发。具体场景是:短时间内同时发起大量创建请求时,请求会卡在 network-agent 分配 tap 设备这一步,表现为 unix socket 通信超时,SDK 侧收到 500 错误。我们的实测结果是,单节点同时发起大约 20 路创建请求还能正常完成,一旦超过这个数量,就会稳定卡在这个环节。这里需要区分两个容易混淆的概念:"稳定存活 80-100 个沙箱"和"同一时刻并发创建 80-100 个沙箱"不是一回事——存活数是一段时间内维持的总量,而创建是瞬时并发的动作,创建请求本身需要排队处理,两者的压力模型完全不同。

三类瓶颈对比表

表 1:三类瓶颈对比表

五、AutoPause 密度杠杆的评估与保守选择

除了识别以上瓶颈,我们也评估过一个能进一步提升密度的杠杆 AutoPause。它的出发点是:Agent 轨迹的执行过程中,大部分时间其实是在等待大模型推理返回结果,这段等待期沙箱是空闲的,但按现有的调度方式,空闲沙箱依然占着全额资源配额。

Cube v0.5.0 之后提供的 AutoPause 机制,能把长时间闲置的沙箱做一次快照存到磁盘,然后关闭对应的 MicroVM 释放物理资源,等新请求到达时再毫秒级唤醒。控制这个机制密度效果的参数,是 Cubelet 配置里的 paused_resource_release_ratio,默认值是 0.0。这个默认值的含义是,沙箱被 pause 之后,尽管物理层面的 CPU 和内存已经归还给宿主机,但调度器的账面记录仍然按占满计算,所以密度不会因为开启 AutoPause 而提升。

我们评估过把这个参数调到 0.7-0.8:按 agent 轨迹活跃占比大约 20% 来估算,账面密度理论上能提升到 3-4 倍,相当于单节点 350-470 个沙箱。但这个提升是有代价的:调高之后,resume 操作会变成尽力而为,如果节点瞬时资源不够,会直接返回 409 错误,也就是说唤醒不再是一个能稳定保证成功的操作。我们目前还没有在 RL 训练侧把"resume 失败之后自动重试"这套容错逻辑做成稳定策略,所以暂时没有调整这个参数,paused_resource_release_ratio 仍然保持默认的 0.0。好在这个参数支持热加载,不需要重启服务就能调整,等后续训练规模真正需要提升密度时,可以再重新评估。

六、写在最后

RL rollout 场景对沙箱密度的要求有三点不同于普通 Agent 服务:轨迹生命周期为分钟级,创建吞吐比稳态存活数更重要;编译、测试等阶段会将标称内存占满,超卖空间有限;轨迹绝大部分时间在等待 LLM 返回,密度仍有理论提升空间,但需要配套容错才能兑现。

对应的瓶颈有三类,表现各不相同:内存超卖兑现时 available 耗尽、宿主 OOM;磁盘水位超阈值时节点被移出调度;创建突发时卡在 network-agent 分配 tap fd。三类瓶颈需分开识别,才能定位调整方向。

我们的实践思路分三步:先按沙箱规格与调度参数推算理论上限,确认瓶颈落在哪个资源维度;再区分安全边界、账面天花板与实测稳态三个数字的含义,明确各自之间的差距来自超卖空间还是负载峰值;最后对每个密度特性先评估收益与代价,配套容错未就绪前保持默认,条件具备后再调整。

感谢云知声 Atlas 智算团队,对以上内容的贡献。

如果你有关于 CubeSandbox 的文章想贡献,欢迎提交 PR!前往 GitHub 贡献 →