从 Docker 容器到 MicroVM:一次 CubeSandbox 接入与生产事故治理复盘
一次横跨 WSL2、KVM、XFS、eBPF、DNS、模板、镜像、生命周期与生产运维的 AI Agent 沙箱迁移。
本文基于实习期间真实的代码变更、故障排查、测试环境事故和本地恢复经验整理。公开版本已做泛化与脱敏,不包含项目名称、内部编号、网络地址、账号、密钥或可追溯的部署标识。

前言:为什么我把 CubeSandbox 作为第一篇实习回顾
如果只看最终代码,CubeSandbox 好像只是我实习期间参与的 Agent 系统中的一个 SandboxProvider:
yaml
sandbox:
provider: cube
但真正把它跑起来后,我才意识到,沙箱并不是一个普通 SDK,也不是"把 Docker 换成 MicroVM"这么简单。
它同时涉及:
- Agent 如何分配、恢复和销毁执行环境;
- 宿主机是否具备真实可用的虚拟化能力;
- MicroVM 的镜像、RootFS、内存快照和模板如何生成;
- WSL2 中 XFS、bpffs、KVM、TAP 和 ARP 邻居表如何协同;
- 沙箱内的 DNS、TLS、出站网络和浏览器工具是否真的可用;
- 用户上传、Skills、工作目录和产物如何跨宿主与 MicroVM 同步;
- 服务重启、OOM、
kill -9或部署中断后,遗留实例如何回收; - 新宿主机、远端配置、systemd 和发布流程如何保证沙箱真正恢复。
这段经历很适合用来解释一个事实:
AI Agent 工程不是简单地调用大模型和工具。只要允许模型执行代码,系统就会迅速进入操作系统、虚拟化、分布式状态和生产运维的深水区。
我也先说明贡献边界:最初的 CubeSandbox provider、WSL2 两层架构、浏览器模板和测试环境接入由团队共同完成;我在后续真实使用中重点负责了 CubeSandbox 的空闲回收与孤儿实例治理,并持续参与本地故障定位、恢复链路验证和运行手册沉淀。本文复盘的是完整团队工程,但会明确区分个人实现与团队产出。
一、Agent 为什么需要真正的执行沙箱
传统聊天机器人只需要生成文本;Agent 则可能执行:
- Shell 命令;
- Python、Node.js 或其他代码;
- 文件读写、依赖安装;
- 浏览器自动化;
- Excel、PPT、Word、PDF 等产物生成;
- OCR、视频处理和网络请求;
- 用户自定义 Skill。
如果这些操作直接落到宿主机,模型生成的一条错误命令就可能影响:
- Agent 服务本身;
- 控制平面和数据库;
- 其他用户的数据;
- 平台密钥;
- 整台服务器。
因此,这套 Agent 系统的安全方向不是削弱 Agent 能力,而是把危险执行迁入隔离环境:用户可以在自己的沙箱内自由执行,但不能越过边界破坏宿主机、控制平面或其他用户。
长期语义是:
text
user = 顶层安全与存储边界
thread = 聊天与历史边界
sandbox = 用户共享的执行环境
线程不是长期的执行隔离边界。同一个用户的多个会话,应该尽可能恢复到同一个用户级沙箱,而不是为每个 thread 复制一套工作空间。
二、CubeSandbox 是什么
CubeSandbox 是腾讯云开源的 AI Agent 沙箱基础设施。官方将它描述为基于 RustVMM 与 KVM 的高性能 MicroVM 沙箱,支持单机部署和多节点扩展,并提供 E2B SDK 兼容接口。
官方核心设计包括:
- 每个沙箱拥有独立 Guest Linux Kernel;
- 使用预构建模板和内存快照加速启动;
- CubeAPI 提供 E2B 兼容 REST API;
- CubeMaster 负责集群级调度;
- Cubelet 管理节点上的沙箱生命周期;
- CubeShim 通过 containerd Shim v2 对接 MicroVM;
- CubeHypervisor 基于 RustVMM 与 KVM 管理虚拟机;
- CubeVS 使用 eBPF 提供网络数据面与隔离;
- CubeProxy 为沙箱暴露的 HTTP 服务提供路由;
- 新版本的 CubeEgress 提供 L7 出站域名过滤、凭证注入与审计。
官方给出的裸机基准是:可服务沙箱冷启动平均低于 60ms,单实例基础内存开销低于 5MB。这里必须强调,官方基准衡量的是经过预热和快照后的基础交付能力,不等于业务系统从 acquire 到完成 Skills、Uploads、DNS、TLS 和应用初始化的端到端时间。
截至本文整理时,CubeSandbox 官方 Changelog 的最新版本为 v0.5.0,已经加入 AutoPause/AutoResume、ARM64、Terraform 集群部署和网络安全增强。项目最初接入和集中排障时使用的是较早版本,因此本文中的很多补丁和运维经验都来自早期版本与真实部署环境之间的差距。
官方资料:

2.1 官方架构在业务 Agent 系统中的落点
业务 Agent 系统没有重写 Cube 的底层虚拟化,而是在应用层做了几件关键事情:
- 把
user_id或thread_id映射成稳定的 owner key。 - 使用确定性 sandbox id,避免并发请求重复创建。
- 把 Cube 远端 sandbox id 写入本地状态存储。
- 进程重启后优先连接旧 MicroVM,连接失败再创建。
- 在 acquire 后准备目录、同步上传和 Skills。
- 在读写和命令执行时刷新活动时间。
- 用 idle sweeper 和 orphan reaper 管理生命周期。
- 通过 DNS/TLS bridge 访问
*.cube.app代理地址。
三、为什么从 AioSandbox(Docker 沙箱)迁移
迁移前使用的是 AioSandboxProvider。本地模式下,它通过 Docker 或 Podman 启动一个 all-in-one 容器;远端模式也可以调用 provisioner 创建容器沙箱。
旧方案并非毫无安全措施。代码中支持:
no_new_privileges;- drop capabilities;
- PID 数量限制;
- 非 root 容器用户;
- 只读 RootFS;
- 用户级和线程级挂载;
- 空闲检查与 shutdown 清理;
- 本地状态恢复。
但它仍然属于共享宿主内核的容器隔离。对于执行来源不可信、能力高度开放、允许安装依赖和访问网络的 Agent,MicroVM 独立内核提供了更强的安全边界。
3.1 AioSandbox 与 CubeSandbox 对比
| 维度 | AioSandbox / Docker 容器 | CubeSandbox / MicroVM |
|---|---|---|
| 隔离边界 | Linux namespace、cgroup、capability,共享宿主内核 | 每个沙箱独立 Guest Kernel,KVM 硬件虚拟化 |
| 本地部署复杂度 | 较低,准备镜像和 Docker/Podman 即可 | 较高,需要 KVM、XFS、模板、快照、Cube 控制面和网络组件 |
| 启动方式 | 创建/恢复容器 | 从模板的 RootFS 与内存快照恢复 MicroVM |
| 文件语义 | 宿主目录 bind mount 到容器,天然共享 | MicroVM 内文件系统为运行时真相,需要显式同步和拉回 |
| Skills | 可直接只读挂载宿主 Skills 目录 | acquire 时同步到沙箱 Skills 目录,需要增量或哈希优化 |
| Uploads/Outputs | 宿主挂载,路径直达 | 上传同步到 MicroVM,产物需要从 MicroVM 拉回宿主缓存 |
| 网络 | 主要依赖 Docker 网络、NAT 和宿主规则 | TAP、CubeVS/eBPF、CoreDNS、CubeProxy、CubeEgress 等多层链路 |
| TLS/域名 | 通常直接访问映射端口 | E2B 风格 *.cube.app 域名,需要 DNS 与 CA 信任桥接 |
| 生命周期 | 容器 idle checker + shutdown cleanup | 应用层 sweeper、远端 metadata 对账、orphan reaper;新版本官方支持 AutoPause/Resume |
| 镜像更新 | 重建 Docker 镜像并重启容器 | 重建派生镜像、推 registry、创建新 template、等待 READY、切配置 |
| 故障定位 | 主要看容器、端口、挂载和进程 | 还要看模板阶段、快照、CubeMaster、Cubelet、Shim、MicroVM、DNS 和 eBPF |
| 适用场景 | 本地开发、可信代码、较低运维成本 | 高自由度 Agent、不可信代码、多人平台、生产安全隔离 |

原创工程图:容器依赖共享宿主内核,MicroVM 为每个沙箱提供独立 Guest Kernel。
这次迁移的本质不是"Cube 比 Docker 新",而是产品安全模型发生了变化:
当用户真正可以让 Agent 执行任意代码时,宿主内核不应该继续成为所有用户共享的最后一道边界。
代价也很现实:安全边界更强,并不意味着运维更简单。我们后来遇到的大部分问题,都来自 MicroVM 基础设施比单容器多出的那些层。
四、业务 Agent 系统的 CubeSandbox 集成设计
4.1 用户级沙箱映射
provider 优先使用 user_id 生成 owner key:
text
user_id
-> SHA-256 截断
-> user-<hash>
-> deterministic sandbox id
如果没有用户信息,才退化到 thread 级 key。这样既兼容旧线程语义,也为"一个用户一个沙箱"预留了迁移路径。
4.2 Acquire:恢复优先,创建兜底
owner 级锁非常重要。如果同一用户的两个 Agent 请求同时发现"没有沙箱",没有锁就可能创建两个远端 MicroVM,既浪费资源,又破坏用户级工作区语义。
4.3 文件语义:MicroVM 是运行时真相,宿主保留产品缓存
CubeSandbox 与 Docker bind mount 最大的区别是文件不再天然共享。
系统在 acquire 时创建以下逻辑目录:
text
/workspace
/uploads
/outputs
/skills/public
/skills/custom
文件流转分成三类:
- Uploads:先进入宿主的用户数据目录,再同步到 MicroVM。
- Skills:公共 Skills 和启用的自定义 Skills 同步到沙箱内的 Skills 目录。
- Outputs:Agent 在 MicroVM 中生成;Artifact、邮件、渠道发送或文件展示需要时,再拉回宿主缓存。
因此必须明确:
"模型能在沙箱里看到文件"和"前端能下载文件"是两条不同链路。
单纯验证 write_file 成功,并不能证明 Artifact API、分享、邮件或移动端下载可用。
4.4 E2B 兼容层并不等于零成本迁移
CubeAPI 对 E2B SDK 兼容,确实降低了应用层接入成本。应用侧主要通过配置 API 地址、访问凭据和 template id 连接 Cube。
但真正迁移时仍然需要处理:
- Cube v0.2.0 API 字段漂移;
sandboxID、sandbox_id和id等返回形态兼容;*.cube.app的跨 WSL DNS;- mkcert Root CA 与
certifi合并; - 文件同步语义;
- provider restart 后的连接恢复;
- 生命周期和孤儿实例治理。
所谓 drop-in replacement,主要解决的是 SDK 接口,而不是整套运行环境的运维复杂度。
五、本地 WSL2:为什么最终选择"两层架构"
Cube 官方开发环境曾提供 QEMU dev VM。放到 Windows + WSL2 上,会形成三层虚拟化:
text
Windows Hyper-V
-> WSL2
-> QEMU dev VM
-> Cube MicroVM
在一台常规开发笔记本的实测中,MicroVM 虽然能够继续启动,但 virtio 设备激活速度下降了一个数量级:vsock 激活需要数分钟,远超正常开发可接受的等待时间。
因此最终方案改成两个并列的 WSL2 distro:

原创工程图:Agent 应用与 Cube 控制面运行在两个并列的 WSL 环境中,由控制面直接创建 KVM MicroVM。
这条路径中,Cube 直接使用 WSL2 暴露的 KVM,不再经过额外 QEMU dev VM,绕过了三层嵌套的性能墙。
5.1 为什么单独建立 Cube 控制面 distro
Cube 安装器会:
- 修改 containerd 配置;
- 安装 Docker、MySQL、Redis 和多个控制面进程;
- 挂载 loop device;
- 写 sysctl;
- 使用
/data/cubelet; - 注册 systemd 服务;
- 创建网络设备和 iptables/eBPF 状态。
把这些直接装进日常开发 distro,会污染开发环境,也让故障恢复更加困难。因此 Cube 控制面应运行在独立、可重建的 WSL distro 中;Agent 应用仍运行在原来的开发 distro,通过网络访问 CubeAPI。
六、本地"修了无数次"的问题,到底都是什么
6.1 /data/cubelet 必须是 XFS
Cubelet 依赖 XFS project quota 管理每个沙箱的可写层大小,而 WSL2 根文件系统默认是 ext4。
解决办法是创建 loop-backed XFS:
bash
truncate -s 64G /data/cubelet.xfs
mkfs.xfs -q /data/cubelet.xfs
mkdir -p /data/cubelet
mount -o loop /data/cubelet.xfs /data/cubelet
WSL 休眠或 wsl --shutdown 后 loop mount 会消失,因此还需要写入 /etc/fstab,由恢复脚本重新挂载。
这类问题最迷惑的地方是:代码完全没变,但第二天启动时安装器突然又提示 /data/cubelet 不是 XFS。
6.2 v0.2.0 的 network-agent 依赖 bpffs
network-agent 会把 eBPF map 固定在 /sys/fs/bpf。WSL2 默认不一定挂载 bpffs。
缺失时,真实错误是:
text
pin map to /sys/fs/bpf/...: not on a bpf filesystem
但当时上游 Fatalf 只记录 FATAL,并没有结束进程;后续代码继续解引用未初始化对象,最终表现成 nil pointer dereference。
如果只看最后的 panic,很容易误判成 Go 空指针,而不是文件系统挂载问题。
修复是:
bash
mount -t bpf bpf /sys/fs/bpf
并持久化到 fstab。
6.3 WSL 冷启动时 ARP 邻居不是 NUD_REACHABLE
network-agent 启动时会读取默认网关 MAC,但刚启动的 WSL2 中,邻居项可能存在却还没有进入 NUD_REACHABLE。
结果是控制面启动失败,随后又被上面的 Fatalf 语义放大成空指针崩溃。
本地恢复逻辑会在启动 network-agent 前:
- 查询默认网关和网卡;
- ping 网关刷新邻居项;
- 读取 MAC;
- 执行
ip neigh replace ... nud reachable。
这个补丁必须是幂等的,因为每次重跑上游安装器,都可能覆盖 one-click 启动脚本。
6.4 tap_init_num 和预热池不适合单开发者 WSL
上游配置会预热大量 TAP、cgroup 和 1GiB storage layer。服务端高并发环境可以用预热换启动速度,但本地 WSL2 会付出明显代价:
- Cube 控制面环境启动变慢;
- network-agent 30 秒健康等待超时;
- Cubelet 插件链没有初始化;
- WSL 的 ext4.vhdx 迅速膨胀几十 GiB;
- template 在 DISTRIBUTING 阶段失败。
早期只把 tap_init_num 从 500 降到 10;后续本地维护基线进一步将:
text
tap_init_num -> 10
cgroup pool_size -> 64
storage pool_size -> 20
storage pool workers -> 1
这体现了一个常见的基础设施误区:
面向生产吞吐优化的默认值,不一定适合单开发者环境。
6.5 升级 v0.2.0 后,旧 Cubelet 状态让插件"静默消失"
这是最难定位的问题之一。
现象:
text
unknown service cubelet.services.images.v1.Images
Cubelet 端口在监听,socket 也存在,quickcheck 甚至可以 5/5 通过,但 template 总是在 PULLING 或 DISTRIBUTING 阶段失败。
根因是旧版本遗留在 /data/cubelet/state、root、storage 下的 schema 与 v0.2.0 不兼容。相关插件初始化失败后,被 loader 静默丢弃,所以本应注册的 gRPC service 根本不存在。
最终通过一段幂等的状态清理流程:
- 停止 Cube stack;
- 保留 XFS mount;
- 清除旧 Cubelet 状态目录;
- 重新启动;
- 重建 template。
修复后 template 才能完整走完:
text
PULLING -> UNPACKING -> DISTRIBUTING -> CREATING_TEMPLATE -> READY
6.6 Cubemaster 指标不是写一次就够了
本地创建 sandbox 偶发出现:
text
CubeMaster returned error code 130597: no more resource
实际并不是 CPU 和内存真的耗尽,而是 Cubemaster 认为节点 metrics 已过期。
一次性向 Redis 写 metrics 只能短暂恢复;调度器每次做决策都会检查 update_at 新鲜度。
因此最终不是"启动时 seed 一次",而是通过后台守护任务周期性刷新节点指标。
6.7 cube-api 默认占用 3000,与 Next.js 正面冲突
Cube upstream 默认把 CubeAPI 绑定到 3000,而很多前端开发服务器也经常使用这个端口。
如果 Cube 先启动:
- 前端开发服务器可能自动切到备用端口;
- 反向代理仍访问原端口;
- 用户访问到的是 CubeAPI;
- 认证 session 请求得到 HTML 404;
- 应用网关把它当成认证失败;
- 前端不断弹登录框。
一种可行的端口约定如下:
text
3000 = Agent Frontend
3030 = CubeAPI(示例独立端口)
而且不能只在当前 shell export。必须同时写入启动环境、systemd 或恢复脚本,因为 upstream 的 one-click 脚本不一定主动 source /etc/profile.d。
6.8 sandbox 能创建,不代表 DNS 和真实出站可用
本地曾出现一个非常典型的"半健康"状态:
- CubeAPI healthy;
- acquire 成功;
execute_command("true")成功;- 但
browser_fetch、urllib、天气 Skill 全部超时。
正常 DNS 链路是:
text
MicroVM /etc/resolv.conf
-> 169.254.254.53
-> 控制面环境 CoreDNS
-> 控制面环境 systemd-resolved / upstream DNS
故障时,MicroVM 的 /etc/resolv.conf 可能为空,或者缺少快速失败参数。最终本地维护基线同时从两层兜底:
- 宿主网络恢复流程修复 Cubelet dynamic conf、NAT、FORWARD 和 template netfile。
- sandbox provider 在 acquire 后检查
/etc/resolv.conf,为空或缺少选项时写入:
text
nameserver 169.254.254.53
nameserver 8.8.8.8
options ndots:0 timeout:1 attempts:1 single-request
最后一行很关键。如果 resolver 默认等待时间太长,getent 可能刚好卡 5 秒,而天气 Skill 的 curl --connect-timeout 5 同样是 5 秒,于是用户看到的是"天气接口超时",真实根因却是 DNS fallback 太慢。
修复后的本机验证:
- 公共天气服务域名可以在短时间内完成解析;
- Forecast API 返回 HTTP 200;
- 备用天气服务同样可以正常访问。
6.9 每次 acquire 全量同步 Skills,简单任务也会很慢
Cube MicroVM 不能像 Docker 那样直接 bind mount 宿主 Skills。早期每次 acquire 都重新删除并同步整个公共 Skills 目录和用户自定义 Skills。
结果是用户只是问天气,却先等待一轮大量文件同步,看起来像浏览器或天气 API 很慢。
后续本地维护版本为公共 Skills 和启用的自定义 Skills 计算 SHA-256 内容哈希,并在沙箱中写入:
text
/skills/.sync-marker
marker 一致时跳过全量同步;Skill 内容变化时,下一次 acquire 自动重新同步。
6.10 WSL 重启后"昨天能用,今天全坏了"
WSL 的 IP、loop mount、bpffs、ARP、后台 seeder 和 Cube 控制面进程都可能在重启后变化。
仅依赖人工记忆逐条恢复不可持续。最终形成了三层自愈:
- 宿主恢复脚本:幂等重建 mount、patch、预热参数、网络、metrics 和 API 端口。
- Agent 主启动流程:发现启用 CubeSandbox 时,自动确保 Cube 控制面健康。
- 后台 keepalive:定期检查 CubeAPI,不健康时重新修复并拉起控制面。
这条链路也曾因为跨 Shell 多层转义而生成错误脚本,例如变量在错误层提前展开,最终出现:
bash
while (( attempts < )); do
if [ -f "" ]; then
因此后续明确规定:复杂逻辑写进脚本,通过 stdin 传给控制面环境,避免在 PowerShell、wsl.exe 和 Bash 之间堆叠多层 one-liner。
七、测试环境真实事故:遗留 MicroVM 持续吞噬内存
7.1 现场现象
测试环境曾出现严重内存压力:
- 机器总内存在数十 GiB 量级;
- 内存使用率长期超过 80%;
- swap 被耗尽;
- 数十个 sandbox 仍处于 running;
- running 实例合计 RSS 达到数十 GiB;
- 单个
containerd-shim-cube-rs进程可能占用数百 MiB 到数 GiB; - OOM killer 最终杀掉应用网关和 Agent runtime 等业务进程。
这里的高占用与官方"基础开销低于 5MB"并不矛盾。官方数字是 MicroVM 基础运行时开销;事故现场统计的是配置了较大规格、加载浏览器和业务依赖、并长期运行的完整沙箱进程与工作负载。真正的问题也不是单实例基础开销,而是实例没有被释放,数量不断累积。
7.2 为什么会泄漏
当时 provider 有:
- 显式
release(); - 正常
shutdown()清理; - SDK keepalive。
但没有后台 idle sweeper。_last_activity 虽然被记录,却没有参与自动回收。
更麻烦的是,OOM、kill -9 或部署中断后:
- Agent 进程内的 sandbox registry 消失;
- 本地状态存储可能只记录一部分;
- CubeMaster 中旧实例仍然 running;
- 新进程没有对象可以调用
release()。
因此生命周期需要拆成三层:
text
idle sweeper = 正常使用结束后的回收
shutdown cleanup = 正常停服时的礼貌收尾
orphan reaper = OOM、kill -9、状态丢失后的事故兜底

原创工程图:正常实例按空闲时间回收;进程异常后,通过多源状态对账、dry-run 和二次确认治理孤儿实例。
7.3 我负责实现的 idle sweeper
第一阶段先解决低风险的正常生命周期:
- 空闲超时设置为小时级;
- 扫描周期设置为分钟级;
- 命令执行、文件读写会进入 active operation;
- active operation 期间不允许回收;
- 操作结束后刷新活动时间;
- shutdown 时停止 sweeper 线程。
关键点不是简单比较时间,而是防止长任务被误杀。最终 release 前还要再次在锁内确认:
- sandbox 仍然存在;
- 没有 active operation;
- 最后活动时间仍超过阈值。
7.4 我负责实现的 orphan reaper
第二阶段处理当前进程已经"忘记"的实例。
创建 Cube sandbox 时写入 metadata:
text
managed_by=<agent-system>
provider=cube
instance_id=<instance>
owner_hash=<owner>
template_id=<template>
reaper 定期对账三份状态:
text
当前进程内存
vs Agent 应用本地状态
vs Cube 远端 Sandbox.list()
候选分成:
managed_orphan:远端还在 running,明确由当前 Agent 系统创建,但本地 state 已经没有。stale_state:本地 state 还在,远端也在,但长期没有活动且当前进程未加载。dead_state:本地 state 仍引用一个已经不存在或不再 running 的远端实例。
为了避免误删人工 smoke 或其他系统实例,默认策略是:
yaml
orphan_reaper:
enabled: false
dry_run: true
即使关闭 dry-run,每次销毁前仍做二次确认,检查:
- 当前进程是否重新加载了该 sandbox;
- active operation 是否重新出现;
- state 是否重新写回;
- owner 对应的 sandbox id 是否变化。
这段经历让我对"安全清理"有了更具体的理解:
删除不是写一条
kill,而是建立可证明的归属、年龄、状态和竞态保护。
八、模板与派生镜像:不是改完 Dockerfile 就结束
Cube template 的官方生命周期分三步:
- 从 OCI image 构造 RootFS;
- 启动 MicroVM,等待系统和语言环境就绪,生成内存与状态快照;
- 注册并发布 template,后续实例从快照热启动。
业务 Agent 系统不能只使用官方 sandbox-code:latest,因为真实任务还需要:
- Chromium;
- 浏览器自动化工具;
- LibreOffice;
python-pptx、pypdf、PyMuPDF;- OCR;
- ffmpeg;
- pandoc;
- 额外的网页正文抓取工具。
因此团队构建了支持浏览器任务的派生镜像,再基于镜像创建 Cube template。
8.1 本地模板构建的真实耗时
早期纯 sandbox-code template 首次构建约十分钟;加入 Chromium、LibreOffice 和 Skill 依赖后,浏览器版 template 的首次构建需要十几分钟。
阶段通常是:
text
PULLING
-> UNPACKING
-> DISTRIBUTING
-> CREATING_TEMPLATE
-> READY
如果只看到失败结果、不记录卡在哪个 phase,排查会非常困难:
- PULLING:镜像、registry、网络;
- UNPACKING:磁盘、RootFS、XFS;
- DISTRIBUTING:Cubelet image/storage plugin;
- CREATING_TEMPLATE:MicroVM 启动、vsock、宿主机性能和快照。
8.2 一次派生镜像重建事故
后来为了给浏览器版沙箱镜像增加一个新的网页抓取工具,最初走错了路径:直接从官方 sandbox-code:latest 全量重建。
这会重复安装已有的 Chromium、浏览器自动化工具和大量系统依赖,也重新暴露远程软件源网络不稳定问题。
正确策略是 additive build:
dockerfile
FROM current-browser-enabled-sandbox-image
COPY tool-release.tar.gz /tmp/
RUN 安装新增工具并验证原有浏览器自动化工具仍存在
当时远程 npm 和 GitHub 下载多次 reset,最终改成本机下载 release 包、上传 build context、Dockerfile COPY 安装。
模板前两次创建仍失败,错误是:
text
Receive event timeout after 10000ms
进一步检查发现服务器 load average 已达到数十,内存使用率超过 85%,swap 被打满;少量运行多日、没有 Agent 状态引用的旧沙箱还在持续消耗多个 CPU 核心。
清理确认无引用的遗留实例后,新 template 才进入 READY。
这次复盘形成了明确规则:
- 先反查当前生效 template 使用的 base image。
- 首次构建才从官方基础镜像开始。
- 追加依赖时基于当前派生镜像做 additive build。
- 远程网络不稳定时,本机下载后上传。
- 创建 template 前检查 CPU、内存、swap 和长跑 sandbox。
- 只有 template READY 后才能切权威配置。
- 镜像/template 操作与 Agent 应用重启分开执行和验证。
九、跨宿主迁移与重启恢复:KVM 可用不等于 Cube 可用
一次新宿主迁移中,其他应用服务都已恢复,CubeSandbox 却持续启动超时。虽然目标机器具备 /dev/kvm、EPT 和嵌套虚拟化,但通用模拟 CPU 与不稳定的单核性能让 MicroVM 快照恢复退化成分钟级冷启动。
app-snapshot 保存的是已运行 MicroVM 的内存状态,与生成它的 CPU 和虚拟化环境强相关,不能像普通配置文件一样直接跨机器复制。后来形成的迁移门禁只有四步:
- 先检查 KVM、CPU 模型和嵌套能力。
- 在目标机真实执行 create/exec/release 并记录耗时。
- template 与 snapshot 在目标机重新生成。
- 沙箱数据面健康后,再迁移和切换业务流量。
宿主机重启还暴露过控制面未纳入 systemd、端口配置漂移、CA bundle 并发写入和地址变化后状态未更新等问题。最终将 Cube 核心进程、统一配置、健康检查和地址修复都纳入启动链路。
/dev/kvm存在只是静态条件;真实 MicroVM 创建、快照恢复和重启自愈才是上线条件。
十、恢复脚本与四层验证
多次故障后,本地恢复逻辑被拆成几类幂等能力:
| 恢复能力 | 目标 |
|---|---|
| 宿主恢复 | 确保 XFS、bpffs、端口和控制面状态正确 |
| 网络修复 | 恢复 ARP、NAT、FORWARD、DNS 与 egress |
| Cubelet 调优 | 缩小本地预热池,清理不兼容状态和无引用缓存 |
| 指标守护 | 持续刷新调度指标,避免误报资源不足 |
| 启动自愈 | 按依赖顺序启动,并通过 keepalive 自动恢复 |
这些脚本不是 Shell 命令的堆积,而是在声明系统最终应该满足什么状态,并保证可以重复执行。
验证也不能只停留在 CubeAPI /health,而要覆盖四层:
| 层级 | 最小验证 |
|---|---|
| 控制面 | CubeAPI、CubeMaster、Cubelet 和 READY template |
| 数据面 | create、exec、文件读写和 release |
| 网络工具链 | DNS、HTTP egress、Chromium 与浏览器工具 |
| 产品端到端 | 登录、真实 prompt、上传、Skills、产物生成与下载 |
acquire/exec/release能跑通,只能证明沙箱接口可用,不能证明 Agent 产品全链路可用。
十一、版本升级:让上游接管成熟能力
项目在早期版本上自行实现 idle sweeper 和 orphan reaper,是当时资源事故下的必要方案。新版本已经提供 AutoPause/AutoResume,应用层可以逐步把常规空闲管理交还平台,只保留异常状态对账和兜底治理。
升级时不能因为功能同名就直接删除旧逻辑,而应先在测试环境比较:
- 空闲判定和活动刷新语义;
- pause/resume 的资源收益与恢复时延;
- 失败状态和重试行为;
- 与现有 state、metadata 和 orphan reaper 的兼容性。
上游出现同名能力,不代表它与现有业务语义完全等价。
十二、最终沉淀的工程方法
- 区分控制面与数据面。 API healthy 不代表 MicroVM、DNS 和真实任务可用。
- 不要只看最后一行错误。 空指针可能来自 bpffs,资源不足可能来自过期 metrics,接口超时也可能来自 DNS。
- 对账多份状态。 生命周期治理至少要比较进程内存、本地 state 和远端实例。
- 把快照视为运行时产物。 template 可以描述,内存快照不能随意跨宿主复制。
- 重新校准默认值。 预热池、端口和超时都带有上游环境假设。
- 删除操作默认保守。 归属标记、grace period、dry-run 和二次确认缺一不可。
- 用真实用户链路验收。 最终必须覆盖上传、浏览器、Skills、产物生成与下载。
- 把事故经验固化成代码。 幂等脚本、preflight、测试和 runbook 才是可复用的工程资产。
一份精简的上线检查只需要抓住关键路径:
- 宿主机能够真实创建并恢复 MicroVM。
- XFS、bpffs、CPU、内存和磁盘满足要求。
- template READY,且在目标宿主本地生成。
- DNS、TLS、HTTP egress 和浏览器工具可用。
- provider 重启后能够恢复已有实例。
- idle 与 orphan 清理具备竞态保护和 dry-run。
- systemd、healthcheck 和重启恢复链路有效。
- 真实 UI 任务能够生成并下载产物。
十三、素材来源与贡献边界
本文基于实习期间的代码变更、故障日志、恢复脚本和运行手册整理,所有项目名称、内部编号、地址和服务标识均已脱敏。
团队共同完成了 CubeSandbox provider、WSL2 架构、浏览器模板和测试环境接入;我重点负责 idle sweeper、active operation 保护、metadata 与多端状态对账、dry-run orphan reaper,并持续参与 DNS、网络恢复、Skills 同步和本地自愈排障。
公开文章保留的是工程问题与解决方法,不将团队成果包装成个人成果。
结语
回头看这段经历,我最大的收获不是记住多少条修复命令,而是学会先画清控制面、数据面和状态边界,再用真实指标定位问题,用幂等脚本固化恢复,并通过端到端任务验证产品语义。
CubeSandbox 同时追求安全隔离、启动速度和部署密度,但基础设施能力只有穿过宿主机、网络、模板、状态和运维链路,最终在真实 Agent 任务中稳定工作,才算真正完成。