最近在研究 Agent 技术时,一个问题反复浮现:既然 Agent 需要的是轻量、安全、快速启动的沙箱环境,为什么几乎没有人考虑使用 Unikernel 呢?早在 2017 年,我就写过一篇介绍 Unikernel 的文章[^2]。从我学生时代起,我就觉得这是一项非常有趣的技术,可惜它从未被业界广泛采纳。
从理论上看,这简直是天作之合。Unikernel 将应用和内核编译成一个单一的、运行在硬件虚拟化之上的镜像——没有多余的 shell,没有冗余的系统调用,攻击面被压缩到极致,启动速度可达毫秒级。对于运行不可信代码的 Agent 沙箱来说,还有比这更理想的底座吗?
然而现实是,不仅没人用,甚至很少有人讨论它。行业在为 Agent 寻找安全底座时,目光都投向了 Firecracker、gVisor、Kata Containers,可能再加上 WebAssembly (WASM)。在深入探讨这个问题前,我们先统一一下 Agent Sandbox 的几个核心需求:
- 冷启动要快:理想情况下在 100 毫秒以内。
- 安全性要高:能有效隔离不可信代码,防止越权访问和攻击。
- 语言支持要好:能良好支持 Python 这类主流的 Agent 开发语言,兼容常见的库和工具。
- 构建流程要方便:能让用户快速构建和部署自己的 Agent 镜像。
下面,我们来逐一审视当前主流的几种技术方案。
e2b:基于 Firecracker 的实现
首先看看目前讨论最多的两种思路:容器和轻量级虚拟机(如 Firecracker)。这里可能存在一个普遍的误解:许多人认为容器的启动速度不如 Firecracker 这类轻量级虚拟机快。这可能是因为我们通常使用 docker run 命令,体感上启动耗时较长。但排除镜像拉取等操作后,容器启动本身做的事情其实相当简单:
- 镜像解压与层合并:Overlay2 存储驱动将只读层与可写层合并。
- 容器创建:runc 创建命名空间、cgroups、网络等。
- 进程启动:执行 ENTRYPOINT 或 CMD。
这个过程的耗时可以控制在 50 毫秒以下,甚至达到 10 毫秒级别。相比之下,Firecracker 轻量级虚拟机的启动时间通常在 125 毫秒以上。许多比较是用容器内进程就绪的时间与一个空 Micro VM 的启动时间对比,这并不公平。从容器的启动速度看,它完全能满足 Agent Sandbox 的需求。
其真正问题更多在于安全性。容器通过命名空间和 cgroups 提供了一定程度的隔离,但它们仍然共享宿主机的内核。而 Firecracker 这类轻量级虚拟机提供了更强的隔离性,因为它们运行在完全独立的内核上,攻击面更小,更适合运行不可信代码。
以 e2b 为例,它是一个近期比较热门的 Agent Sandbox 方案,设计目标就是为 Agent 提供轻量、安全、快速启动且支持高密度部署的环境。它利用 Firecracker 来实现沙箱。
在 e2b 中,沙箱镜像被称为 Template。由于 e2b 基于 Firecracker,它的 Template 就是一个完整的 Linux 镜像。但它提供给用户的构建语言是 Dockerfile 的一个子集。其实现流程(隐藏在 e2b-dev/infra[^5] 代码库中)大致如下:
- 拉取基础镜像:从远程仓库获取指定的 Docker 镜像。
- 注入配置层:添加包含主机名、DNS、Envd 服务配置及预置脚本的新文件层。
- 提取文件系统:从镜像中提取 ext4 格式的根文件系统,类似
buildfs[^6] 的实现。
- 初次启动(预置阶段):启动 Firecracker 微虚拟机,运行仅包含预置脚本的 BusyBox init 进程,安装 systemd 后退出。
- 二次启动(服务初始化):再次启动 Firecracker 虚拟机(使用 systemd),等待 Envd 服务就绪。
- 构建模板层:生成模板所需的各个层/步骤。
- 沙箱内收尾配置:重启沙箱,执行配置脚本(启用 swap、创建用户等)和启动/就绪命令。
- 创建快照:对配置完成的系统进行快照。
- 上传模板:将模板及相关层上传到存储系统。
e2b 通过这种方式实现了基于 Firecracker 的 Agent Sandbox。那么它是如何调度这些沙箱的呢?其调度系统称为 Orchestrator,负责管理沙箱的生命周期。
e2b 的调度算法非常简单,基于 best of k[^7]。当需要创建沙箱时,Orchestrator 会从集群中随机选取 k 个节点,基于预设的启发式规则(如已用 CPU、内存、运行中沙箱数等)计算一个分数(越低越好),最后选择分数最低的节点来创建沙箱。这个算法适合沙箱高并发、生命周期短的 Agent 场景,调度效率高,但仍有优化空间。
这里引申出我的一个观点:Kubernetes 可能并不适合 Agent Sandbox 的调度。Kubernetes 更适合长期运行或生命周期较长的工作负载。Agent Sandbox 的生命周期通常很短,且场景可能涉及本地运行、小集群部署或在本地与云端之间无缝迁移。e2b 这种自研轻量级调度器的思路,不失为一种合理的选择。
当然,作为基础设施开发者,我总在思考能否将这类调度共性抽象成一个通用系统,以支持不同的沙箱方案。不过这与本文主题关系不大,暂且不表。
除了启动快,Firecracker 还带来了 Snapshot(快照) 能力,可以在极短时间(文档数据:Intel < 8ms,AMD < 3ms)内恢复一个已运行的沙箱状态。这对生命周期短、频繁创建销毁的 Agent 来说极具吸引力,能显著减少性能开销。
from e2b_code_interpreter import Sandbox
sbx = Sandbox.create()
print('Sandbox created', sbx.sandbox_id)
# 暂停沙箱
# 你可以将沙箱ID保存到数据库,以便后续恢复
sbx.beta_pause()
print('Sandbox paused', sbx.sandbox_id)
# 连接到沙箱(如果已暂停,会自动恢复)
same_sbx = sbx.connect()
print('Connected to the sandbox', same_sbx.sandbox_id)
k7:基于 Kata Containers 的实现
看完 e2b 这种将 Docker 镜像转换为内核镜像并自研调度器的方案,再来看看基于 Kata Containers 的实现——k7。国内不少大厂的 Agent Sandbox 方案也基于 Kata,我们可以把 k7 作为一个典型案例来分析。
k7 的虚拟化监视器(VMM)也使用了 Firecracker。由于 Kata 兼容 OCI 标准的容器镜像,因此 k7 的 Template 就是一个普通的 Docker 镜像。Kata 与 Kubernetes 的兼容性非常好,所以 k7 直接使用 Kubernetes(或 k3s)进行调度。
整体上,由于采用了成熟组件,k7 的实现相对简单许多。其沙箱镜像构建流程也非常直观:基于 Dockerfile 构建一个 Docker 镜像,然后直接用这个镜像启动沙箱。相比 e2b 需要将 Docker 镜像提取为 ext4 rootfs 的复杂流程,k7 的方式简洁得多。
from katakate import Client
k7 = Client(
endpoint='https://<your-endpoint>',
api_key='your-key')
# 创建沙箱
sb = k7.create({
"name": "my-sandbox",
"image": "alpine:latest"
})
# 执行代码
result = sb.exec('echo "Hello World"')
print(result['stdout'])
# 列出所有沙箱
sandboxes = k7.list()
# 删除沙箱
sb.delete()
但是,k7 可能无法像 e2b 那样利用 Firecracker 的快照功能来实现快速恢复。Kata 的设计目标是为了兼容容器生态,它很可能屏蔽了 Firecracker 的原生快照功能。而且 Kata 的架构是在虚拟机里运行容器,直接使用 Firecracker 快照能否达到预期的效果和延迟,我也存疑。
那么,快照恢复能力是否是 Agent Sandbox 的核心需求?这可能要打个问号。对于一些简单的 Agent 场景,或许并不需要。而 Kata 带来的 完全兼容 OCI 镜像 的能力,对许多用户而言可能更具吸引力。毕竟,构建一个 Docker 镜像远比构建一个 Firecracker Template 简单和快速。e2b 是用更长的构建时间,换取了 Micro VM 的启动速度和快照恢复的便捷性。
Monty:基于 WASM 的探索
看完虚拟机方案,我们再来看看 WebAssembly (WASM) 领域有没有出色的设计。WASM 的启动速度可以比容器更快,前面提到容器启动约 50ms,而 WASM 的启动可以达到 10ms 级别,因为它直接在运行时内实例化,没有内核加载、设备初始化等开销。
但是,WASM 支持 Python 非常困难。首先,WASM 的设计假设是单一线性内存空间,而 Python 的内存管理依赖复杂的垃圾回收机制。其次,与 Unikernel 类似,WASM 通过 WASI 访问系统,这是一个精简、可移植的接口层,功能远少于 Linux 原生接口。Python 高度依赖各种系统调用和库函数,WASI 的限制会导致许多 Python 库无法正常工作。
Pyodide 是一个将 CPython 解释器和部分科学计算库编译到 WebAssembly 的项目,但其冷启动特别慢。目前优化较好的思路是 Cloudflare 的方案[^9],它将运行时的耗时通过快照转移到了部署阶段。Wasmer 也做过类似的快照和缓存优化。但在我看来,这些方案有些“治标不治本”,WASM 的设计初衷并非为了支持像 Python 这样复杂的动态语言。
值得一提的是 Monty。它是一个支持 Python 部分语法子集的解释器。下图展示了它与一些常见技术的对比:

0.06ms 的启动时间远超容器和 Firecracker 等方案。其中的 Starlark 也是一种 Python 方言,是 Bazel 引入的构建语言子集。在我们之前的项目 envd[^11] 中也用过 Starlark 作为构建语言。它支持的语法非常有限,虽然冷启动快,但参考价值不大。
表格中还提到了 Wasmer,虽然快,但项目本身维护状态不佳,不值得考虑。Sandboxing Service 举例提到了 e2b、Daytona 和 Modal,这三者技术完全不同,测试也比较随意,不具备参考性。YOLO Python 就是直接在本地运行 Python 解释器,虽然启动快,但完全没有安全性保障。
看起来 Monty 是个不错的方案,但它支持的语法子集太小,不支持 class,也不支持 sys 等模块。在这种程度的“阉割”下,Unikernel 反而显得更具优势。
Unikernel:被忽视的潜力股
看完这些项目的设计,让我们回到最初的问题:为什么没人用 Unikernel 来做 Agent Sandbox?
一个重要原因是,传统 Unikernel 不支持多进程。它没有内核和用户态的概念,应用与内核编译为一体,运行在单地址空间内。如果没有多进程模型,像 Python 这样重度依赖多进程的语言就无法得到良好支持。
不过,情况正在发生变化。Unikraft 在 2025 年 5 月的 0.19 版本中宣布 支持了多进程[^12],这对支持 Python 来说是一个关键特性。但其实现方式是 vfork 后子进程与父进程共享地址空间,如果子进程不立即执行 execve,父子进程的内存访问就会互相干扰。因此,这并非真正意义上的完全隔离的多进程。
另一种探索是 Unikernel Linux (UKL)[^13]。UKL 的思路截然不同,它不是从零设计 Unikernel,而是通过配置选项将 Unikernel 的优化技术集成到 Linux 中。在 UKL 中,单个应用可以直接链接到修改过的 Linux 内核和 glibc 运行。这样做的好处是未修改的应用可以开箱即用,保留了 Linux 的多进程支持能力,但也失去了传统 Unikernel 攻击面极小、启动极快的核心优势。
从另一个角度看,我们不能只关注启动时间。镜像构建和分发的效率同样至关重要。Docker 能成为主流,其镜像分发的高效与便捷功不可没。Unikernel 能够生成极小的镜像,这本身就极具竞争力。如果能在支持多进程与保持小镜像体积之间找到良好的平衡点,我认为 Unikernel 在 人工智能 和 Agent 沙箱领域依然拥有巨大的潜力。
Modal:在镜像分发上做文章
谈到镜像效率,就不得不提 Modal。他们之前在 YouTube 上有一个专门讲解加速技术的视频 “Fast, lazy container loading in modal.com”[^14]。实际上,镜像拉取带来的启动延迟,往往远大于沙箱本身的启动时间,这是延迟的最大单一来源。
传统的 Docker 拉取镜像,需要串行完成下载多个 gzip 压缩层、单线程解压、解包到文件系统这一整套流程。对于一个 8GB 的镜像,这个过程可能需要一分钟,且容器在数据完全就绪前无法启动。
Modal 采取了 Lazy Loading(按需加载) 的方式。当你运行一个 PyTorch 进程时,很可能不会访问容器文件系统里的所有文件。这个思路与 Dragonfly[^15] 或 estargz 等分层镜像格式类似,但 Modal 的实现不同——它通过 FUSE 实现。我推测其原理是:在创建容器文件系统时,根据镜像元数据生成一个占位的文件系统树;当文件被访问时,FUSE 拦截读取请求,并按照内存、本地 SSD、同可用区缓存服务器、区域 CDN、对象存储的优先级顺序去获取数据。视频中还提到了许多 FUSE 性能调优的细节,感兴趣可以自行观看。Modal 的这个设计非常巧妙,能极大减少镜像拉取时间,提升容器启动速度。
结语
Agent Sandbox 的设计有多种方案,各有优劣:
- Firecracker 提供了强隔离和快照能力,但镜像构建相对复杂。
- Kata Containers 提供了完美的 OCI 镜像兼容性,但可能无法利用底层虚拟机的快照功能。
- WASM 启动最快,但对 Python 等复杂语言的支持是一大难题。
- Unikernel 理论上非常适合,但历史上对多进程支持的缺失限制了其应用,不过新技术发展正在改变这一点。
此外,镜像构建与分发的效率同样关键,Modal 的 Lazy Loading 方案为我们提供了一个优秀的思路。对于运行大模型推理的服务,镜像和模型体积巨大,这点尤为重要;而对于多数 Agent 任务,可能没那么夸张,但仍不可忽视。
未来,随着 Unikernel 等技术的发展,以及业界对轻量、安全沙箱需求的持续增长,我们可能会看到更多创新方案的涌现。如果你对这类基础设施技术感兴趣,欢迎到 云栈社区 交流讨论。
License
- This article is licensed under CC BY-NC-SA 3.0.
- Please contact me for commercial use.
参考资料
[^1]: 刚刚体验了 agent rl: https://gaocegege.com/Blog/jiucai-rl
[^2]: Unikernel:从不入门到入门: https://gaocegege.com/Blog/%E5%AE%89%E5%88%A9/unikernel-book
[^3]: 知乎上的一篇文章: https://zhuanlan.zhihu.com/p/1999938129465979624
[^4]: e2b: https://github.com/e2b-dev/e2b
[^5]: e2b-dev/infra: https://github.com/e2b-dev/infra/blob/main/packages/orchestrator/internal/template/build/builder.go#L101
[^6]: buildfs: https://github.com/rust-firecracker/buildfs/
[^7]: best of k: https://github.com/e2b-dev/infra/blob/d79e4ca97205d304d8116a78d8f4d485bc644cfe/packages/api/internal/orchestrator/placement/placement_best_of_K.go#L106
[^8]: k7: https://github.com/Katakate/k7
[^9]: Cloudflare 的思路: https://developers.cloudflare.com/workers/languages/python/how-python-workers-work/
[^10]: monty: https://github.com/pydantic/monty
[^11]: envd: https://github.com/tensorchord/envd
[^12]: 0.19 版本支持了多进程: https://unikraft.org/blog/2025-05-15-multiprocess?trk=public_post_comment-text
[^13]: Unikernel Linux (UKL): https://ar5iv.labs.arxiv.org/html/2206.00789
[^14]: Fast, lazy container loading in modal.com by Jonathon Belotti: https://www.youtube.com/watch?v=SlkEW4C2kd4
[^15]: dragonfly: https://github.com/dragonflydb/dragonfly
[^16]: CC BY-NC-SA 3.0: https://creativecommons.org/licenses/by-nc-sa/3.0/