强化学习和后训练工作流正在大量采用 Ray:训练器、推理引擎、rollout worker、数据管道和评估任务可以由同一个分布式运行时统一调度。但随着 agentic workflow 变得更复杂,模型生成的代码、工具调用和多轮交互也带来了新的问题:如何在不暴露宿主机的前提下,大规模、低延迟地执行不可信代码?
Google 与 Anyscale 正在推进一套实验性的 Ray 沙箱库,把 gVisor 提供的隔离环境直接纳入 Ray 的资源和 Actor 模型。这样,沙箱不再是集群外部的一套旁路服务,而是可以像其他 Ray 资源一样被放置、限额、恢复和扩缩容。
沙箱为什么适合建模成 Ray Actor
一个可用的沙箱需要处理的不只是“启动一个容器”:它要被调度到合适的节点,预留 CPU 和内存,管理生命周期,处理失败,并与周围的 rollout 或推理任务保持一致的资源边界。
Ray Sandboxing 通过一个 Ray Actor 表示高层沙箱:Ray 调度器决定 Actor 所在的节点并预留资源;Actor 管理沙箱生命周期;gVisor 则负责在该节点上提供隔离的执行环境。对使用者来说,调用沙箱就是普通的 Ray Actor 调用,因此沙箱可以位于集群中的任意节点。
从 Ray 2.58 开始,框架作者可以继续使用熟悉的 Ray API 管理这类环境。沙箱 API 覆盖了 agent 工作负载通常需要的操作:
- 从 OCI 镜像创建环境
- 设置 CPU、内存、环境变量和工作目录
- 配置网络
- 执行命令
- 读写、上传和下载文件
- 检查沙箱状态
- 终止或删除环境
一个最小的分布式执行示例
下面的示例假设集群节点已经具备 Ray 和对应的 gVisor 沙箱运行环境。它从 OCI 镜像创建一个只分配 1 个 CPU 和 512 MiB 内存的沙箱,并在其中执行 Python 命令:
import ray
from ray.experimental import sandbox
ray.init()
# 创建 gVisor 沙箱;调用返回一个可远程调用的 Ray Actor 代理
sb = sandbox.create(
cpu=1.0,
memory="512Mi",
image="python:3.12-slim",
)
result = ray.get(
sb.exec.remote(
"python -c 'import sys; print(sys.version)'"
)
)
print(result.stdout)
print(result.stderr)
这个模型的关键点是 sb.exec.remote(...)。命令不是在提交脚本的进程中同步执行,而是通过 Ray 的 Actor 调度机制发送到沙箱代理。实际项目中,可以把每个 rollout 任务绑定到一个沙箱,或者让沙箱负责执行模型生成的代码、运行测试以及收集输出。
运行前需要根据部署环境调整三项内容:image 应使用工作负载真正需要的 OCI 镜像,cpu 和 memory 应与并发量及代码复杂度匹配,同时应明确是否允许网络访问。对不可信代码而言,网络默认策略、文件挂载和凭据注入尤其重要。
在一个 Actor 内维护沙箱池
如果每次工具调用都创建和销毁环境,生命周期成本可能成为高频 rollout 的瓶颈。较低层的 SandboxRuntime 可以直接管理本地 gVisor 沙箱,因此可以在一个 Ray Actor 内预创建一组沙箱并复用:
import ray
from ray.experimental.sandbox.runtime import SandboxRuntime
@ray.remote
class SandboxPool:
def __init__(self, size: int = 3, image: str = "python:3.10-slim"):
self.runtime = SandboxRuntime()
self.sandboxes = [
self.runtime.create(image=image, memory="512Mi")
for _ in range(size)
]
def run_command(self, index: int, command: str):
return self.runtime.exec(self.sandboxes[index], command)
def close(self):
for sandbox_id in self.sandboxes:
self.runtime.delete(sandbox_id)
pool = SandboxPool.remote(size=3)
result = ray.get(
pool.run_command.remote(
0,
"python3 -c 'print(\"Hello from pool!\")'",
)
)
print(result.stdout)
ray.get(pool.close.remote())
这个池化方式适合批量代码执行、单元测试和多轮工具交互。生产实现还应补上索引分配、超时、异常回收和并发控制,避免两个任务同时使用同一个沙箱,也避免失败的命令让后续请求继承脏状态。对于需要强状态隔离的任务,执行完成后销毁沙箱通常比长期复用更容易审计。
gVisor 带来的隔离边界
模型生成的代码应被视为不可信输入。普通容器隔离仍然依赖宿主机内核,而 gVisor 通过用户态实现相当一部分 Linux 系统调用接口,在工作负载与宿主机内核之间增加了一层隔离。它兼容 OCI 镜像,也不要求把 Docker daemon 或宿主机 Docker socket 暴露给沙箱。
这对 agent 工作负载有三个实际价值:
- 可以继续使用现有的标准容器镜像。
- 沙箱能够按需创建,适合细粒度的分布式资源调度。
- 较低的启动延迟和内存开销有助于支撑大量短任务。
隔离并不等于自动获得完整的安全策略。应用仍应限制网络、控制文件系统可见范围,不把云凭据或宿主机敏感信息放进环境变量,并对 stdout、stderr、退出码和资源消耗设置上限。对于需要更强隔离或不同性能特征的场景,未来也可能采用 Agent Substrate、Kata Containers 等其他运行时。
落地时的检查清单
采用 Ray Sandboxing 时,可以按以下顺序验证:
- 确认集群版本和实验性 API 的兼容性,当前示例面向 Ray 2.58 起的能力。
- 为每类代码任务制作最小 OCI 镜像,避免把不必要的工具和凭据带入沙箱。
- 通过 Ray 资源声明控制并发,分别压测冷启动和沙箱池复用。
- 明确网络出口、工作目录、文件上传下载和环境变量策略。
- 为命令设置超时、CPU、内存和输出大小限制,并在失败后清理状态。
- 记录沙箱 ID、任务 ID、镜像摘要和执行结果,便于审计和重放。
Ray 的价值在于把隔离执行纳入已有的调度和 Actor 编程模型。它不会替团队决定安全边界,但能让安全沙箱与训练、推理和 rollout 工作负载使用同一套分布式控制面。对于需要频繁执行模型生成代码的系统,这是比在普通容器中直接运行更清晰、也更容易扩展的工程起点。