CubeSandbox 能把沙箱还原到什么程度
2026-10-29 实测报告。问题:用户在上一轮里改了环境(例如设了环境变量、起了 dev server),沙箱释放后再恢复,能不能原样回来?代价是什么?
本文的数据都来自本机实测,标 估算 的除外;引用 Cube 官方文档的数字会注明“官方”。
后续(2026-11-02):§9 的 AGS 结论已经用进产品,即工具沙箱模式下空闲 2 分钟带内存暂停、下次用工具时恢复,见 docs/architecture.html §3–§4。
结论
9.135.181.73 本身是 KVM 虚拟机,没有 /dev/kvm、没开嵌套虚拟化,装不了 Cube,除非换 PVM 宿主内核并重启。ConnectionResetError),应用要自己重连;墙上时钟会跳过暂停的时长(单调时钟不计暂停时间)。快照只能在同一台机器上恢复,跨机需要 S3 后端,还要求内核和 CPU 一致。回答原问题:假设用户在上一轮里,在一个还活着的 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 |
| Cube | v0.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):
- 在沙箱里起两个长驻进程,都通过 envd 起,和网关持有 dsh 的方式一样:
- 一个
bash,在里面执行export FOO=bar123、cd /tmp、定义 shell 函数、起后台sleep; - 一个模拟 dsh 的 Python 进程:在内存里放 300 MB 随机数据、一个计数器、一个只存在于进程内存里的随机串,持有一条沙箱内部的回环 TCP 连接和一条连到公网的 TCP 连接。
- 一个
- 暂停 60 秒,然后恢复。
- 恢复后按原 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):
- 起一个计数进程,喂 3 次,再写文件
v1; - 拍快照,用时 0.05 秒;
- 再喂 4 次(计数到 7),把文件改成
v2; - 回滚到快照,用时 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 s | 56 ms |
| 暂停 | 0.10–0.30 s | 106 ms |
| 恢复 | 成功时 10.7–27.3 s;失败的卡在 31 s | 154 ms |
| 拍快照 / 回滚 | 0.05 s / 0.5 s | 97 ms / — |
| 删除暂停中的沙箱 | 18–28 ms,快照一起清掉,不用先唤醒 | — |
| 对比:我们现在 Docker 冷恢复 | 2.26–2.30 s(之前在服务器上实测,见 architecture.html §4) | |
本机比官方慢两个数量级,主要是多层嵌套虚拟化造成的。恢复慢的原因见 §6。
暂停写多少盘
Cube 的内存快照只写和模板快照不同的页(日志里的 PagemapAnon),所以写盘量跟着沙箱实际用的内存走,和 2000 MB 的内存规格无关:
| 沙箱内容 | 暂停写入 | 占规格 | 暂停后宿主释放内存 |
|---|---|---|---|
| 空闲 | 17.8 MB | 0.9% | — |
| 持有 300 MB 数据的进程(§4 的主实验) | 353.6 MB | 17.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 包丢了、靠重传才通,在本环境里是“慢”和“刚好超时”的分别。
其他观察:
- 恢复失败的沙箱状态会先显示
running约 6 秒,再回到paused,快照不丢,可以重试。 - 有的沙箱重试几次后成功(一个在第 4 次);有的一直失败(一个失败 11 次,主机重启后还消失了)。
- 把 CubeMaster 的
common_timeout_insec从 30 调到 120 无效,30 秒是 Cubelet 探测自己的上限,配置里没有暴露。 - 暂停时长与成败无关(暂停 0/2/5/10 秒都有成有败)。
6.2 一次跨沙箱的错误路由(复现 0/3)
- 沙箱 A(
915172…)处于paused,主机重启后它恢复失败了 5 次; - 随后发往 A 的 envd 命令被另一个正在运行的沙箱 B(
81704…)执行了。证据:两者返回的boot_id都是2d6a62a8…;看到的是 B 的环境变量FOO=survives-reboot;A 自己的文件/home/user/agent.py不存在。
影响:一旦在生产上发生,就是一个用户的命令跑进了另一个用户的电脑。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 基础设施
- 需要 KVM,有三条路:
- 现有服务器装 PVM 宿主内核并重启:只支持 x86;这台服务器
/只读、跑着线上网关,不建议。 - 新申请一台物理机、开了嵌套虚拟化的云主机,或装好 PVM 内核的 OpenCloudOS 9 云主机。
- 直接用 AGS(托管的 Cube):没有运维负担,但要账号。
- 现有服务器装 PVM 宿主内核并重启:只支持 x86;这台服务器
- AGS 的计费(引自
docs/research/ags-custom-image.md,未实测):- 带内存暂停期间,CPU 和内存停止计费;
- 存储按“磁盘实际用量 + 声明的内存规格”乘休眠态单价计费,没有免费额度;
- 第三方实测每个账号最多 10 个暂停实例;
- 暂停能保留多久,官方没有说明。
- 自建 Cube:控制面要运维 16 个服务(MySQL、Redis、MinIO、CubeProxy、CoreDNS、生命周期管理、出口代理等)。暂停快照默认只能在本机恢复;要跨机恢复,得用 S3 后端,每个节点预留约 2 核 + 19 GiB 内存给 s3lvol(官方),还要求各节点内核和 CPU 一致。
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. 建议与下一步
先在 x86 上复测恢复可靠性:已完成,AGS 上 100/100,见 §9。- 复测通过后,把 L1 做进产品:空闲后暂停(而不是停容器),恢复失败时自动退回 L2。用户感知是“回来时什么都在”。已完成(2026-11-02):工具沙箱空闲 2 分钟带内存暂停;恢复失败或实例丢失时重建(工作区丢失,事件
hands: reset),尚未做 L2 的工作区还原。 - 评估用快照回滚做“撤销 / 分叉一轮”,这是冷恢复给不了的能力。
- 不论选不选 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 校验和)、计数器连续、回环 TCP | 100 / 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 |
| 新建实例到 RUNNING | 2.1–2.7 s |
结论:本机实验环境(§6.1)的恢复失败是 arm64 + 多层嵌套环境的问题,生产形态上没有出现。恢复 2.3 秒和我们现在 Docker 冷恢复(2.3 秒)一样快,但进程和 shell 都在。暂停要 10 秒左右,应该在轮次结束后异步做,不在用户等待的路径上。
9.2 暂停期间的超时与计费
- 暂停期间超时不计时:实例设 5 分钟超时,启动后立刻暂停,8 分钟后仍是
PAUSED,ExpiresAt字段在暂停时消失。所以“单实例最长 24 小时”只约束运行时间。暂停最长能保留多久,官方没写;我留了一台暂停实例(tuxh4bms…)观察。 - 价格(官方 1814/133249,各地域统一):CPU 0.000081 元/核/秒,内存 0.000025 元/GiB/秒,系统盘和休眠存储都是 0.0021 元/GiB/小时;运行时系统盘 15 GiB 内免费,暂停时没有免费额度。带内存暂停的计费容量 = 磁盘实际用量 + 声明的内存规格。
- 折算 2C4G:运行约 0.94 元/小时;带内存暂停约 0.011 元/小时(约 0.25 元/天、7.6 元/月),按 1 GiB 磁盘估算。不带内存暂停只收磁盘,约 0.05 元/天。
- 超时最小值实际是 300 秒(API 文档写 30 秒)。
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 | 快照回滚、自动暂停 / 恢复 |