CubeSandbox 能把沙箱还原到什么程度

2026-10-29 实测报告。问题:用户在上一轮里改了环境(例如设了环境变量、起了 dev server),沙箱释放后再恢复,能不能原样回来?代价是什么?
本文的数据都来自本机实测,标 估算 的除外;引用 Cube 官方文档的数字会注明“官方”。
后续(2026-11-02):§9 的 AGS 结论已经用进产品,即工具沙箱模式下空闲 2 分钟带内存暂停、下次用工具时恢复,见 docs/architecture.html §3–§4。

结论

① 我们现在不在 Cube 上线上网关给每个会话起的是一个普通 Docker 容器,不是 Cube MicroVM。AGS(底座是 Cube)只做过本地 PoC。我们的服务器 9.135.181.73 本身是 KVM 虚拟机,没有 /dev/kvm、没开嵌套虚拟化,装不了 Cube,除非换 PVM 宿主内核并重启。
② Cube 带内存暂停 = 进程级还原暂停 60 秒再恢复:同一个 PID 的进程还在跑,shell 里 export 的变量、当前目录、shell 函数、后台任务、进程内存里的数据(300 MB 校验和一致)、沙箱内部的 TCP 连接、文件,全部原样。之前打开的 envd 输出流也没断。
③ 保不住的沙箱对外的 TCP 连接会被重置(ConnectionResetError),应用要自己重连;墙上时钟会跳过暂停的时长(单调时钟不计暂停时间)。快照只能在同一台机器上恢复,跨机需要 S3 后端,还要求内核和 CPU 一致。
④ AGS(x86 生产形态)复测:100/1002026-10-30 在腾讯云 AGS 广州实测:100 次带内存暂停 → 恢复,进程、内存、shell 状态全部保留;恢复 p50 2.3 秒。本机实验环境里的 8/18 失败率是嵌套 arm64 环境的问题,不代表生产(§9)。代价:暂停期间按“磁盘 + 内存规格”收存储费,2C4G 约 0.25 元/天。

回答原问题:假设用户在上一轮里,在一个还活着的 shell 里设了环境变量。换成 Cube 并用带内存暂停代替我们现在的“停容器”,这个变量在恢复后还在(实测 FOO=bar123 原样)。我们现在的 Docker 方案做不到:休眠会杀掉所有进程,只剩文件和对话。所以这是我们架构选型的问题,不是 Cube 的限制。(dsh 默认的 bash 工具每次调用都新起 shell,变量本来就留不住,按要求本文不讨论这一层。)

1. 先澄清:我们现在用的是什么

现在(线上)Cube / AGS
每个会话是什么一个 Docker 容器 cs-<id>,和宿主共享内核一台 MicroVM,有自己的内核(实测 guest 内核 6.6.119-cube)
空闲后怎么省资源先 session/close,再杀 dsh,最后 docker stop带内存暂停:把整台虚拟机的内存和 CPU 状态存成快照,释放 CPU 和内存
恢复后剩什么文件(/workspace、/tmp)和对话(session/resume)暂停前的一切,对外连接除外(见 §4)
部署位置9.135.181.73(TencentOS,AMD EPYC,本身是 KVM 虚拟机)需要 KVM:物理机、开了嵌套虚拟化的云主机,或者装了 PVM 内核的普通云主机

我们的服务器检查结果:CPU 标志里没有 svm,也没有 /dev/kvm,systemd-detect-virt 输出 kvm。Cube 文档给的出路是 PVM:一种不依赖硬件嵌套虚拟化的方案,要安装 Cube 提供的宿主机内核,用 grubby 设为默认后重启。它只支持 x86_64。交接文档里记着这台服务器 / 只读、/data 是别人的,换内核风险高,所以这次没有动它,改在本机搭环境(§3)。

2. 名词与还原层级

名词意思
暂停(pause)/ 恢复(resume)把运行中的 MicroVM 冻结成快照(CPU 寄存器、内存、文件系统改动),之后从快照继续运行,沙箱 ID 不变。在 E2B SDK 里叫 pause() / connect()
快照(snapshot)/ 回滚(rollback)给运行中的沙箱拍一份完整状态,之后可以原地回到这个状态,或者从它新建、分叉出沙箱。这是 Cube 特有的能力,E2B 没有
自动暂停 / 自动恢复空闲超过 timeout 秒后平台自动暂停;下一个请求到达时自动恢复
envd沙箱里的守护进程,提供“起进程、写 stdin、读 stdout、按 PID 重连”等接口。我们的网关就是通过它持有 dsh 的
层级做法恢复后有什么我们现在
L1 带内存暂停Cube pause进程、内存、shell 状态、文件全在没有
L1′ 快照回滚Cube snapshot + rollback回到任意一次拍快照时的状态,内存也一起回去没有
L2 冷恢复重建环境,再 session/resume只有文件和对话,进程都要重新起现状(约 2.3 秒)

3. 实验环境

层配置
宿主MacBook(Apple M4 Pro,64 GB,macOS 26)
Linux 虚拟机Lima + Apple Virtualization,开启嵌套虚拟化;Ubuntu 24.04 arm64,8 vCPU / 16 GiB;另挂 40 GiB XFS 盘(reflink=1)作 /data/cubelet。虚拟机里有 /dev/kvm
Cubev0.7.3 arm64 一键安装包(安装时把网段改成 10.100.0.0/18,避开 Lima 的 192.168.5.0/24)
沙箱模板来自官方镜像 sandbox-code:latest(多架构),每个沙箱 2 vCPU / 2000 MB,可写层 2 GiB
客户端cubesandbox 0.7.3 SDK 负责暂停和恢复;e2b 2.4.1 负责 envd 进程接口
这套环境和生产差得很远,耗时与可靠性数字只能当下限参考:
  • MicroVM 跑在“Mac → Apple 虚拟化 → Linux 虚拟机 → KVM”嵌套里,而且是 arm64。
  • 官方的 cube-proxy、cube-lifecycle-manager、cube-egress 镜像只发布了 amd64,只能用 qemu-user 模拟运行。cube-egress 在模拟下段错误,重启主机后一直起不来(它不在暂停 / 恢复的路径上)。
“恢复后状态在不在”这类结论不受影响:它们由快照机制决定,与速度无关。

4. 能还原到什么程度

实验做法(脚本 poc/cube-lab/exp_pause.py):

  1. 在沙箱里起两个长驻进程,都通过 envd 起,和网关持有 dsh 的方式一样:
    • 一个 bash,在里面执行 export FOO=bar123、cd /tmp、定义 shell 函数、起后台 sleep;
    • 一个模拟 dsh 的 Python 进程:在内存里放 300 MB 随机数据、一个计数器、一个只存在于进程内存里的随机串,持有一条沙箱内部的回环 TCP 连接和一条连到公网的 TCP 连接。
  2. 暂停 60 秒,然后恢复。
  3. 恢复后按原 PID 重连这两个进程,向 stdin 写命令,检查输出。
状态Cube 暂停 → 恢复现在的 Docker 休眠 → 唤醒证据
进程本身(同一 PID)保留丢恢复前后 commands.list() 都是 pid 32(bash)、35(agent)
活 shell 里 export 的环境变量保留丢FOO=bar123
shell 的当前目录、函数、后台任务保留丢PWD=/tmp,greet x → hi-x,jobs 里的 sleep(pid 34)还在
进程内存里的数据保留丢计数器接着从 n=2 开始;随机串不变;300 MB 数据的 sha256 前 16 位前后都是 4fab78f648647666
沙箱内部的 TCP 连接保留丢回环连接上收发正常
沙箱对外的 TCP 连接被重置丢连 tcpbin.com:4242 的连接恢复后收到 ConnectionResetError;官方文档也写明“面向外部的连接会断开,由应用层重连”
文件(/tmp、家目录、rootfs)保留保留/tmp/t.txt、/home/user/note.txt 内容不变
暂停前打开的 envd 输出流没断—恢复后的回复,在暂停前打开的那条流上也收到了
时钟墙钟跳变—这期间墙上时钟走了 98 秒,单调时钟只走了 33.1 秒:暂停的时间不计入单调时钟
主机整机重启后保留(2/3)文件和对话保留暂停后重启 Linux 虚拟机,沙箱仍是 paused;恢复后 FOO=survives-reboot、PWD=/tmp、n=41 都在。第三个沙箱之前已经恢复失败过 11 次,重启后整个消失

快照回滚:回到第 N 轮

实验(exp_auto.py):

  1. 起一个计数进程,喂 3 次,再写文件 v1;
  2. 拍快照,用时 0.05 秒;
  3. 再喂 4 次(计数到 7),把文件改成 v2;
  4. 回滚到快照,用时 0.5 秒。

回滚后,同一个进程下一次回复是 N 4,文件变回 v1:内存和文件一起回到了第 3 轮。第二次重跑时,回滚后的输出流断了(代理返回 502),所以记为 1/2 次干净成功。

产品上的意义:这能做成“撤销上一轮”或“从第 N 轮分叉”,连运行中的进程状态一起回去,这是冷恢复做不到的。

自动暂停 / 自动恢复

创建沙箱时设 timeout=20, lifecycle={on_timeout:"pause", auto_resume:true},空闲 26 秒后自动进入 paused。下一次读文件确实触发了自动恢复,但在本环境里 60 秒内一直返回 503,原因和 §6 的恢复失败相同。

注意:e2b SDK 的 Sandbox.connect() 会悄悄把空闲超时改成默认的 300 秒,前两次测试就是因为这个没等到自动暂停。

5. 耗时与资源

操作本机实测官方基准(x86 嵌套 KVM 云主机,xfs,1 并发,平均)
新建沙箱6.4–15.9 s56 ms
暂停0.10–0.30 s106 ms
恢复成功时 10.7–27.3 s;失败的卡在 31 s154 ms
拍快照 / 回滚0.05 s / 0.5 s97 ms / —
删除暂停中的沙箱18–28 ms,快照一起清掉,不用先唤醒—
对比:我们现在 Docker 冷恢复2.26–2.30 s(之前在服务器上实测,见 architecture.html §4)

本机比官方慢两个数量级,主要是多层嵌套虚拟化造成的。恢复慢的原因见 §6。

暂停写多少盘

Cube 的内存快照只写和模板快照不同的页(日志里的 PagemapAnon),所以写盘量跟着沙箱实际用的内存走,和 2000 MB 的内存规格无关:

沙箱内容暂停写入占规格暂停后宿主释放内存
空闲17.8 MB0.9%—
持有 300 MB 数据的进程(§4 的主实验)353.6 MB17.7%351 MB(3257 → 2906 MB)
rootfs 改动(每个快照)约 1.3 MB本次实验写得很少

用 du 看到的是 464–739 MB:内存快照文件是 2000 MB 的稀疏文件,从模板快照 reflink 克隆而来,du 把共享块也算进去了。表中是 hypervisor 日志里记录的实际写入字节数。

控制面本身的开销

零个沙箱时,整套 Cube 占用 2.7 GB 内存:mysqld 约 420 MB,cubelet 约 400 MB,cube-proxy 约 460 MB(含 qemu 模拟的开销)。一共 16 个 systemd 单元,安装目录 1.7 GB,Docker 镜像约 3 GB。

6. 可靠性与发现的问题

6.1 恢复不稳定(本机)

18 个沙箱的首次恢复中有 8 个成功,成功时耗时 10.7–27.3 秒,失败的全部卡在约 31 秒。Cubelet 日志里的失败原因每次都一样:

telnet [10.100.0.2] failed. errno:PortBindingFailed, costtime:30.08s

也就是说:hypervisor 层 22 毫秒就把虚拟机恢复好了,guest agent 的 vsock 连接也建立了。之后 Cubelet 要确认能连上沙箱的端口,这个 TCP 探测在 30 秒内没通,于是判定失败,把虚拟机杀掉。成功的耗时都接近 30 秒,像是前几个 SYN 包丢了、靠重传才通,在本环境里是“慢”和“刚好超时”的分别。

其他观察:

这一条决定 Cube 能不能用,但本机数据不足以下结论。官方在 x86 嵌套 KVM 云主机上的恢复是 154 毫秒,我们没有在那种环境里测过。上生产前必须在 x86 PVM 或物理机上复测恢复成功率和耗时。

6.2 一次跨沙箱的错误路由(复现 0/3)

观察到一次:
  1. 沙箱 A(915172…)处于 paused,主机重启后它恢复失败了 5 次;
  2. 随后发往 A 的 envd 命令被另一个正在运行的沙箱 B(81704…)执行了。证据:两者返回的 boot_id 都是 2d6a62a8…;看到的是 B 的环境变量 FOO=survives-reboot;A 自己的文件 /home/user/agent.py 不存在。
按同样的步骤复现了 3 次(2 次不重启、1 次重启),都没有复现:A 暂停时请求被拒绝,恢复后回答的是 A 自己。触发条件不明。猜测是恢复失败后 CubeProxy 留下了指向某个 guest IP 的旧路由,而这个 IP 后来分给了别的沙箱。

影响:一旦在生产上发生,就是一个用户的命令跑进了另一个用户的电脑。Cube 自带的每沙箱访问令牌在代理层按沙箱 ID 校验,挡不住“请求合法、却被转发到错误虚拟机”这种情况。如果用 Cube,网关应该在每次重连后做一次端到端身份校验:创建时在沙箱里写一个随机 nonce,重连后先读出来比对。另外要把这个现象报给上游。

6.3 其他坑

问题影响 / 处理
最新 e2b SDK(2.62)创建沙箱时调用 POST /v2/sandboxes,Cube 返回 405固定使用 e2b==2.4.1,这也是 Cube 示例里的版本
e2b 2.4.1 的 Sandbox.connect() 不会恢复暂停中的 Cube 沙箱暂停和恢复改用 Cube 的 API / SDK
e2b 的 connect() 会把空闲超时改成 300 秒自动暂停不按预期触发;每次 connect 都要显式传 timeout
主机重启后,cube-lifecycle-manager 进入崩溃循环(重启 429 次)Docker 里残留了一条没有名字的容器记录,要手动删除才能恢复。根因没查清,可能与 amd64 模拟有关。自动暂停依赖这个服务
暂停中的沙箱默认仍占用节点的调度配额(paused_resource_release_ratio=0)暂停只省电不省“名额”。本机攒了几个暂停沙箱后,新建开始返回 408(疑似配额原因)。调高 ratio 后,资源不足时恢复会被拒绝
arm64 版的 proxy、lifecycle、egress 镜像未发布只影响 arm64 部署

7. 代价

7.1 基础设施

7.2 存储

暂停一次的写盘量约等于沙箱实际用的内存。dsh 会话容器在 Docker 里实测:刚启动约 170 MiB,跑过几轮约 415 MiB。由此推算每个暂停会话约 0.2–0.45 GB 估算,1000 个暂停会话约 0.2–0.45 TB 估算。在 AGS 上,因为按声明规格计费,4 GiB 规格的会话就按 4 GiB 算。

7.3 网关要改的地方 估算

改动说明
新增 Cube 运行时,替换 internal/sandbox/docker.go新建、暂停、恢复、删除都走 Cube API;envd 改为通过 CubeProxy 访问(TLS + 域名)。这样的运行时我们的 PoC 已经有雏形
休眠方式改成暂停不再 session/close + 杀 dsh,dsh 跟着虚拟机一起冻结。恢复后按 PID 重连 dsh 的 stdout(实测可行),旧流甚至可能直接可用
恢复失败的兜底失败后等状态回到 paused 再重试;多次失败或快照丢失时,退回 L2 冷恢复(新建沙箱 + session/resume)。所以 workspace 和 dsh 会话日志仍要存在虚拟机之外
身份校验每次重连后比对沙箱里的 nonce(见 §6.2)
对外连接只在轮次之间暂停,这本来就是现状。dsh 连模型服务的 keep-alive 连接恢复后会被重置,需要实测 dsh 能否自动重试
镜像把我们的运行镜像做成 Cube 模板,并确认我们打包的 envd 0.5.14 与 Cube 兼容(Cube 自带的模板是 envd 0.2.0)

8. 建议与下一步

  1. 先在 x86 上复测恢复可靠性:已完成,AGS 上 100/100,见 §9。
  2. 复测通过后,把 L1 做进产品:空闲后暂停(而不是停容器),恢复失败时自动退回 L2。用户感知是“回来时什么都在”。已完成(2026-11-02):工具沙箱空闲 2 分钟带内存暂停;恢复失败或实例丢失时重建(工作区丢失,事件 hands: reset),尚未做 L2 的工作区还原。
  3. 评估用快照回滚做“撤销 / 分叉一轮”,这是冷恢复给不了的能力。
  4. 不论选不选 Cube,把 §6.2 的现象和 e2b SDK 兼容性问题提给上游。

9. AGS 实测(2026-10-30,腾讯云广州)

环境:公有云 AGS,沙箱工具 cs-tools-010(custom,2C4G,系统盘 20Gi,镜像 image/tools.Dockerfile)。测试程序 poc/ags-lab/agsbench 跑在我们的服务器 9.135.181.73 上,也就是将来 Agent Pool 的位置。到 AGS 广州数据面 ping 3–6 ms。原始数据:poc/ags-lab/s3-2026-10-30.jsonl。

9.1 暂停 / 恢复可靠性(S3)

5 台实例并行,每台 20 轮,共 100 轮。每轮:带内存暂停(PauseSandboxInstance(Memory=true))→ 停 20 秒 → 恢复(显式传 Timeout)→ 重新取 envd 令牌 → 按原 PID 重连 → 检查状态。沙箱里跑的是 §4 同款的模拟进程(300 MB 内存、计数器、回环连接、对外连接)和一个设了变量的 bash。

指标结果
恢复成功,且以下状态全部保留100 / 100
进程(同 PID)、进程内存(随机串和 300 MB 校验和)、计数器连续、回环 TCP100 / 100
bash 的 FOO=bar123、PWD=/tmp、shell 函数、后台任务100 / 100
对外 TCP 连接100 / 100 断开(ConnectionReset / BrokenPipe),与 Cube 文档和 §4 一致
暂停前打开的 envd 输出流100 / 100 断开,必须按 PID 重新 Connect(本机 Cube 上旧流还活着,这一点不同)
暂停耗时(调用到状态变 PAUSED)p50 10.5 s,p95 12.6 s,最短 6.5 s
恢复耗时(调用到状态变 RUNNING)p50 2.3 s,p95 2.6 s
恢复后 envd 可用p50 2.4 s,p95 2.8 s
新建实例到 RUNNING2.1–2.7 s

结论:本机实验环境(§6.1)的恢复失败是 arm64 + 多层嵌套环境的问题,生产形态上没有出现。恢复 2.3 秒和我们现在 Docker 冷恢复(2.3 秒)一样快,但进程和 shell 都在。暂停要 10 秒左右,应该在轮次结束后异步做,不在用户等待的路径上。

9.2 暂停期间的超时与计费

9.3 时延(S2)

见 agent-pool-design.html §7.1:从服务器到 AGS 沙箱,envd 读写文件、列目录、执行命令 p50 21–32 ms,p95 26–39 ms。

附录:复现方法

脚本在 poc/cube-lab/,虚拟机配置在 lima-cube.yaml。本机的实验虚拟机 cube 还在运行(占用 8 vCPU / 16 GiB 内存)。停止:limactl stop cube;彻底删除:limactl delete cube && limactl disk delete cubedata。

limactl disk create cubedata --size 40GiB --format raw
limactl start --name=cube poc/cube-lab/lima-cube.yaml
# 虚拟机内:mkfs.xfs -m reflink=1 /dev/vdb → /data/cubelet,安装 docker、qemu-user-static
# 安装 cube-sandbox-one-click-v0.7.3-arm64,设 CUBE_SANDBOX_NETWORK_CIDR=10.100.0.0/18
cubemastercli tpl create-from-image --image cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \
  --writable-layer-size 2G --expose-port 49999 --expose-port 49983 --probe 49999
pip install "cubesandbox>=0.2.0" "e2b==2.4.1"
python exp_pause.py 60      # 状态保留主实验
python exp_size.py          # 快照大小
python exp_auto.py          # 快照回滚、自动暂停
python iso_stage1.py; (重启主机); python iso_stage2.py   # 重启后恢复、路由隔离
脚本内容
agent.py沙箱里模拟 dsh 的进程:300 MB 内存、计数器、回环和对外 TCP
exp_pause.py§4 主实验,输出 JSON
exp_wait.py / exp_net.py暂停时长与恢复成败;恢复期间的网络探测
exp_size.py不同内存占用下的快照大小
mk_paused.py / check_after_reboot.py / retry_resume.py主机重启后恢复;失败后重试
isolation.py / iso_stage1.py / iso_stage2.py§6.2 的复现尝试
exp_auto.py / exp_idle2.py快照回滚、自动暂停 / 恢复