作者:花椒直播 CTO 封冰清、花椒直播基础架构部负责人王成龙。
本文根据两位作者刊载于「Cube Sandbox」公众号的《沙箱是选项,不是标配:花椒 Agent 平台的架构思考与 Cube 实践》整理为掘金版,由花椒技术部分享。
在花椒 Agent 平台接入沙箱之前,我们用单台服务器上的不同目录区分用户工作区。目录虽然分开了,隔离能力却不够:用户 A 理论上可以通过 Agent 操作到用户 B 的文件。
接入沙箱能解决这类隔离问题,但接下来还有一串工程问题:沙箱回收后,用户文件放在哪里?命令跑了很久还没结束,平台怎么继续收取输出?休眠恢复失败后,究竟该重试还是重建?
我们的做法是先把"命令在哪执行"抽成可配置的执行环境,再按每个 Agent 的需要接入沙箱。在选定 CubeSandbox 后,我们继续处理持久化、终端会话和资源管理,并向开源项目贡献了 Go SDK 和问题修复。
以下实践对应 2026 年 9 月原文发布时的部署:CubeSandbox 已在生产运行三个多月,所在的两套 Agent 平台合计落地约 30 个应用。这个数字说明应用落地范围,不代表沙箱并发量。
1. 先确定隔离需求,再决定哪里使用沙箱
搭建平台时,我们把业务逻辑留在具体 Agent 的配置中,平台负责通用的运行能力。其中一层叫"执行环境",回答的是一个具体问题:大模型要执行命令时,应该在哪台机器、哪个环境里执行?
一个 Agent 可以配置一个或多个执行环境,也可以动态注册、移除个人私有执行环境。它们可能运行在 Windows、Linux 或 macOS 上,部署形态也可能是裸金属、云服务器或 Kubernetes。
CubeSandbox 是接入这层服务的一种沙箱方案。本文涉及的实际部署在普通云服务器上,Kubernetes 部署是当时的后续计划。
我们判断是否使用沙箱,主要看执行环境是否由多个用户共同使用,以及他们的工作状态是否需要相互隔离:
| 使用方式 | 我们的处理 |
|---|---|
| Agent 管理专属机器,工作内容由单一主体使用 | 按具体任务评估,不统一强制套一层沙箱 |
| 同一执行环境服务多个用户,需要隔开各自的工作状态 | 通常使用沙箱 |
| 面向不同组织部署平台 | 使用独立实例,分别管理平台、数据和执行环境 |
花椒业务侧完成验证后,我们基于同一套 Agent Runtime,为集团中台部署了另一套独立实例。两者复用底层能力,但不共享控制面。我们在这个阶段优先保证组织之间的边界清晰,接受独立部署带来的资源开销。
这里的判断主要针对用户和组织之间的隔离。单用户场景是否还需要限制不可信代码的执行,要结合具体任务评估。
为什么选 CubeSandbox
目录隔离暴露问题后,我们评估了容器、传统虚拟机和 MicroVM 方案。启动开销、隔离要求、资源分配方式和成本,都会影响选择。
在当时的候选方案测试中,CubeSandbox 的启动表现和隔离能力符合我们的需求,因此选择接入。这里不展开没有统一测试口径的性能排名,后文重点讲接入之后实际要解决的问题。
2. 沙箱可以回收,用户文件要独立保存
选型完成后的第一个问题是存储。
我们最初把用户数据放在外部数据盘上,避免依赖云服务器的系统盘。继续考虑故障切换时,又遇到了限制:当时使用的普通数据盘无法同时挂载到多台云服务器。
于是,我们把需要持久保存的数据移到 NFS,也就是可以通过网络挂载的文件系统。执行环境守护进程再通过 CubeSandbox 的 host-mount 机制,把相应目录映射到沙箱内部。
这个关系可以简化为:
text
用户持久化目录(NFS)
↓ 云服务器挂载
执行环境守护进程
↓ host-mount 映射
会话对应的沙箱
↓
命令读取文件、写入产物
云服务器故障后,新机器可以挂载同一个 NFS,再创建沙箱继续访问已保存的文件。我们在会话粒度管理沙箱,一个会话对应一个沙箱。
这条恢复路径保住的是已经写入持久化目录的文件。原来运行中的进程仍然会丢失,不能把"新机器能读到文件"理解为"原来的命令从中断处继续执行"。
因此,接入时要分别验证两件事:沙箱重建后文件是否仍然可见,以及被中断的任务接下来怎样处理。文件持久化只回答了前一件事。
3. 让长命令、输出和退出状态有明确的协议
文件可以跨沙箱保留后,我们还需要管理执行过程。
一个命令可能很快返回,也可能持续运行,中途等待输入。平台需要知道:这次命令还在执行吗?上次读取之后又产生了哪些输出?它是正常退出、超时,还是已经无法连接?
CubeSandbox 的原生组件 envd 已经具备命令执行、文件读写和 PTY 等能力。PTY 可以理解为给程序提供一个可交互的终端。
我们仍然增加了一层自研终端服务,原因是希望自己定义平台使用的终端协议,让运行时的会话模型能够独立演进。同时,我们需要直接检查挂载进去的 Skill 和产物是否在沙箱内可见。
自研终端服务与 envd 保持并存,分工如下:
| 职责 | 承担方 |
|---|---|
| 沙箱创建、暂停、恢复、销毁 | Cube API / Cube SDK |
| 模板就绪、初始化,以及原生 SDK 的命令和文件调用 | envd |
| 本平台的命令会话、增量输出、标准输入和退出状态 | 自研终端服务 |
增加这层服务后,业务侧对接的是我们定义的终端协议,底层执行环境的差异由接入层处理。
一次命令怎样执行
下面按当前 CubeSandbox 路径展开。外层负责协调执行的进程称为 Worker,平台用它连接任务与具体沙箱。
- 准备沙箱和工作区。 Worker 根据会话找到或创建专属沙箱,准备上传文件、Skill 和持久化目录挂载。
- 建立命令会话。 Worker 通过 CubeProxy 访问沙箱内的自研终端服务,由后者为命令创建独立进程组。交互命令走 PTY,非交互命令分别捕获标准输出与标准错误。
- 保存并增量读取输出。 输出写入容量受限的内存环形缓冲区,每个输出块带有单调递增的
chunk_id。Worker 按游标读取新增内容,避免反复上报已经读取的输出。 - 区分快命令与长命令。 短命令直接返回最终状态;长命令返回
session_id,Worker 将它映射到平台的后续输入会话,继续轮询输出、传递输入并上报退出结果。 - 处理超时和回收。 超时会取消进程;会话结束后保留短暂时间,再回收相关状态。沙箱断连或终端服务持续不健康时,Worker 才将会话标记为丢失。
这里有几组状态需要分开处理:
| 现象 | 平台需要表达的状态 |
|---|---|
| 命令尚未结束 | 会话仍在运行,可以继续读取输出或输入内容 |
| 命令已经退出 | 返回退出状态与退出码 |
| 执行超过超时时间 | 取消进程,表达超时结果 |
| 沙箱断连或终端服务持续不健康 | 标记会话丢失,避免误判成正常退出 |
输出缓冲区有容量上限,也就需要明确输出截断的语义。这个缓冲区用于收取执行输出,不能据此承诺完整日志永久保留。
把这些状态与平台持久化的任务状态对应起来之后,执行层才能持续告诉上层"发生了什么",上层也才能据此决定下一步。
4. 开源接入之后,我们改了哪些代码
有些问题可以在接入层处理,有些问题需要回到开源项目本身。
Go SDK:连接资源应该由谁释放
我们开始使用 CubeSandbox 时,项目还没有 Go SDK,于是补了一版,覆盖生命周期管理、命令执行、文件读取和代理传输等能力,提交并合并了 PR #254:Add Go SDK。
随后,代码审查发现了一个资源归属问题:Sandbox.Close() 会清理共享 HTTP 客户端的空闲连接,影响其他复用连接的沙箱。
我们通过 PR #322 调整了连接清理的归属,将其集中到客户端层,并处理默认 HTTP Transport 被共享的问题。
这类接口命名很容易产生歧义。接入方需要分清:关闭本地客户端连接、结束一次命令会话、销毁远端沙箱,分别改变了哪个对象的生命周期。
挂载目录后,休眠恢复为何失败
另一个问题出现在 host-mount 场景:挂载了目录的沙箱在休眠后恢复失败,报错为 InvalidVirtioFsState。
我们定位到,快照生成前没有为 virtio-fs 准备好迁移元数据。virtio-fs 负责虚拟机与宿主机之间的文件共享,根 inode 丢失迁移状态后,恢复过程无法正确还原文件系统状态。
对应的 PR #341 已合并。改动围绕快照序列化前后的准备与清理、根 inode 的失败处理,以及虚拟机操作成功后的状态提交展开。
后续我们又提交了 PR #354,尝试进一步处理暂停、恢复失败后的状态一致性。这个改动后来被我们关闭,没有合并;测试后,我们仍无法确认它完整解决了相关问题。
截至原文记录时,我们对尚未解决的部分采用销毁沙箱、重新创建的处理方式,用户已持久化的文件由外部存储保留。这里仍然存在运行中进程无法延续的代价。
5. 资源规划:先查清预分配,再讨论回收与扩容
在资源配置上,我们也走过弯路。
最初,我们以为沙箱会按照实际用量动态分配资源,没有进一步检查预分配机制,也没有主动调整沙箱规格。后来宿主云服务器内存不足,监控触发告警,我们先紧急扩容了云服务器。
那次处理没有进一步确认,内存不足具体表现为 OOM、卡死还是变慢。事后检查 CubeSandbox 默认配置时,我们认为默认值仍适合当时的使用情况,因此没有继续调整。
后续我们把测试环境和生产环境按不同规格部署,并加入沙箱闲置回收,结合云服务器监控调整服务器规格。
对类似部署,资源规划需要同时看两层:单个沙箱的规格和预分配方式,以及宿主机承载多个沙箱后的总资源占用。即使一个命令暂时没有消耗太多资源,也不能直接推断整台机器还有足够容量继续创建沙箱。
6. 接入前可以逐项验证的清单
根据这次实践,我们建议把下面几项放进接入验证。它们是对前面问题的检查方法,需要结合自己的部署补充测试条件。
- 隔离对象: 哪些用户或组织必须相互隔开?验证工作目录和运行状态的访问边界,不只检查目录名称是否不同。
- 持久化: 创建文件后销毁沙箱,再重建并检查文件是否可见;单独记录运行中进程的处理方式。
- 挂载可见性: 实际检查 Skill、输入文件和输出目录在沙箱内是否存在,避免把挂载配置成功当成文件已经可用。
- 命令状态: 分别验证短命令、长命令、等待输入、非零退出和超时取消。
- 输出管理: 连续轮询是否重复读取?输出超过缓冲区容量时如何呈现截断?
- 故障恢复: 在自己的版本和挂载方式下验证暂停、恢复和重建,记录失败后的最终状态。
- 容量与回收: 查清沙箱规格、预分配方式、宿主机告警和闲置回收策略,并观察并发增加后的实际资源占用。
写在最后
我们把沙箱接入平台时,持续围绕两条边界做决定:哪些工作需要隔离,以及哪些执行行为必须由平台自己定义。
前一条决定沙箱在哪里使用;后一条决定持久化、命令会话、输出和故障状态怎样接进原有运行时。CubeSandbox 承担沙箱基础设施的职责,我们补上平台所需的执行协议,也把使用中发现的问题反馈到开源项目。
本文记录的是这套部署的经验,涉及的故障和处理方式对应当时使用的版本,不能直接外推到 CubeSandbox 的所有版本。若你也在接入 Agent 沙箱,欢迎交流:最先卡住你们的是隔离、持久化,还是长任务的状态管理?