把 gVisor 变成 Ray 集群里的沙箱原语:为强化学习安全执行动态代码

2026-08-26 43 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

强化学习和后训练工作流正在大量采用 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 镜像,cpumemory 应与并发量及代码复杂度匹配,同时应明确是否允许网络访问。对不可信代码而言,网络默认策略、文件挂载和凭据注入尤其重要。

在一个 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 工作负载有三个实际价值:

  1. 可以继续使用现有的标准容器镜像。
  2. 沙箱能够按需创建,适合细粒度的分布式资源调度。
  3. 较低的启动延迟和内存开销有助于支撑大量短任务。

隔离并不等于自动获得完整的安全策略。应用仍应限制网络、控制文件系统可见范围,不把云凭据或宿主机敏感信息放进环境变量,并对 stdout、stderr、退出码和资源消耗设置上限。对于需要更强隔离或不同性能特征的场景,未来也可能采用 Agent Substrate、Kata Containers 等其他运行时。

落地时的检查清单

采用 Ray Sandboxing 时,可以按以下顺序验证:

  • 确认集群版本和实验性 API 的兼容性,当前示例面向 Ray 2.58 起的能力。
  • 为每类代码任务制作最小 OCI 镜像,避免把不必要的工具和凭据带入沙箱。
  • 通过 Ray 资源声明控制并发,分别压测冷启动和沙箱池复用。
  • 明确网络出口、工作目录、文件上传下载和环境变量策略。
  • 为命令设置超时、CPU、内存和输出大小限制,并在失败后清理状态。
  • 记录沙箱 ID、任务 ID、镜像摘要和执行结果,便于审计和重放。

Ray 的价值在于把隔离执行纳入已有的调度和 Actor 编程模型。它不会替团队决定安全边界,但能让安全沙箱与训练、推理和 rollout 工作负载使用同一套分布式控制面。对于需要频繁执行模型生成代码的系统,这是比在普通容器中直接运行更清晰、也更容易扩展的工程起点。


相关推荐