1. 为什么 Agent Sandbox 需要 Firecracker 这层底座
第一次接触 Agent Sandbox 这个概念是在给一个自动化代码执行平台做架构评审的时候。当时团队想让 AI Agent 能够自主执行用户提交的代码片段,跑完再把结果返回。听起来很简单,但真正落地时才发现,最大的问题不是"能不能跑",而是"跑崩了怎么办"。一个不受约束的 Agent 进程,理论上可以读宿主机任意文件、发起任意网络请求、fork 出无数子进程把机器拖垮。这时候你需要的不是容器,而是一层真正的隔离边界。
Agent Sandbox 本质上是一个"给不可信代码准备的临时牢房"。它要满足三个硬性条件:启动足够快(毫秒级,因为 Agent 调用是高频的)、隔离足够强(不能只是 namespace 隔离,要防内核逃逸)、资源开销足够小(一台机器要能同时跑几百上千个沙箱)。容器方案在这三点上都有短板——共享内核意味着内核漏洞就是逃逸通道,而传统虚拟机虽然隔离强,但启动动辄几秒到几十秒,完全不适合 Agent 这种高频短任务场景。
Firecracker 就是在这个夹缝里长出来的东西。它是 AWS 为 Lambda 和 Fargate 打造的 microVM 管理器,用 KVM 做硬件虚拟化,但把传统 QEMU 那套臃肿的设备模型砍到只剩最必要的部分。一个 Firecracker microVM 的启动时间可以压到 125ms 以内,内存开销只有几 MB,同时每个 VM 都有独立内核,隔离级别和传统虚拟机一致。这就是为什么现在几乎所有主流 Agent Sandbox 方案——不管是 E2B、Modal 还是各家自研的执行沙箱——底层都在用 Firecracker。
这篇文章我想把 Firecracker 在 Agent Sandbox 里的运行路径完整拆一遍,从 KVM 设备怎么被打开、microVM 怎么被拉起、rootfs 怎么挂载,到安全边界到底卡在哪几个点上。同时会补充一些实操中踩过的坑,比如 KVM 权限配置、网络隔离的取舍、快照恢复的注意事项。如果你正在做 Agent 执行环境、在线代码评测、或者任何需要"跑不可信代码"的系统,这些内容应该能直接抄作业。
2. Firecracker 运行路径的整体设计与选型逻辑
2.1 从 KVM 到 microVM:为什么不是容器也不是 QEMU
要理解 Firecracker 的定位,得先把它和两个邻居对比清楚。容器(Docker/runc)走的是 namespace + cgroup 路线,所有容器共享宿主内核,隔离靠的是内核提供的边界。好处是启动快、开销小,坏处是内核一旦有提权漏洞(比如历史上的 Dirty COW、各种 eBPF 提权),容器就是纸糊的墙。对于跑自己人的服务,容器够用;对于跑陌生人的代码,容器不够。
传统虚拟机(QEMU/KVM)走的是硬件虚拟化路线,每个 VM 有独立内核,隔离强度高一个数量级。但 QEMU 为了兼容各种老硬件和外设,模拟了完整的 PC 架构——IDE 控制器、VGA 显卡、PS/2 键盘、一堆 PCI 设备。这些设备模型代码量巨大,既是攻击面也是启动慢的根源。一个 QEMU VM 启动要几秒,内存开销几百 MB 起步。
Firecracker 的做法是"只保留跑一个 Linux 内核所必需的东西"。它砍掉了 BIOS(用 Linux 的 PVH 启动协议直接引导)、砍掉了大部分设备模拟(只留 virtio 系列:virtio-net、virtio-block、virtio-vsock)、砍掉了图形和输入设备。整个 VMM 用 Rust 写成,代码量只有几万行,而 QEMU 是百万行级别。代码少意味着攻击面小,也意味着启动路径短。
在 Agent Sandbox 场景下,这个取舍非常关键。Agent 执行的任务通常是"跑一段 Python 脚本"、"执行一个 shell 命令"、"编译并测试一段代码",任务时长从几十毫秒到几十秒不等,但调用频率极高。你不可能为每个任务等 5 秒启动 VM,也不可能让每个 VM 占 200MB 内存。Firecracker 的 125ms 启动和几 MB 内存开销,正好卡在这个需求上。
2.2 运行路径的三个阶段:准备、启动、回收
Firecracker 在 Agent Sandbox 里的完整生命周期可以拆成三个阶段,每个阶段都有明确的职责和优化空间。
准备阶段是沙箱的"冷启动"部分,包括:打开 KVM 设备、创建 VM 的 fd、配置内存映射、准备 rootfs 镜像、准备内核镜像。这个阶段最耗时的是 rootfs 的准备——如果每次都要从零构建一个 ext4 镜像,那启动时间就废了。所以实践中通常用"基础镜像 + 快照"的方式,把准备好的 VM 状态冻结成快照,后续直接从快照恢复。
启动阶段是真正拉起 microVM 的过程:通过 ioctl 配置 vCPU、加载内核到 guest 内存、设置启动参数、启动 vCPU 线程。Firecracker 用 KVM 的KVM_RUNioctl 让 vCPU 进入 guest 模式,guest 内核开始执行。这个阶段在快照恢复模式下可以压到 10ms 以内。
回收阶段是任务结束后的清理:销毁 VM、回收内存、清理网络资源。如果用了快照,还要决定是丢弃还是保留。高频场景下,回收速度直接影响吞吐量。
提示:很多团队一开始只关注启动速度,忽略了回收阶段。实测下来,如果回收时没有正确释放 vsock 连接和 tap 设备,跑几千次之后会出现 fd 泄漏,最终导致 Firecracker 进程无法创建新 VM。
2.3 快照机制:Agent Sandbox 的性能命门
Firecracker 最被低估的能力是快照(snapshot)。它可以把一个运行中的 microVM 的完整状态——CPU 寄存器、内存、设备状态——冻结成文件,之后从快照恢复出一个一模一样的 VM。恢复时间可以做到 5ms 级别,比冷启动快一个数量级。
在 Agent Sandbox 里,快照的用法通常是这样的:预先启动一个"模板 VM",在里面装好 Python 运行时、常用库、Agent 需要的工具链,然后把它冻结成快照。每次有新任务来,直接从快照恢复一个 VM,注入任务代码,执行,销毁。这样每个任务的启动成本就只有恢复快照的时间。
但快照有个坑:恢复出来的 VM 和原 VM 共享内存页(copy-on-write),如果任务写内存,会触发页复制。对于内存写入密集的任务,这个开销不可忽略。另一个坑是网络状态——快照里的网络配置是冻结的,恢复后需要重新配置网络,否则 VM 可能连不上网。实践中通常的做法是快照里不配网络,恢复后再通过 Firecracker 的 API 动态挂载网络设备。
3. 核心细节解析与实操要点
3.1 KVM 设备与权限:第一道门槛
Firecracker 依赖 KVM 做硬件虚拟化,所以宿主机必须满足几个条件。首先是 CPU 支持虚拟化扩展——Intel 的 VT-x 或 AMD 的 AMD-V。用grep -E 'vmx|svm' /proc/cpuinfo可以确认,有输出就说明支持。如果是在云服务器上,还要确认实例类型是否开启了嵌套虚拟化,很多云厂商的默认实例是不给 KVM 权限的。
其次是内核模块。KVM 相关的模块通常是kvm、kvm_intel或kvm_amd。用lsmod | grep kvm检查,如果没有就modprobe kvm_intel(Intel)或modprobe kvm_amd(AMD)。模块加载后会出现/dev/kvm设备节点。
权限是新手最容易卡住的地方。/dev/kvm默认属于kvm组,普通用户没有访问权限。你有两个选择:把运行 Firecracker 的用户加入kvm组(usermod -aG kvm $USER,需要重新登录生效),或者改/dev/kvm的权限(不推荐,安全性差)。在容器里跑 Firecracker 的话,需要把/dev/kvm设备透传进容器,并且容器要有足够的权限。
注意:在容器里跑 Firecracker 时,除了
/dev/kvm,还需要CAP_NET_ADMIN权限来配置 tap 网络设备。如果容器用了默认的 seccomp 或 AppArmor 配置,可能会拦截 Firecracker 需要的 ioctl 调用,导致启动失败。排查时先看dmesg有没有 seccomp 拒绝记录。
3.2 内核镜像与 rootfs:guest 里装什么
Firecracker 需要一个 Linux 内核镜像(vmlinux 格式,不是 bzImage)和一个 rootfs。内核镜像建议用 Firecracker 官方提供的或者自己编译一个精简版,把不需要的驱动和子系统都裁掉,这样启动更快、攻击面更小。官方推荐的内核配置里,virtio 驱动是必须的,其他大部分都可以关。
rootfs 的选择上,实践中常见三种方案。第一种是 ext4 镜像文件,简单直接,但每次修改都要重新构建镜像,适合固定环境。第二种是 overlayfs,基础镜像只读,上层可写,适合需要持久化的场景。第三种是 initramfs,把整个 rootfs 打包进内核,启动最快但灵活性差。
对于 Agent Sandbox,我倾向于用 ext4 基础镜像 + 快照的组合。基础镜像里预装好运行时和工具链,快照冻结后每次恢复都是干净状态。任务如果需要写文件,写到 VM 内的临时目录,任务结束随 VM 一起销毁。这样既保证了环境一致性,又避免了状态污染。
rootfs 镜像的构建有个细节:镜像大小要控制。一个装好 Python 3.11 + 常用库的镜像大概 500MB 到 1GB,如果每个 VM 都完整加载这个镜像,内存和 IO 开销都不小。实践中可以用稀疏文件(sparse file)或者精简的基础镜像(比如 Alpine + 按需安装),把镜像压到 100MB 以内。
3.3 网络隔离:tap 设备与 vsock 的取舍
Agent Sandbox 的网络配置是个需要仔细权衡的点。默认情况下,Firecracker VM 是没有网络的,这其实是最安全的配置——Agent 跑代码但连不上任何东西。但很多任务需要联网(比如 pip install、调用 API),这时候就要开网络。
Firecracker 的网络通过 tap 设备实现。宿主机上创建一个 tap 设备,配置好 IP 和路由,然后通过 Firecracker API 把它挂给 VM。VM 里看到的是一个 virtio-net 网卡。这个方案的问题是:tap 设备在宿主机上是一个真实的网络接口,如果配置不当,VM 可能访问到宿主机的内网,甚至通过宿主机访问外网。
安全做法是给每个 VM 分配独立的网络命名空间,在命名空间里配置 NAT 和防火墙规则,限制 VM 只能访问白名单地址。更严格的做法是用用户态网络栈(比如通过 vsock 转发),完全不走内核网络路径。vsock 是 virtio 的 socket 接口,宿主机和 guest 之间可以直接通信,不经过网络协议栈,适合 Agent 和宿主机控制平面之间的通信。
提示:如果 Agent 任务不需要外网,强烈建议不开 tap 网络,只用 vsock 做控制通道。这样即使 Agent 被恶意代码控制,也无法发起任何网络请求,安全边界清晰得多。
3.4 资源限制:cgroup 与 vCPU 配额
Firecracker 本身不做资源限制,它把 vCPU 和内存的配置交给调用方。你可以在创建 VM 时指定 vCPU 数量和内存大小,但 CPU 时间片、IO 带宽这些需要靠 cgroup 在宿主机层面限制。
实践中,每个 Agent Sandbox 的 VM 通常配 1 到 2 个 vCPU、128MB 到 512MB 内存。vCPU 数量不是越多越好——Firecracker 的 vCPU 是独立线程,vCPU 多了上下文切换开销大,而且 Agent 任务大多是单线程的。内存方面,给太少会导致 OOM,给太多浪费。建议根据任务类型定:跑 Python 脚本 256MB 够用,跑编译任务可能要 1GB。
cgroup 限制要卡在 Firecracker 进程上。用 cgroup v2 的话,把 Firecracker 进程放进一个 cgroup,设置cpu.max限制 CPU 配额,memory.max限制内存上限。这样即使 VM 里的进程想跑满 CPU,也会被 cgroup 限住。注意memory.max要留出 Firecracker 自身开销的空间,不能设得和 VM 内存一样大。
4. 实操过程与核心环节实现
4.1 环境准备:从裸机到可运行 Firecracker
先确认宿主机环境。以 Ubuntu 22.04 为例,第一步是检查 KVM 支持:
grep -E 'vmx|svm' /proc/cpuinfo | head -1 lsmod | grep kvm ls -l /dev/kvm如果/dev/kvm不存在,加载模块:
sudo modprobe kvm_intel # Intel CPU # 或 sudo modprobe kvm_amd # AMD CPU然后安装 Firecracker。官方提供静态编译的二进制,直接下载解压即可:
ARCH=$(uname -m) release_url="https://github.com/firecracker-microvm/firecracker/releases" latest=$(basename $(curl -fsSLI -o /dev/null -w %{url_effective} ${release_url}/latest)) curl -L ${release_url}/download/${latest}/firecracker-${latest}-${ARCH}.tgz \ | tar -xz sudo mv release-${latest}-${ARCH}/firecracker-${latest}-${ARCH} /usr/local/bin/firecracker准备内核和 rootfs。官方提供了一个快速开始的脚本,但生产环境建议自己构建。内核用 Firecracker 的配置模板编译,rootfs 用 debootstrap 或 Alpine 构建。
4.2 手动拉起一个 microVM:完整配置流程
Firecracker 的配置通过 REST API 完成,它监听一个 Unix socket。启动 Firecracker 进程:
firecracker --api-sock /tmp/firecracker.sock --no-api--no-api表示不自动启动 VM,等 API 调用。然后用 curl 配置:
# 配置 boot source curl --unix-socket /tmp/firecracker.sock -X PUT \ http://localhost/boot-source \ -H 'Content-Type: application/json' \ -d '{ "kernel_image_path": "/path/to/vmlinux", "boot_args": "console=ttyS0 reboot=k panic=1 pci=off" }' # 配置 rootfs curl --unix-socket /tmp/firecracker.sock -X PUT \ http://localhost/drives/rootfs \ -H 'Content-Type: application/json' \ -d '{ "drive_id": "rootfs", "path_on_host": "/path/to/rootfs.ext4", "is_root_device": true, "is_read_only": false }' # 配置 vCPU 和内存 curl --unix-socket /tmp/firecracker.sock -X PUT \ http://localhost/machine-config \ -H 'Content-Type: application/json' \ -d '{ "vcpu_count": 2, "mem_size_mib": 512 }' # 启动 VM curl --unix-socket /tmp/firecracker.sock -X PUT \ http://localhost/actions \ -H 'Content-Type: application/json' \ -d '{"action_type": "InstanceStart"}'启动后,VM 的串口输出会打到 Firecracker 进程的 stdout。如果配置了console=ttyS0,你能看到 guest 内核的启动日志。看到登录提示就说明 VM 起来了。
4.3 快照的创建与恢复:把启动时间压到 10ms
快照是 Agent Sandbox 性能的关键。创建快照前,VM 要先暂停:
# 暂停 VM curl --unix-socket /tmp/firecracker.sock -X PATCH \ http://localhost/vm \ -H 'Content-Type: application/json' \ -d '{"state": "Paused"}' # 创建快照 curl --unix-socket /tmp/firecracker.sock -X PUT \ http://localhost/snapshot/create \ -H 'Content-Type: application/json' \ -d '{ "snapshot_type": "Full", "snapshot_path": "/path/to/snapshot", "mem_file_path": "/path/to/memfile" }'恢复快照时,启动一个新的 Firecracker 进程,加载快照:
curl --unix-socket /tmp/firecracker-new.sock -X PUT \ http://localhost/snapshot/load \ -H 'Content-Type: application/json' \ -d '{ "snapshot_path": "/path/to/snapshot", "mem_backend": { "backend_path": "/path/to/memfile", "backend_type": "File" }, "enable_diff_snapshots": false, "resume_vm": true }'resume_vm: true表示加载后立即恢复运行。实测下来,从快照恢复到 VM 可执行任务,稳定在 8 到 15ms 之间,比冷启动快 10 倍以上。
注意:快照恢复出来的 VM 和原 VM 的 MAC 地址、IP 地址是一样的。如果同时恢复多个 VM,网络会冲突。解决办法是在恢复后通过 API 重新配置网络设备,或者用
enable_diff_snapshots配合网络重置。
4.4 任务注入与结果回收:vsock 通道
VM 起来之后,怎么把任务代码送进去、怎么拿结果出来?最干净的方式是 vsock。Firecracker 支持 virtio-vsock,宿主机和 guest 之间可以建立 socket 连接,不经过网络。
配置 vsock:
curl --unix-socket /tmp/firecracker.sock -X PUT \ http://localhost/vsock \ -H 'Content-Type: application/json' \ -d '{ "guest_cid": 3, "uds_path": "/tmp/vsock.sock" }'guest 里跑一个 agent 进程监听 vsock 端口,宿主机通过uds_path连接进去,发送任务代码,接收执行结果。这个通道是双向的,也可以用来传文件、传环境变量。
任务执行的完整流程是:恢复快照 → 通过 vsock 发送任务 → guest 内执行 → 通过 vsock 返回结果 → 销毁 VM。整个过程在 100ms 级别完成,其中大部分时间花在任务本身的执行上,沙箱开销只占很小一部分。
5. 安全边界到底卡在哪:威胁模型与防护要点
5.1 隔离层级的真实强度
Firecracker 的安全边界建立在 KVM 之上。KVM 是 Linux 内核的一部分,它利用 CPU 的虚拟化扩展(VT-x/AMD-V)实现硬件级隔离。guest 代码运行在非特权模式(ring 3 或 guest ring 0),无法直接访问宿主机的内存和硬件。要逃逸,攻击者需要利用 KVM 或 Firecracker 的漏洞,难度远高于容器逃逸。
但这不意味着绝对安全。历史上 KVM 和 Firecracker 都出过漏洞(比如 VENOM、Firecracker 的 CVE-2019-18960),只是利用难度和影响范围比容器漏洞小得多。实践中,安全边界要分层设计:Firecracker 提供硬件隔离,seccomp 限制 Firecracker 进程的系统调用,cgroup 限制资源,网络隔离限制外联。任何一层被突破,其他层还能兜底。
5.2 攻击面收敛:Firecracker 进程本身怎么防
Firecracker 进程运行在宿主机上,它本身也是一个攻击目标。如果攻击者能控制 Firecracker 进程,就能控制所有 VM。所以 Firecracker 进程要用最小权限运行:非 root 用户、只保留必要的 capability、seccomp 白名单限制系统调用。
Firecracker 官方提供了一个 seccomp 过滤器,只允许它需要的系统调用。启用方式是在启动时加--seccomp-filter参数,或者用 jailer 工具。jailer 是 Firecracker 配套的沙箱工具,它会 chroot 到指定目录、切换用户、设置 cgroup、应用 seccomp,把 Firecracker 进程关进一个更小的笼子里。
提示:生产环境强烈建议用 jailer 启动 Firecracker,不要直接跑裸的 firecracker 二进制。jailer 的配置虽然麻烦,但它把 Firecracker 进程的攻击面收敛了一个数量级。
5.3 侧信道与资源竞争
多租户场景下,侧信道攻击是个绕不开的话题。同一台宿主机上跑多个 VM,它们共享 CPU 缓存、内存带宽、分支预测器等硬件资源。理论上存在通过缓存时序推断其他 VM 行为的可能(类似 Spectre/Meltdown 那一类)。
Firecracker 层面能做的缓解有限,主要靠 CPU 微码更新和内核的侧信道缓解措施。实践中,如果租户之间信任度低,可以考虑 CPU 绑核(把不同 VM 的 vCPU 绑到不同物理核)、关闭超线程、或者用物理机隔离。但这些都会降低密度,需要在安全和成本之间权衡。
对于大多数 Agent Sandbox 场景,租户是平台自己的用户,信任度中等,侧信道的实际风险不高。但如果你的平台允许任意用户提交代码,且用户之间有竞争关系,那就要认真评估。
6. 常见问题与排查技巧实录
6.1 启动失败类问题速查
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
Permission denied打开 /dev/kvm | 用户不在 kvm 组 | ls -l /dev/kvm | 加入 kvm 组或改权限 |
KVM not supported | CPU 不支持或模块未加载 | grep vmx /proc/cpuinfo | 加载 kvm 模块或换机器 |
| VM 启动后无输出 | 内核镜像格式不对 | 检查是否 vmlinux | 用官方内核或重新编译 |
Failed to create tap | 缺 CAP_NET_ADMIN | capsh --print | 给进程加 capability |
| 快照恢复后网络不通 | 网络状态未重置 | 检查 tap 设备 | 恢复后重新配置网络 |
6.2 性能调优的实操心得
启动速度的瓶颈通常在 rootfs 加载。如果 rootfs 是 1GB 的 ext4 镜像,每次冷启动都要读 1GB 数据,即使有页缓存也要几百毫秒。优化方向有两个:一是用快照,跳过 rootfs 加载;二是精简 rootfs,把镜像压到 100MB 以内。
内存开销的优化在于共享。Firecracker 的快照恢复用 copy-on-write,多个 VM 可以共享同一份内存页,只有写操作才触发复制。如果任务以读为主,内存密度可以很高。实测一台 64GB 内存的机器,跑 500 个 256MB 的 VM 没问题,因为大部分内存页是共享的。
CPU 开销主要在 vCPU 线程的调度。如果 vCPU 数量超过物理核数,会有上下文切换开销。建议 vCPU 总数不超过物理核数的 2 倍,并且用 cgroup 限制每个 VM 的 CPU 配额,避免单个 VM 抢占过多资源。
6.3 那些文档里不会写的坑
第一个坑是 fd 泄漏。Firecracker 每个 VM 会打开若干 fd(KVM fd、tap fd、vsock fd、rootfs fd)。如果销毁 VM 时没有正确关闭,跑几千次之后进程就会因为 fd 耗尽而失败。解决办法是在销毁流程里显式关闭所有 fd,或者定期重启 Firecracker 进程。
第二个坑是时间同步。快照恢复出来的 VM,系统时间是冻结时的时刻,可能和宿主机差很远。如果任务依赖时间(比如证书校验、日志时间戳),会出问题。解决办法是在恢复后通过 vsock 同步时间,或者在 guest 里跑 NTP。
第三个坑是熵池。新启动的 VM 熵池是空的,如果任务需要随机数(比如生成密钥),可能会阻塞。解决办法是在内核启动参数里加random.trust_cpu=on,或者用 virtio-rng 设备给 guest 提供熵源。
第四个坑是 rootfs 的写放大。如果多个 VM 共享同一个 rootfs 镜像文件,且以读写模式挂载,会互相干扰。正确做法是每个 VM 用独立的 overlay 层,或者用只读基础镜像 + 临时可写层。
7. 从单机到集群:Agent Sandbox 的扩展思路
单机跑 Firecracker 只是起点,真正的 Agent Sandbox 平台需要管理成百上千台机器上的几十万个 microVM。这时候要考虑的是调度、镜像分发、快照管理和故障恢复。
调度层面,需要一个中心化的调度器决定哪个任务分配到哪台机器。调度依据包括机器的剩余资源、已有 VM 数量、镜像缓存情况。如果任务需要特定镜像,优先调度到已经缓存了该镜像的机器,避免重复拉取。
镜像分发是个容易被低估的问题。一个 500MB 的 rootfs 镜像,分发到 100 台机器就是 50GB 流量。实践中通常用 P2P 分发(比如 Dragonfly、Kraken)或者分层镜像(基础层共享,差异层按需拉取)来降低带宽压力。
快照管理要考虑版本和失效。基础镜像更新后,旧快照就失效了,需要重新生成。快照文件本身也可能很大(内存快照和磁盘快照加起来可能几个 GB),需要定期清理。建议给快照打上版本标签,和基础镜像版本绑定,更新时批量重建。
故障恢复方面,Firecracker VM 是无状态的(任务执行完就销毁),所以单机故障影响有限。但如果一台机器上跑了几百个 VM,机器挂了这些任务都要重试。调度器需要能检测机器故障并把任务重新分配到其他机器。任务本身要设计成幂等的,或者至少能安全重试。
我个人在实际操作中的体会是,Agent Sandbox 的复杂度不在 Firecracker 本身,而在它周边的这套管理体系。Firecracker 提供了很好的隔离原语,但怎么把这些原语组织成一个高可用、高密度、易运维的平台,才是真正花时间的地方。建议一开始就把快照、镜像分发、调度这三块设计好,不然后期改起来很痛苦。