Skip to main content

02 - AgentENV 拆解

前置01 - 训练环境为什么不是运行时沙箱 里的三处约束。读过 执行沙箱 · 03 - E2B 与快照机制 会更快看懂快照那一节。

本篇回答:一个能支撑 150 万镜像、冷启动 50 毫秒的环境平台,内部是怎么搭的?每一处设计分别解决 01 篇提出的哪个问题?

本篇会用到的词

意思
FirecrackerAWS 开源的 microVM 管理器(★36,156,Apache-2.0),去掉了传统虚拟机绝大多数设备模拟,所以启动快、内存开销小
overlaybdcontainerd 旗下的分层块设备格式(★396,Apache-2.0)。和 OverlayFS 的区别是:它分层的是块设备而不是文件系统,所以可以直接挂给虚拟机当磁盘
LSMTLog Structured Merge Tree,overlaybd 的镜像格式。底下是若干只读层,最上面一个可写层,写操作一律追加到可写层
ublkLinux 内核 6.8+ 提供的机制,允许在用户态实现一个块设备(表现为 /dev/ublkbN)。它是 overlaybd 能变成虚拟机磁盘的那座桥
io_uringLinux 的异步 IO 接口,一次系统调用可以提交一批 IO 请求,避免了传统同步 IO 的频繁陷入内核
写时复制(COW)多个副本共享同一份底层数据,谁要改就先复制自己那一份再改。fork 出成千上万个环境却不会成倍占存储,靠的就是它
内存气球(ballooning)让虚拟机把暂时用不到的内存还给宿主机的机制。它是「空闲环境很便宜」这句话的实现手段
内存超分分配给所有虚拟机的内存总量超过宿主机物理内存。只有当大部分虚拟机同时空闲时才成立

一、它是什么

kvcache-ai/AgentENV(简称 AENV),Rust 写的,MIT,2026-07-23 建仓,实测当天 ★3,240。开发方是 kvcache-ai —— 也就是做 KTransformers 和 Mooncake 的那个团队。

它的定位一句话:大规模跑 Agent 环境的分布式平台。官方 README 里写明了它的第一个生产用户:

AgentENV (AENV) is a platform for running agent environments at scale, powering agentic RL training for Kimi K3.

Kimi K3 是 Moonshot 2026 年 7 月发布的开放权重模型(2.8T 总参数、104B 激活、1M 上下文)。也就是说,这不是一个演示项目,是一个前沿模型训练时真在用的环境层。

官方给出的四条核心能力,正好对上 01 篇那三处约束:

官方说法回应的是哪条约束
按需加载 OCI 镜像,生产中扩到 150 万镜像,本地盘只当有界缓存镜像分发:几百种镜像 × 几百台机器没法预热
快照支撑的环境,启动或恢复 < 50 ms,暂停 < 100 ms生命周期短,起停开销成主要成本
原生快照与 fork,一个运行中的环境可分叉成多个独立沙箱reset 语义缺失
ublk 高性能 IO + 共享宿主页缓存,内存气球实现生产中 9.6 倍内存超分空闲成本:环境闲着但内存占着

上面的 150 万镜像与 9.6 倍超分两个数字,README 标注来源为 Kimi K3 技术报告与生产实践,本文按官方声明转述,未自行复现。

二、单节点:一次请求怎么走

一次 POST /sandboxes 走到底客户端API 层Axum · 鉴权校验编排器生命周期状态机Firecracker microVM/dev/vda根文件系统/dev/vdb额外磁盘VM 内存从快照恢复ublk/dev/ublkbN用户态块设备overlaybdupper(可写)layer 2(只读)layer 1(只读)layer 0(只读)虚拟机看到的是三块普通磁盘和一段普通内存;它不知道底下是分层镜像,也不知道那些只读层正被同机的另外几百个沙箱共用。整套系统的重心不在虚拟机,而在右边那两格 —— 官方文档原话:AgentENV 的核心是一个存储子系统。
信息来自 AgentENV 官方文档 concepts/overview 与 internals/architecture 两节的架构图(MIT),按本站版式重绘。

沙箱内部还跑着一个 envd 守护进程,负责执行命令、流式返回输出、上报健康状态 —— 这和 执行沙箱 04 篇里讲的「宿主不直接伸进沙箱,只通过协议说话」是同一个结构。

三、存储:为什么分层的是块设备而不是文件系统

这是整套设计里最需要想明白的一处。

容器用的是 OverlayFS,分层发生在文件系统这一层:底下若干只读层,上面一个可写层,合并成一个目录树。但虚拟机要的不是目录树,是一块裸磁盘。你不能把 OverlayFS 直接塞给 Firecracker 当 /dev/vda

overlaybd 的做法是把同一套「分层 + 写时复制」的思路下沉到块设备:

3.1 LSMT 格式与读路径

每一层是一个文件,内部结构是「头尾信息 + 一张段映射表」。段映射表的每一项 16 字节,位打包存着这一段在虚拟磁盘上的偏移、长度、在物理文件里的位置。

读一个块的时候,自上而下逐层查这张表,第一个命中的层提供数据;上层没映射到的范围落到下层。写操作一律追加到最上面那个可写层。

读:自上而下找第一个命中  写:一律落到最上层upper(可写)本沙箱写过的块都在这里每个沙箱独有layer 2(只读)pip install 装出来的那些文件layer 1(只读)语言运行时layer 0(只读)基础系统同机所有沙箱共用同一份读请求逐层往下找第一个命中的层写请求只追加到 upper这就是几万个环境不会成倍占存储的原因:底下三层在磁盘上只有一份,每个沙箱真正独占的只有自己那个 upper。
压缩用 zstd(级别 3),配随机访问跳表和 CRC32C 校验 —— 跳表是关键,没有它就只能整层解压,按需读取无从谈起。

3.2 ublk:把镜像变成虚拟机能挂的磁盘

overlaybd 是用户态的一堆文件,Firecracker 要的是 /dev/xxx。中间这一步靠 ublk:

应用在 VM 里读 /dev/vda
↓ Firecracker 转成 virtio 请求
↓ 宿主上表现为对 /dev/ublkbN 的块 IO
↓ 内核 ublk 驱动把请求丢进一块 mmap 的描述符数组
↓ 用户态的 uvm-ublk-daemon 从 io_uring 取出请求
↓ 交给 overlaybd 按 3.1 的路径查层、读数据、返回

几个值得注意的工程选择:

  • 每队列一个工作线程,各自持有线程本地的 io_uring 实例,用 MPSC 通道提交,避免跨线程加锁
  • 内核 6.8+ 上用 AutoRegBuffer 走稀疏缓冲表实现零拷贝,老内核回落到传统分配
  • ublk 设备统一由一个独立守护进程 uvm-ublk-daemon 管理,节点服务只通过 Unix socket 指挥它 —— 把 io_uring 控制权和生命周期编排分在两个进程里

这也解释了 README 里那条硬性前提:Linux 内核 6.8 以上

四、快照与 fork:reset 是怎么变便宜的

01 篇 2.3 节说过,训练环境要的是「每条 rollout 从同一起点开始」。AgentENV 把快照做成了系统里的基础原语,官方文档的说法是:其他一切都建在它上面。

  • 模板就是快照。构建一个模板 = 提交一个快照,模板 ID 只是指向它的别名
  • 启动沙箱就是从快照恢复
  • 运行中的沙箱可以再产出新快照,用于后续复用或分叉

暂停一个沙箱时发生的事情,比「把内存 dump 到文件」精细得多:

① Firecracker 生成一个仅含状态的差分快照
② AgentENV 向 Firecracker 查询「脏页 / 已存在页」的范围
—— 只读这些范围,没碰过的内存不用存
③ 用 process_vm_readv 直接把选中的内存读出来
④ 直接写成一个 overlaybd 内存层,堆叠在上一次快照的层之上

关键在第 ②③ 步:内存快照也是分层的、增量的。一个跑久了的沙箱反复暂停恢复,每次只落新脏的那部分,所以官方能声称「即使磁盘改动很大,快照也在 100 毫秒内完成」。

磁盘侧的暂停路径叫 create_snapshot_and_restack():把当前可写层封存成一个新的只读层,再在上面开一个空的可写层。原来的数据一个字节都不用搬。

五、内存快照的共享:一处很漂亮的设计

恢复时,AgentENV 不用 userfaultfd(内核 6.8 之前的常见做法),而是从内存层堆叠出一个只读 ublk 设备,作为 BackendType::File 交给 Firecracker。Firecracker 把这个块设备 mmap 进来,写第一次的时候写时复制到匿名内存 —— 底层设备永远不被修改。

不被修改,就意味着可以共享:

同一个快照并发拉起几百个沙箱时沙箱 A沙箱 B沙箱 C · · ·同一个只读 ublk 设备按引用计数共享谁都改不了它宿主页缓存只需缓存一份并发拉起时 IO 大幅下降overlaybd 内存层snap 0 / 1 / 2 · · ·增量堆叠沙箱各自写内存时才写时复制出私有页,所以「共享」不影响隔离 —— 三个沙箱看到的是三份独立内存,物理上却大部分是同一份。这一条对训练场景特别值钱:一轮 rollout 就是从同一个模板同时拉起几百上千个环境,正好落在这个共享路径上。
页缓存被复用是这里的主要收益。如果每个沙箱各自持有一份内存镜像文件,宿主机会为同样的内容重复缓存几百遍,很快把 page cache 挤爆。

六、按需加载:本地盘只是缓存,不是仓库

这是回应「几百种镜像没法预热」那条约束的部分。

配置上只有两个后端选项 —— POSIX 共享文件系统或 S3 兼容对象存储:

# 快照仓库放在共享存储上,本地盘只做有界缓存
[snapshot]
repository_backend = "oss" # 或 "posix_fs"

[backend.oss]
endpoint = "YOUR_ENDPOINT"
bucket = "YOUR_BUCKET"
cache_max_size_gb = 100 # 本地缓存上限:满了就淘汰冷数据

[image.cache.remote_blocks]
max_size_gb = 100 # 远端块的本地缓存上限,同样是有界的

思路很简单,但对训练场景是决定性的:镜像总量可以远超单机磁盘容量若干个数量级,因为每台机器只保留自己正在用的热数据,冷的自动淘汰。150 万镜像这个数字就是这么来的 —— 不是每台机器都存 150 万,而是集群整体能寻址这么多,且不需要提前把哪一台预热好。

官方在文档里给了一条容易被忽略的前提:存储网络至少 1 Gbps,强烈建议 10 Gbps 以上。 按需加载把磁盘容量问题转成了网络带宽问题。

七、空闲很便宜:内存气球与超分

训练时环境的空闲比例很高 —— 模型在生成下一个动作时,环境就在那儿等着。

AgentENV 的应对是内存气球:把虚拟机里可回收的内存还给宿主机,需要时再要回来。官方声称生产中做到 9.6 倍内存超分,并且强调这个比例是在「环境跑得越久、彼此差异越大」的条件下维持的 —— 这句话的潜台词是,超分最难维持的恰恰是长时间运行之后。

配合 README 里那组数字:启动或恢复 < 50 ms、暂停 < 100 ms。暂停快,才敢频繁暂停;频繁暂停,超分才有意义。 这三件事是一个整体。

八、多节点:网关 + 调度器

控制面只做两件事:新建时选节点,已有时查节点客户端HTTP网关 :8080按 sandbox ID 路由gRPC返回节点调度器 :9090选节点 · 查归属代理 HTTP代理 HTTP节点 A :8000一台机器上的全部沙箱节点 B :8000同上
控制面刻意做得很薄:调度器只回答「去哪台」,数据面完全不经过它。沙箱一旦创建就和节点绑定,这也意味着节点故障时其上的沙箱不会自动漂移。

九、E2B 兼容:一个务实的选择

AgentENV 暴露的是一套 E2B 兼容的 HTTP API。把 E2B_API_URL 指过来,现有的 E2B Python / TypeScript SDK 代码一行不改就能用。

这一步的意义不在技术,在迁移成本:执行沙箱 03 篇讲过 E2B 已经是这个领域事实上的接口参考,兼容它等于免费获得整个 SDK 生态。自建方案对托管方案做 API 兼容,是这两年基础设施领域一个反复出现的模式网关 08 篇里阿里云 AI 网关和 Higress 的关系也是同一回事,只是方向相反)。

十、它的边界

写文档最容易失衡的地方是只讲好处,这里明确列出适用边界:

限制说明
Linux 内核 6.8+ublk 的零拷贝路径依赖它。老内核上要么不可用,要么退回传统缓冲区
需要 /dev/kvm很多云主机默认不给嵌套虚拟化。官方提供 PVM 部署方案作为退路,但那是另一条路径了
传输不加密README 明确警告:AgentENV 认证 API 请求但不加密流量,必须跑在可信网络里或在反向代理上终结 HTTPS
对存储网络有硬要求按需加载把容量问题换成了带宽问题,至少 1 Gbps,建议 10 Gbps 以上
很新2026-07-23 建仓,本文实测时不足一个月。生产验证目前主要来自单一用户(Kimi K3 的训练)
沙箱与节点绑定控制面很薄的代价:节点挂了,其上沙箱不自动迁移

最后一条值得展开:「给一个前沿模型训练用过」既是最强的背书,也是最大的未知数。 它证明这套设计在那一种负载下成立,但不保证在你的负载下成立 —— 尤其是任务镜像形态、单条 rollout 时长、并发曲线这三样如果差得远,结论未必能搬。

下一篇03 - 环境接口标准:AgentENV 解决的是「环境怎么跑起来」,而「训练框架怎么跟环境对话」是另一个正在打架的问题。

← 回到 专题索引  ·  Agent Infra 板块总览