CubeSandbox 架构篇

CubeSandbox 是腾讯云在 2026 年开源的 AI Agent 沙箱,仓库是 TencentCloud/CubeSandbox,协议 Apache-2.0。每台沙箱是一台带独立内核的 KVM 微虚拟机,对外兼容 E2B 的 SDK,官方给出的单并发创建时间在 60 毫秒以内。

这是四篇系列的第一篇,讲整体设计和组件分工。另外三篇分别拆开讲存储、网络和虚拟化。文中的描述以仓库源码为准,和官方文档不一致的地方会指出来。

先把难题说清楚

Agent 用沙箱的方式和传统云主机很不一样。一次对话里可能要开好几台,开完马上就用,用几秒或几分钟就扔;同一台机器上要挤几千台;里面跑的是模型生成的代码,不能信任。

这几条放在一起很难同时满足。容器启动快、密度高,但和宿主机共享内核,一个内核漏洞就能逃逸。虚拟机隔离够硬,可冷启动一台要走固件、内核、init、服务初始化,少说几百毫秒,还要为每台预留整份内存。

CubeSandbox 的回答可以压成一句话:把冷启动的成本挪到模板阶段,每台节点只付一次;运行时的创建只做本机克隆和快照恢复。模板构建时,节点真的冷启动一台虚拟机,等服务就绪后把内存和磁盘冻结成快照。之后每次创建,磁盘用 XFS 的 reflink 克隆,内存把快照文件直接映射进来,几乎不搬数据。

这个思路决定了后面的许多设计:调度器必须知道哪台节点上已经有这份模板;存储必须建在支持 reflink 的本地 XFS 上;暂停、克隆、回滚都可以归结成“拍快照”和“从快照创建”的组合。

组件全景

E2B SDK / HTTP 客户端 控制面 CubeAPI Rust · E2B 兼容 REST CubeOps Go · 节点清单 · 运维 TemplateCenter Go · OCI → ext4 MySQL 模板 · 规格 · 快照绑定 lifecycle-manager Go · 空闲暂停 · 唤醒 CubeMaster Go · 调度 · 编排 Redis 路由 · 事件流 · 锁 CubeProxy OpenResty · 入站路由 REST HTTP 访问沙箱里的服务 计算节点(每台一套) gRPC Cubelet Go · 节点上的总编排:克隆盘、配网络、拉起 Shim containerd containerd-shim-cube-rs(每个沙箱一个进程) Shim v2 CubeHypervisor rust-vmm · KVM · 以库的形式链入 MicroVM(独立内核) cube-agent · envd :49983 · 用户进程 和 Shim 之间走 vsock CubeCoW Rust 库 · XFS reflink / S3 卷 CubeVS eBPF · TAP / 网卡 / cube-dev CubeEgress OpenResty · L7 出站代理 TAP TPROXY

黑色实线是创建沙箱的控制路径,蓝色虚线是访问沙箱和沙箱出网的数据流量。CubeMaster 还读写 Redis 和 MySQL,并从 CubeOps 拿节点清单,图里省略了这些连线。CubeCoW 和 CubeVS 都在 Cubelet 进程里被直接调用,不是独立服务。

组件分成两层。控制面负责接请求、调度、记账,计算节点负责真正把虚拟机跑起来。

组件语言位置做什么
CubeAPI Rust 控制面 兼容 E2B 的 REST 网关,鉴权、限流,翻译成内部 HTTP 调用
CubeMaster Go 控制面 调度、编排创建/暂停/恢复/销毁、模板状态
CubeOps Go 控制面 节点清单的权威来源、运维接口、WebUI 后端
TemplateCenter Go 控制面 把 OCI 镜像构建成 ext4 镜像
lifecycle-manager Go 控制面 空闲暂停、请求到达时唤醒
CubeProxy OpenResty 入口 把访问沙箱的请求路由到正确的节点和端口
Cubelet Go 每台节点 节点上的总编排
CubeShim Rust 每台沙箱一个进程 containerd Shim v2,进程里内嵌 VMM
CubeHypervisor Rust 同上 基于 rust-vmm 和 Cloud Hypervisor 的 VMM
CubeCoW Rust Cubelet 进程内 XFS reflink 和 S3 两种后端的卷与快照
CubeVS Go + eBPF Cubelet 进程内 + 内核 每沙箱的 NAT、连接跟踪、出站策略
CubeEgress OpenResty 每台节点 L7 出站代理,域名过滤、凭证注入、审计

存储侧还有一个可选的 CubeS3lvol,用 SPDK 把 S3 做成写时复制的块设备,供跨节点暂停恢复使用,放在存储篇里讲。

控制面:北向 HTTP,南向 gRPC

请求从 SDK 进来,先到 CubeAPI。它用 Axum 写成,自己不连数据库,只做三件事:按 E2B 的接口格式收请求,按配置做 API Key 校验或调用外部鉴权回调,然后用 HTTP 调 CubeMaster 的 /cube/sandbox 系列接口。官方架构文档的时序图把这一跳画成了 gRPC,源码里是 HTTP。

CubeMaster 是控制面的核心。它到每台节点 Cubelet 的调用才是 gRPC,端口 9999。它的状态分两处放:

所以“控制面无状态、一切在 Redis”这个说法不准确。CubeAPI 确实可以随便横向加;CubeMaster 依赖共享的 MySQL 和 Redis,并在进程里缓存一份节点资源视图供调度使用。

0.7.0 之后又拆出了 CubeOps,节点的注册、封锁、版本矩阵都以它为准,WebUI 的后端也从直连 CubeAPI 改成了经过它。TemplateCenter 和 CubeMaster 的分工是“前者管字节,后者管状态”:构建镜像、上传制品由 TemplateCenter 做;构建任务走到哪个阶段、副本分发到哪些节点,由 CubeMaster 记。

一次创建请求

SDK CubeAPI CubeMaster Cubelet CubeShim 客户机 Redis POST /sandboxes POST /cube/sandbox 过滤 → 打分 → 选节点 gRPC Create ① 分配 ID · 确认模板在本机 ② 并行:克隆 rootfs · 分 TAP · 挂卷 ③ 主机侧 cgroup ④ 起容器 经 containerd mmap 内存镜像 · 接设备 从快照恢复 vCPU vsock 就绪 CreateSandbox 任务已启动 IP · 端口映射 写代理路由 · 发生命周期事件(同时把规格写进 MySQL) 201 sandbox_id

节点内部的 ①–④ 是 Cubelet 的创建工作流:步骤之间串行,同一步里的插件并行。橙色那一步才真正碰到虚拟机,也只是把已经烤好的快照映射回来。

CubeMaster 收到创建请求后,先把模板里的规格、默认超时、网络策略、卷合并进请求,然后交给调度器选节点。

调度流程仿照 Kubernetes:先预选,再让所有启用的过滤器并行过一遍,取交集,然后加权打分,在最高分附近随机挑一台。默认的过滤器是 CPU、内存、模板本地性和“这台节点正在创建的沙箱数”。其中模板本地性最能体现这套系统的特点:只有本机已经有这份模板快照的节点才算候选。因为毫秒级创建的前提就是模板在本地盘上,调度到一台没有模板的节点,就得现场下载、冷启动、拍快照,等于退回慢路径。

选中节点后,CubeMaster 发起 gRPC Create。Cubelet 内部的创建是一条配置出来的工作流,分四步:

  1. 分配沙箱 ID,确认本机有可用的模板快照。
  1. 并行准备资源:用 CubeCoW 从模板克隆一份私有的可写层,从池里取一块 TAP 设备并下发网络策略,挂载数据卷,写本地元数据。
  1. 建宿主机侧的 cgroup。
  1. 通过 containerd 创建并启动“容器”,containerd 会拉起一个 CubeShim 进程。

步与步之间串行,同一步里的插件并行。第二步里最耗时的操作也只是几次元数据级的调用:reflink 克隆不拷数据;TAP 设备是 Cubelet 启动后在后台预先建好的,默认配置是一池 500 块,每块现建要六十多毫秒,正好省掉。

CubeShim 进程里直接链着 VMM 库。它从快照恢复虚拟机:把内存镜像文件映射进来,接回块设备、网卡和 vsock,恢复 vCPU。客户机里的 agent 本来就在快照里跑着,恢复后通过一个专用的通知设备报告“vsock 已就绪”,Shim 连上它,下发 CreateSandbox 把存储和网络配置补齐。

结果沿原路返回。CubeMaster 拿到沙箱的 IP 和端口映射后,并行做两件事:把路由写进 Redis 供 CubeProxy 使用,把沙箱规格写进 MySQL 供以后暂停恢复使用;再往 Redis 的生命周期事件流里追加一条“已创建”。最后 CubeAPI 拼出 E2B 格式的响应。

访问沙箱里的服务

创建成功后,SDK 调用 get_host(port) 拿到的地址形如 端口-沙箱ID.域名。执行代码、读写文件,都是对这个地址发 HTTP 请求,由客户机里的 envd 处理(默认端口 49983)。这条路不经过 CubeAPI,也不经过 vsock,而是走 CubeProxy。

CubeProxy 是一组 OpenResty。它从 Host 头里拆出端口和沙箱 ID;不方便配泛域名时,也支持 /sandbox/<沙箱ID>/<端口>/... 这种路径写法。拿到 ID 后查 Redis 里的路由,找到沙箱所在节点和映射出来的主机端口,转发过去。节点网卡上的 eBPF 程序再把这个主机端口改写成沙箱里的监听端口。

为了不让每个请求都打 Redis,lifecycle-manager 会把路由和状态主动推送到每个 CubeProxy 副本的共享内存里。CubeProxy 启动时往 Redis 注册自己并定期心跳,lifecycle-manager 据此发现所有在线副本,不需要静态配置。

暂停、恢复与超时

kill · 空闲超时且 on_timeout=kill(默认) running 占着 CPU 和内存 pausing 代理返回 503 paused 只剩盘上的快照 terminated 代理返回 410 resuming 同一个 ID 重新创建 空闲 · pause 快照落盘 kill 请求到达 · auto_resume restore 完成

暂停不是让 vCPU 停在原地,而是把内存和磁盘写成快照、让整个 VMM 进程退出。恢复走的是一次带暂停快照的创建,配了 S3 后端时可以落到另一台节点上。

沙箱可以设空闲超时,单位秒。不传时用 CubeMaster 的 default_timeout_insec,仓库默认值是 -1,也就是永不因空闲回收。超时后的动作默认是销毁;设成 on_timeout=pause 则改为暂停。

暂停不是让 vCPU 停在原地。Cubelet 让 VMM 依次做暂停、拍快照、删除虚拟机,Shim 进程随之退出,CPU 和内存全部还给宿主机,只留下盘上的内存镜像和可写层快照。恢复则是用同一个沙箱 ID 再走一次创建,只是请求里带上暂停快照的位置。这样设计的好处是恢复和创建共用一条代码路径;配了 S3 后端时,暂停包可以上传到对象存储,恢复也就可以落到另一台节点上。

空闲扫描由 lifecycle-manager 做。沙箱是否活跃,是在 CubeProxy 上看到的:每个代理副本记下每台沙箱最后一次被访问的时间,lifecycle-manager 定期轮询所有副本,取最大值。沙箱的元数据则来自 Redis 里的生命周期事件流。它自己刚重启时有一段宽限期,免得还没收到活跃记录就把所有沙箱当成空闲。Kubernetes 部署默认两个副本,用 Redis 的 SET NX PX 租约选出一个 leader 做扫描;另一个副本照样消费事件、照样能处理唤醒请求。每台沙箱在 Redis 里有一个状态键,暂停和恢复都要先抢到它,跨副本的并发操作因此被串行化。

打开 auto_resume 后,请求打到一台已暂停的沙箱时,CubeProxy 会调 lifecycle-manager 的唤醒接口,等沙箱恢复后再转发。沙箱正在暂停中时返回 503 并带 Retry-After,已经销毁则返回 410。官方文档把典型的恢复耗时写成亚秒到一秒。暂停本身不取消空闲销毁,暂停太久仍会被清理。

调度器默认把暂停的沙箱仍算作占着 CPU 和内存,这样恢复时一定有配额。paused_resource_release_ratio 可以把这部分按比例还给调度器,代价是恢复时要重新准入,节点放不下就返回 409。

模板从哪里来

模板是这套系统的地基,构建流程放在存储篇里展开,这里只说控制面的分工。用户提交一个 OCI 镜像,CubeMaster 建一条构建任务,TemplateCenter 拉镜像、解包、打成 ext4、上传到制品存储,然后 CubeMaster 挑选节点逐台下发副本。每台节点各自下载镜像、冷启动一台临时虚拟机、等配置好的 HTTP 探针返回 2xx,再把内存和磁盘冻结成快照,登记成本地模板并上报给调度器。

运行中的沙箱也可以提交成新模板,提交那一刻的文件和内存一起进模板。克隆和回滚同样建立在快照上:克隆是先给源沙箱拍一份临时快照,再从它创建 N 台;回滚是从目标快照派生一份新的可写层,再从目标内存快照恢复。

部署形态

单机一键部署用 systemd 管理所有进程:控制面的 CubeMaster、CubeAPI、CubeOps、TemplateCenter、lifecycle-manager、CubeProxy、WebUI(默认 :12088),依赖的 MySQL、Redis、可选的 MinIO,以及计算面的 Cubelet 和 CubeEgress。Shim、VMM、客户机内核和 agent 镜像作为文件安装到节点上,由 containerd 按需拉起。

Kubernetes 部署把控制面做成 Deployment,计算节点上跑四类 DaemonSet:一个大 Pod 装 Cubelet 和 CubeEgress,一个负责安装 Shim、内核和客户机镜像,一个负责准备 KVM 和 XFS,还有一个可选的 PVM 支持。

计算节点有两个硬性要求:能用 KVM(没有裸金属时可以用 PVM),/data/cubelet 必须是开了 reflink 的 XFS。CubeCoW 启动时会真的做一次 reflink 试验,不通过就拒绝启动。

公开过的数字

README 把单并发冷启动写成 60 毫秒以内,32 GB 以下规格的额外内存开销写成 5 MB 以内。

2026 年 6 月 1 日的官方基准在腾讯云 BMI5 上测得:96 个逻辑核,375 GiB 内存,沙箱规格 2 vCPU / 2 GiB,统计从 POST /sandboxes 到状态变成 running。

并发平均p95吞吐
1 47.8 ms 57.4 ms 17.9 个/秒
10 88.7 ms 116.9 ms 101.4 个/秒
20 98.1 ms 175.8 ms 180.9 个/秒
50 276.1 ms 508.4 ms 147.6 个/秒

同一份报告里,1000 台空载沙箱总共占约 25 GiB 内存,单台均摊 21 到 26 MB。这个数比 2 GiB 的规格小得多,原因在虚拟化篇里会讲:内存是按页从快照映射进来的,只读的页在同一模板的所有沙箱之间共享。

参考