大规模跑 agent 评测和 RL 训练时,沙箱是最先卡住的基础设施:容器隔离不够(agent 代码带 root 权限跑,内核共享面太大),KVM 整机启动秒级太慢,镜像全量预推到几百台机器上直接打爆带宽和磁盘。MoonshotAI 在 2026-07 开源的 AgentENV(kvcache-ai/AgentENV,Rust + Go,三周 3.2K star)给的答案是把三件事拧在一起:Firecracker microVM 做隔离、overlaybd 做镜像/快照懒加载、快照 + fork 做环境复用。它是 Kimi K3 agentic RL 训练背后的环境平台,生产环境跑过 150 万镜像,单机内存超卖 9.6x。这篇文章从源码拆它的控制面、存储子系统和快照流水线,再落到部署配置和测试团队的接入建议。
控制面:axum + OpenAPI 生成路由,外加一个手写反向代理
单节点上 AgentENV 是三层:HTTP API(axum)→ orchestrator(生命周期管理)→ Firecracker VM。API 路由大部分由 OpenAPI 生成(agentenv_http_servercrate),手写部分只有/proxy/*反向代理——沙箱内 agent 起的 HTTP 服务通过它暴露给外部,代理按 sandbox_id 分类路由。Prometheus 指标挂在/metrics。
多节点控制面在services/里,是独立的 Go 模块,两个组件:
- Gateway:HTTP 反向代理,按 sandbox ID 路由到对应节点
- Scheduler:gRPC 服务,负责节点选择、沙箱与节点的绑定、心跳、P2P 对端发现
Scheduler 支持 static(配置里的节点列表)和 kubernetes(watch headless Service 的 EndpointSlices)两种发现模式。K8s 部署模型值得抄:gateway 是 Deployment + ClusterIP,scheduler 单副本,agentenv-node是带/dev/kvm的 privileged DaemonSet——每个宿主机一个节点实例,hostPath 挂本地缓存。
生命周期:LaunchPlan 三态 + 不透明的后端状态
orchestrator 用LaunchPlan表达沙箱的创建来源:
pub enum CreateLaunchSource { Snapshot { snapshot: Box<RunnableSnapshot> }, // 从已提交快照冷启 Fresh { build_spec: Box<FreshSandboxBuildSpec> }, // 从模板现构建 } pub enum LaunchPlan { Create(Box<CreateLaunchPlan>), // 过渡态 Creating Resume(Box<ResumeLaunchPlan>), // 过渡态 Resuming,从 PausedSandboxState 恢复 }orchestrator 对沙箱后端只认SandboxBackendtrait,PausedSandboxState对它是完全不透明的:pause 时序列化成 JSON 存进 metadata,resume 时原样交回后端,orchestrator 不解释里面的任何字段。换后端(envd/Firecracker、process、mock)不需要动 orchestrator 一行代码——这是它能同时服务评测、RL 训练和本地开发三种场景的结构性原因。
fork 也是 trait 上的一个操作:SandboxForkSpec只带新的 sandbox_id 和访问令牌,一个 running 环境可以 fork 成多个独立沙箱跑并行 agent 工作流。对评测来说,这意味着"同一环境状态"可以低成本复制出 N 份,并行度不再受环境启动成本限制。
模板流水线:aenv pull 到底做了什么
aenv pull ubuntu:22.04 --name ubuntu不是简单拉镜像。它走 template builder:build_spec描述 FROM 哪个 OCI 镜像 + 要执行的自定义步骤,step_executor在沙箱里逐步执行构建(装依赖、写配置),产物 seal 成不可变的只读层集合,注册成模板。之后每次aenv start从模板冷启,或从某个已提交快照秒级恢复。模板是"环境定义",快照是"环境状态",两者分离是这套系统能复用的关键。
存储子系统:LSMT 分层镜像 + ublk 用户态块设备
真正硬核的部分在storage/,四件事:
overlaybd 镜像格式是 LSMT(Log-Structured Merge Tree)分层的。每层文件头有 magicLSMT\0\1\2,索引是一组 16 字节位压缩的DiskSegmentMapping(50-bit 数据偏移、14-bit 长度、55-bit 物理偏移,外加 zeroed 标记和层标签)。底层是只读压缩层,顶层一个可写 upper layer。读路径自顶向下查 segment index,第一个命中的层出数据;写全部 append 到 upper,sync 时刷索引。压缩用 zstd level 3 + 随机访问跳表 + CRC32C 校验。
ublk 把镜像变成块设备。用户态进程通过 io_uring 的UringCmd向/dev/ublk-control发 ADD,内核分配设备 ID 并创建/dev/ublkcN(控制)和/dev/ublkbN(块设备),块 I/O 通过 mmap 的ublksrv_io_desc数组派发给用户态 worker 处理。VM 里看到的是正经块设备,但数据实际来自 overlaybd 懒加载层——没读到的块根本不会下载,本地磁盘只是一个有界缓存。
内存快照走同一条路。VM 内存被做成一个 ublk 只读块设备,同一个快照 fork 出来的沙箱共享同一份内存镜像,靠 refcount 管理生命周期。这是 9.6x 内存超卖的物理基础:环境跑得越久越分化,可回收的 guest 内存越多。
ublk-daemon 独立进程。所有 ublk 设备由一个常驻 daemon 统一管理(Unix socket RPC),带 warm-pool 预分配,设备创建开销不落在关键路径上。
快照流水线:seal 上层 → 变新底层 → 开新上层
pause/快照的核心就一句话:create_snapshot_and_restack()。把活的 upper layer seal 掉变成最新的只读层,原地开一个新的可写 upper——整个增量快照 <100ms。快照产物(vm_state + Firecracker manifest)发布到 repository:
- PosixFS:共享目录
/mnt/aenv-snapshots,节点本地image-cache/commits/可随时回收(已提交快照不 pin 本地层) - OSS:S3 兼容对象存储
提交后快照工件走 P2P 分发推到集群。P2P 层基于 iroh + iroh-blobs,对外是Arc<dyn P2pTransport>trait(lookup/fetch/publish/unpublish,支持 byte-range fetch),对端发现由 scheduler 的ListP2pPeers提供,scheduler 不存工件、不转发数据。单节点部署直接 disabled 模式,零开销。
| 操作 | 延迟 | 说明 |
|---|---|---|
| boot / resume | <50ms | 快照冷启或从暂停恢复 |
| pause | <100ms | 释放 CPU/内存回主机 |
| 增量快照 | <100ms | 重磁盘修改下也成立 |
评测接入模式:fork 并行 + 快照复现
对测试团队,这套平台最值钱的用法是"环境状态可版本化"。评测回归可以固定在一个已提交快照上:跑用例前 fork 出 N 个沙箱并行执行,用例结束后整体丢弃,下一轮回归还是从同一个快照出发——环境漂移被彻底排除,比在 CI 里反复 docker build 可复现得多:
from e2b import Sandbox BASE = "eval-snapshot-v42" # 已提交的评测基准快照 results = [] for i in range(16): # 16 路并行 sb = Sandbox.create(BASE) # fork 自同一快照 r = sb.commands.run(f"pytest case_{i}.py -q") results.append((i, r.exit_code, r.stdout[-500:])) sb.kill() # 用完即弃,不留状态配套的还有两个基础设施细节:沙箱内服务通过/proxy/*反向代理暴露(外部测试编排可以直接访问 agent 起的 HTTP 端口做黑盒断言),每个节点的 Prometheus/metrics暴露环境启停、快照、P2P 传输指标——环境平台的健康度本身可观测。
选型对比:为什么不直接用 Docker 或 KVM
| 维度 | Docker | 传统 KVM | AgentENV |
|---|---|---|---|
| 隔离级别 | 共享内核(cgroup/namespace) | 完整虚拟化 | Firecracker microVM |
| 启动延迟 | 百 ms 级 | 秒级 | <50ms(快照) |
| 镜像分发 | 全量拉取/预推 | 全量 | overlaybd 懒加载 |
| 内存密度 | 高但隔离弱 | 低 | 快照共享 + ballooning 9.6x |
| 状态复用 | 无原生快照 | 有但重 | 增量快照 + fork |
Docker 输在隔离(agent 评测要防恶意代码,共享内核面太大),KVM 输在启动速度和镜像分发,AgentENV 用 microVM + 懒加载 + 快照把三个短板同时补上。
实践:部署配置和 E2B 兼容
单机部署要求内核 6.8+、/dev/kvm,install.sh 或 Docker(--privileged -v /dev:/dev)都行。共享存储配置(懒加载的关键):
[snapshot] repository_backend = "posix_fs" [backend.posix_fs] snapshot_store = "/mnt/aenv-snapshots" [image.cache.remote_blocks] max_size_gb = 100 # 本地磁盘是"有界缓存":热数据留存、冷数据驱逐OSS 后端把repository_backend换成oss并配 endpoint/bucket 即可。存储网络至少 1Gbps,10Gbps 起步。
E2B 兼容是最大的迁移红利:API 层兼容 E2B,现有 E2B 代码零改动切换:
export E2B_API_URL=http://127.0.0.1:8000 export E2B_SANDBOX_URL=${E2B_API_URL} export E2B_API_KEY=e2b_000000from e2b import Sandbox, SandboxQuery, SandboxState sandbox = Sandbox.create("<template-id>") # 从模板起沙箱 result = sandbox.commands.run("pytest -q") # 沙箱内跑测试 sandbox.beta_pause() # 暂停,释放资源 sandbox.kill()aenv CLI 同样够用:aenv pull ubuntu:22.04 --name ubuntu、aenv start ubuntu、aenv pause/fork/timeout <sandbox-id>。
踩坑清单:
- 目前无鉴权,README 明确警告别暴露公网,放可信内网或加授权代理
- 无 KVM 的环境走 PVM 部署(有专门 guide),性能打折但能跑
- 空闲环境 TTL 默认短,长任务记得
aenv timeout续期 - 快照存 OSS 时注意跨 region 拉取延迟,P2P 分发能缓解但别裸奔公网 OSS
收尾
AgentENV 值得抄的不是"又一个沙箱平台",而是三个工程决策:镜像和内存统一走 overlaybd 懒加载 + 有界本地缓存,让集群总镜像量超过单机磁盘几个数量级;快照和 fork 作为一等公民,把"起环境"从秒级操作变成 <50ms 的廉价操作;API 兼容 E2B,迁移成本趋近于零。对测试团队来说,这意味着评测环境可以做到:从同一快照 fork 出 N 个并行沙箱跑同一组用例(可复现性有保障),空闲即 pause 释放资源(成本可控),agent 评测和 RL 训练共用一套环境平台(基础设施复用)。
进阶方向:agentic RL 训练的 on-policy 数据生产、评测用例在沙箱内的编排、把快照流水线接进 CI 做回归环境的版本化。