每天万亿次调用的底层
AWS Lambda 每天运行万亿次函数调用。Fly.io 调度数百万容器。每个都是完整 Linux 内核,在 fraction of a second 内启动。
怎么做到的?大约 5 万行 Rust 代码,叫 Firecracker。
它存在是因为行业终于承认:一个控制资源使用的 Linux 容器,从来就不是为安全边界设计的。
容器不是安全边界
你笔记本上的每个 Docker 容器都是三个 Linux 内核特性穿风衣假装的:
Namespaces 是眼罩
进程在里面获得系统的私有视图:自己的 PID 列表、网络栈、挂载表、主机名、用户 ID。容器里的 PID 1 是宿主机上的某个随机 PID;容器甚至看不到其他进程。
cgroups 是预算
控制组是内核的会计和限速层。它们限制进程树被允许消耗的 CPU、内存、磁盘 IO 和网络带宽。
seccomp + capabilities 是白名单
capabilities 把 root 的权力切成约 40 个独立权限(绑定低端口、加载内核模块、挂载文件系统等),这样你只授予需要的那些。seccomp 是每个进程的过滤器,决定进程甚至被允许做哪些 syscall。
所有保护都漏斗进单个 Linux 内核,暴露约 400 个 syscall。宿主机上的每个容器都调用同一个内核。任何一个 syscall 里的 bug,机器上的每个租户都完蛋。
完整 VM 的问题
完整虚拟机用蛮力解决隔离:每个 VM 有自己的内核。
现代 CPU 有"guest mode",在真实硅片上运行 guest 指令。宿主机只在 guest 做特权操作时才介入(触碰真实硬件、故障、被中断)。Hypervisor 是仲裁这些时刻的薄层。
Linux 把它的 hypervisor 作为内核模块 KVM 发布,暴露在 /dev/kvm。
但完整 VM 又慢又胖。经典 QEMU VM 模拟整个想象中的 PC(BIOS、PCI 总线、IDE 控制器、VGA 卡、PS/2 键盘),因为那是 1998 年的 OS 期望启动时面对的。镜像几百兆。启动几秒。内存占用几百 MiB,在你的 workload 开始前。对于一个存活 40ms 的 web 请求,你花 40 倍时间启动机器。
所以你在两难之间:
- 容器:50ms 启动,5 MiB 开销,共享内核攻击面
- VM:5+ 秒启动,300+ MiB 开销,硬件隔离
每个运行不可信多租户代码的人(AWS、基本上每个现有 AI 沙箱厂商)需要同时拿到两边。
MicroVM:删除 1998
VMM(虚拟机监视器)是驱动 hypervisor 的用户空间进程:设置 guest 内存、插入虚拟设备、告诉 KVM 开始运行 guest 代码。
MicroVM 是删除了 1998 年 PC 的 VMM:没有 BIOS、没有 PCI 总线、没有 VGA、没有 USB、没有 ACPI(真实桌面启动时经过的 legacy 硬件,对一个 40ms 函数调用都不相关)。剩下:KVM、一个串口控制台、 handful of virtio 设备(net、block、vsock)。
virtio 是标准的"我知道我在 VM 里"设备接口。Guest 通过轻量级虚拟 NIC 和磁盘(virtio-net、virtio-block)与 hypervisor 协作,而不是假装驱动真实的 Intel e1000 卡或 IDE 控制器。这种协作,加上上面缺失的所有 legacy 硬件,是 microVM 启动快的最大原因。
结果:
- ~125ms 从 VMM 启动到 guest userspace 运行 init
- <5 MiB VMM 内存开销(宿主机为每个 VM 支付的 bookkeeping 内存,在 guest workload 分配任何东西之前)
- 单主机 150 VMs/秒 创建速率
- ~2-8% 运行时性能损失 vs 裸机
与完整 VM 相同的硬件级隔离,与容器相同数量级的密度。
Firecracker 架构
2018 年 11 月,AWS 在 re:Invent 开源了 Firecracker。它已经在生产环境运行 Lambda——让你的 import pandas 冷启动快到可以按毫秒计费的东西。
Firecracker 从 Google 的 crosvm fork,用 Rust 重写,删了超过一半代码。每个 Firecracker 进程是一个 microVM,只有三种线程类型:
API thread — 接单台
绑定到 Unix socket 的 REST 服务器(本地 socket,以文件形式存在,不是 TCP 端口)。接受启动前的配置和启动后的有限操作。
VMM thread — 硬件车间
假装成 guest 能看到的每个设备。当 guest 戳它以为是 NIC 寄存器的东西时,CPU 暂停 guest,VMM 处理戳击("guest 踢了 TX 队列,排空它"),然后恢复。机制:guest 读/写魔法地址;CPU 把那些 trap 到宿主机。
vCPU threads — 执行者
每个 guest CPU 一个,各自在紧循环中:请求 KVM 运行 guest 直到有趣的事发生(设备戳、中断、halt),处理它,循环。
它们通过 Rust channels(进程内、线程间无锁消息队列)互相通信。Guest 只看到四个设备。
四个设备
- virtio-net:VM 的网卡,没有 1998 年模拟。Guest 把包写进 virtqueue(共享内存中的环形缓冲区);VMM 通过 host-side TAP 设备(内核暴露为文件的虚拟以太网接口)把它们排出去,由 io_uring 或 epoll 驱动,VMM 线程不会阻塞。
- virtio-block:VM 的磁盘,只是宿主机上的文件 IO。Guest 把扇区请求放进 virtqueue;VMM 对 host 文件发出普通 pread/pwrite。没有 IDE、没有 AHCI、没有 SCSI。
- virtio-vsock:VM 与宿主机的对讲机。用 (context-id, port) 元组寻址而非 IP/port,所以 guest agent 可以打电话回家(日志、健康 ping、快照元数据),不需要 guest IP,wire 上也没有可欺骗的东西。
- 8250 serial UART:启动控制台。固定地址模拟的微小 legacy 串口芯片。用于 virtio 启动前的 early-boot 日志和崩溃转储。便宜、通用、永远不会消失。
启动一个 microVM
API 是整个控制平面:配置通道,刻意与数据平面(实际运行 guest 代码的 vCPU 线程)分离。
# 1. 配置启动源
curl --unix-socket /tmp/fc.sock -X PUT 'http://localhost/boot-source' \
-H 'Content-Type: application/json' \
-d '{"kernel_image_path": "./vmlinux-6.1", "boot_args": "console=ttyS0 reboot=k panic=1 pci=off"}'
# 2. 配置 rootfs
curl --unix-socket /tmp/fc.sock -X PUT 'http://localhost/drives/rootfs' \
-d '{"drive_id": "rootfs", "path_on_host": "./rootfs.ext4", "is_root_device": true}'
# 3. 配置网络
curl --unix-socket /tmp/fc.sock -X PUT 'http://localhost/network-interfaces/eth0' \
-d '{"iface_id": "eth0", "guest_mac": "06:00:AC:10:00:02", "host_dev_name": "tap0"}'
sleep 0.015
# 4. 启动 VM
curl --unix-socket /tmp/fc.sock -X PUT 'http://localhost/actions' \
-d '{"action_type": "InstanceStart"}'
四个 HTTP 调用。这就是整个控制平面。
安全洋葱
单层 KVM 边界已经很强。Firecracker 又包了两层。
Jailer 是沙箱构建器。它的唯一工作是在 VMM 运行前把它装箱。它创建 chroot(锁定进程到单目录子树的 Linux 特性,进程根本不能命名上面的任何东西),进入新的 PID namespace 看不到宿主机其他进程,切换到无特权 uid/gid,应用 cgroup CPU/内存限制,然后才在那个 jail 里 exec Firecracker 二进制:
jailer --id vm-42 --uid 1000 --gid 1000 \
--chroot-base-dir /srv/jailer \
--exec-file /usr/local/bin/firecracker \
-- --api-sock /run/fc.sock
现在 VMM 进程本身没有文件系统,除了专用 chroot;看不到宿主机其他进程;没有 root capabilities。如果 guest-to-host 逃逸真的通过 virtio 或 KVM 落地,攻击者落在那个带 cgroup 限制的 chroot 里。
Seccomp 是每线程 syscall 白名单。不在列表上的任何东西在到达内核 syscall handler 前就被杀死(或返回 EPERM)。Firecracker 三级:
- Level 0:关闭。生产别用。
- Level 1:按 syscall 号白名单。
- Level 2:还约束参数值(比如 ioctl 可以,但只能用 KVM_RUN 作为命令)。默认且推荐。
每个线程拿到它可能的最小表面:API thread 不需要 ioctl(KVM_RUN);vCPU threads 不需要 socket()。
每层必须独立失败,攻击者才能到达宿主机。
Snapshots:Lambda SnapStart 的作弊码
对运行中的 microVM 拍快照。在毫秒级恢复到不同主机上的全新 VMM 进程。跳过内核启动、跳过 init、跳过 JIT 预热。
# 冻结 VM
curl --unix-socket /tmp/fc.sock -X PATCH 'http://localhost/vm' \
-d '{"state": "Paused"}'
# 创建快照
curl --unix-socket /tmp/fc.sock -X PUT 'http://localhost/snapshot/create' \
-d '{"snapshot_type": "Full", "snapshot_path": "/snap/vm.state", "mem_file_path": "/snap/vm.mem"}'
快照捕获预热后的状态,所以恢复的 VM 从生命中间醒来,不是从开头。
这正是 Lambda SnapStart 做的:初始化一次 Java Lambda,拍 microVM 快照,每次后续冷启动恢复那个快照。JVM 冷启动突然从 8+ 秒降到亚秒级。
替代方案对比
| 方案 | 隔离级别 | 启动时间 | 适用场景 |
|---|---|---|---|
| runc/bubblewrap | 共享内核 | ~50ms | 可信代码,快速简单 |
| gVisor | 用户空间内核 | ~100ms | 需要 syscall 级审计,无嵌套虚拟化 |
| Firecracker | 硬件虚拟化 | ~125ms | 不可信通用代码,需要真实 Linux 内核 |
| V8 Isolates | V8 沙箱 | ~5ms | 已知 JS 工作负载 |
| WASM | 确定性沙箱 | ~ms | 浅生态,非通用 drop-in |
Firecracker 坐在"我自己的内核,但没有 PCI BIOS"的格子里:硬件隔离、微小设备模型、毫秒级启动。
Agent 工作负载的新需求
第一代 AI Agent 住在你的应用内部。你导入库、接线循环、在现有后端里运行它。每次调用是到模型的 HTTP 往返。每次工具调用是你自己进程里的函数。"沙箱"是你自己的服务器。这是 LangChain、Vercel AI SDK、OpenAI Agents 的世界。
第二代不同了。
Claude Code、Codex CLI、OpenClaw 是主机级 Agent:接管机器的二进制文件,不是住在你里面的库。它们期望真实的 shell、包管理器、可写磁盘。当你给 Claude Code 一个任务时,它运行的是:
apt-get install -y git ripgrep build-essential
git clone https://github.com/user/project && cd project
npm install
npm run test
rg 'TODO' -l
# 编辑文件
# git commit
那是 shell/bash。需要真实文件系统、真实 fork/exec、包管理器、可写磁盘、可达网络。这些都不能表达为 chat-completion 工具 schema,在共享内核容器里与其他租户一起运行也不安全。
实验室正在直接在这些 harness 上 post-train 模型:shell、文件编辑器、测试运行器、Agent 循环本身。这意味着"模型 + 它被训练的 harness"与"模型 + DIY 脚手架"之间的差距每季度都在变大。
一个 Agent 一台完整 Linux 机器,运行 Agent 刚发明的不可信代码——这正是 Firecracker 为之构建的工作负载。上面的趋同不是偶然。
未来的基础设施
通用"运行任意代码"沙箱已经是商品。基础设施完全开源。microVM 层是 Firecracker 或 Cloud Hypervisor,Apache 2.0 许可。容器到 rootfs 转换是 200 行 Go 脚本。有才华的工程师一个周末就能搭起一个工作的沙箱平台。
你付费的是连接到 VM 的东西。裸 microVM 是 table stakes。
有趣的产品表面:
可观测性就是产品,不是调试辅助。Agent 做的每件事(stdout、syscall、文件写入、网络请求)都通过单个 socket 流向 host-side 收集器。Agent 构建者需要完整会话回放,和每个动作的 artifact 来创建最好的产品。
Secrets 在 wire 上代理,从不交给 guest。Guest 只看到占位符 env var;在沙箱里 echo $API_KEY 返回占位符。Host-side egress 代理(每个出站包必须穿过它)在 host-side TAP 替换真实凭证,对照显式白名单,带每会话审计追踪。Agent 可能正在运行 5 秒前生成的任意代码,仍然不能泄露它从未拥有的凭证。
身份在宿主机签名,不在 Agent 内部。出站请求可以携带加密每会话身份(包括 mTLS 签名,基于 HTTP Message Signatures + Ed25519),由宿主机在包离开 bridge 前铸造。签名密钥从不进入 microVM。
其他计算捆绑在同一个 microVM。Browserbase 把每个 Agent runtime 1:1 配对同一主机上的浏览器,经常在同一个 microVM。Agent 进程和 Chromium 之间的物理距离实际上为零:CDP 命令走 Unix socket,不是跨服务网络,所以动作延迟是个位数毫秒。Screencast 帧不需要跨公网到达会话回放。
你不能只在 Docker 上干净地把这些缝在一起。接缝不在那里。Agent runtime 市场不会用原始计算获胜,而是用最好的可观测性、secrets、身份、合作伙伴关系,和折叠进一个产品表面的同地计算。
结语
Firecracker 把计算中最古老的想法之一——虚拟机——删掉了足够多,让它变便宜。它赌的是:如果你能让它足够快,硬件强制隔离是值得的。
那个赌对 serverless 总是会有回报。变化的是"不可信多租户代码"工作负载已经从"我不想沙箱的 web 函数"增长到"Agent 生成可能触碰生产的任意命令"。边界移动了,对共享内核逃逸的容忍度从"可接受风险"变成"不可发布"。
它做到了。一个 5 万行的 Rust 二进制,跟 /dev/kvm 对话。
容器打包软件。MicroVM 隔离它。下一个十年有趣的工程,是你包在盒子周围的一切。