---
url: https://cubesandbox.com/zh/blog/posts/2026-09-01-unisound-rl-rollout.md
description: >-
  Agent RL rollout 对沙箱的需求和普通 Agent
  服务不同：每条轨迹都需要干净隔离的执行环境，生命周期分钟级，数量规模化并行，沙箱里可能运行模型生成的任意代码。云知声团队基于 128 核 251 GiB
  单机配置、1 核 4096 MiB 沙箱规格，推导出调度上限 117、不超卖安全边界 58、实测稳态 80-100
  三组数值。本文记录了从规格配置、调度参数推导到实测瓶颈的完整过程。
---

# 云知声工程实践：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 向它下发沙箱创建和销毁指令。

![控制面 / 计算面节点职责划分](./assets/2026-09-01-unisound-rl-rollout/01-control-compute-split.jpg)

*图 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 要低。原因很直接，只要有一批沙箱同时撞上编译阶段的内存高峰，宿主机的物理内存会先于调度器的账面数字被撑爆。

![密度推导阶梯图](./assets/2026-09-01-unisound-rl-rollout/02-density-derivation.jpg)

*图 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 个沙箱"不是一回事——存活数是一段时间内维持的总量，而创建是瞬时并发的动作，创建请求本身需要排队处理，两者的压力模型完全不同。

![三类瓶颈对比表](./assets/2026-09-01-unisound-rl-rollout/03-bottleneck-comparison.jpg)

*表 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 智算团队，对以上内容的贡献。*
