Firecracker:微型虚拟机
Firecracker: MicroVMs
AWS Lambda 的计费粒度是毫秒,租户是陌生人。容器共享宿主机内核,启动快,边界宽;传统虚拟机边界窄,启动却要拖着一整套模拟设备。2018 年开源、2020 年 NSDI 成文的 Firecracker 赌的是第三种形状:用 KVM 做硬件虚拟化,但把设备模型砍到几乎只剩 virtio 网卡、virtio 块设备和一条串口。论文给出的数字是:Linux 客户机在 125 毫秒内启动,每个实例的内存开销低于 5 MiB。
这篇文章要拆的不是“又一个 hypervisor”,而是设备模型与启动路径如何决定隔离成本。虚拟机慢,常常不是因为 CPU 虚拟化指令贵,而是因为有人坚持模拟一套 1990 年代的 PC。
破除误解
误解
**误解一:虚拟机天生比容器慢一个数量级。实情**
慢的是完整 PC 机的模拟:PCI 总线、启动固件、一堆从不被无服务器工作负载用到的设备、以及客户机内核在这些设备上的探测。Firecracker 把可热插拔的设备清单收成极短的一张表,客户机几乎没有东西可探测。KVM 提供的那条 VM exit 路径还在,只是退出之后要模拟的事情少了。NSDI 论文把启动时间和每实例内存开销放到与容器同一量级,不是因为虚拟化税消失了,而是因为税的大头本来就不在那条 CPU 路径上。
误解二:microVM 等于“小一点的 QEMU”。
QEMU 是通用模拟器,设备模型以兼容性为第一目标,代码体量大,攻击面随设备一起长。Firecracker 用 Rust 重写 VMM,明确不为桌面、不为任意外设、不为完整 ACPI 动物园服务。目标负载是短寿命、无状态或少状态的函数与容器任务。兼容性从“像一台 PC”降到“能启动一份指定的 Linux 内核和一份根文件系统”。少掉的那些设备,不是优化掉的,是拒绝实现的。
误解三:有了 microVM,就不需要容器那套 cgroup 与 seccomp。
Firecracker 的进程自己还要被关进 jailer:先降权、改根、加 seccomp、再进入 KVM 循环。客户机与宿主机之间的边界是虚拟化;VMM 与宿主机其余部分之间的边界是普通 Linux 进程隔离。两条边界叠在一起,才是 Lambda 多租户能接受的形状。拿掉 jailer,VMM 里的缺陷会直接变成宿主机进程上的缺陷。论文把这一点写进了设计,而不是当成部署附录。
设备模型:把攻击面写成一张短表
客户机能碰到的东西,大约就是:一块 virtio-net、一块 virtio-block、串口控制台,以及用来复位的极简键盘设备。没有 USB,没有 VGA,没有任意 PCI 直通的热闹菜单。网络和磁盘走 virtio,是因为这条半虚拟化路径已经被 Linux 客户机广泛支持,不必再模拟真实网卡的寄存器迷宫。串口存在,是因为函数计算仍然需要一条把日志送出来的管子,而不是因为有人要在上面跑 BIOS 设置界面。
短表的安全含义很具体。VMM 里每一段设备模拟代码都是一次从客户机到宿主机的数据转换。转换代码越少,客户机输入能抵达的代码路径越少。这不是形式化证明,这是在缩小可信计算基的表面积。Rust 在这里的贡献主要是内存安全默认值:缓冲区越界和释放后使用这类缺陷,在 C 写成的 VMM 里反复出现,在 Rust 里被类型系统挡在编译期。它不证明设备状态机正确,它只让一类最常见的内存缺陷不再是默认风险。
启动路径同样被砍过。Firecracker 不走完整的客户机 BIOS 探测。它用一种极简的启动协议把内核、内核命令行和根文件系统塞进客户机内存,然后直接跳到 Linux 入口。客户机内核仍然要做自己的初始化,但省掉了固件阶段那几百毫秒,也省掉了固件代码这条额外的信任依赖。快,来自少做;少做,同时收紧了边界。
快照是这条路径的后续。Lambda 一类负载会反复启动几乎相同的客户机。把已经初始化完的内存做成快照,下一次从快照恢复,比从内核入口再走一遍更便宜。恢复路径引入了新的一致性问题:设备队列、 inflight 的 virtio 描述符、客户机看到的时钟,都必须在快照里说清楚。这不是设备模型的对立面,这是设备模型必须保持极简的另一个理由——要保存和恢复的状态机越少,快照才越可能做对。
密度:一台上要站住的是函数,不是虚拟机产品
NSDI 论文关心的密度数字,是一台宿主机上能同时活着的 microVM 数量,以及每秒能启动多少个新实例。内存开销低于 5 MiB,说的不是客户机内存——客户机内存由函数自己决定——而是 VMM 自己的常驻成本。若 VMM 像传统栈那样先吃掉几十兆,再乘上每台机器上百个租户,密度会先被 VMM 自己挤死。
CPU 记账走 cgroup,网络带宽和速率也可以在宿主机一侧限流。Firecracker 并不发明新的资源隔离原语,它把已有的 Linux 机制用在 VMM 进程上,再让客户机内核在自己的小世界里调度线程。于是出现两层调度:宿主机调度 VMM,客户机调度函数里的用户线程。无服务器平台必须同时看这两层。只看客户机内部的利用率,会看不见邻居 VMM 在宿主机上的争抢;只看宿主机,会看不见函数自己的事件循环有没有把 vCPU 空转。
和 Kata Containers、gVisor 放在同一张光谱上,位置会更清楚。Kata 也是“每个容器一台轻量虚拟机”,更强调与 OCI/Kubernetes 工作流兼容,设备模型和启动路径可以更通用。gVisor 走另一条路:用用户态内核拦截系统调用,不走硬件虚拟化,边界在系统调用语义的再实现上。Firecracker 选择硬件虚拟化加极简设备,换来的是与 Linux 客户机二进制兼容,以及一条窄的 VM exit 边界。三者都承认:默认容器的系统调用面太宽,不适合陌生人租户。他们拒绝的方式不同,账单也不同。
NSDI 论文写明:Firecracker 已用于 AWS Lambda 与 Fargate 的生产隔离。Fargate 要的是容器体验加虚拟机边界——用户仍然交镜像,平台把镜像放进微型虚拟机。和 Kata 的差别不在“要不要虚拟机”,而在设备表有多短、控制平面是否拥有客户机内核。
生产里的 Firecracker 还要面对一个产品约束:函数的代码是用户给的,内核和 VMM 是平台给的。平台可以锁定客户机内核版本、锁定允许的设备、锁定 seccomp 配置。这和桌面虚拟化“用户自带任意操作系统”完全相反。极简之所以可行,是因为控制平面拥有内核。一旦你想让用户启动任意发行版、任意驱动,设备表就会重新膨胀,microVM 会退回普通虚拟机。
边界停在哪里
硬件虚拟化把客户机关进另一套页表和另一套特权级。这比共享内核窄,但不是神话。VMM 仍在宿主机上以进程身份运行,KVM 仍是宿主机内核的一部分。内核里的虚拟化实现若有缺陷,隔离会在这条通道上失败。Firecracker 能做的是:让通往该通道的设备数据尽可能少、让 VMM 进程的权限尽可能低。它不能把 KVM 从可信计算基里删掉。
另一条停驻点是 virtio 后端。块设备和网卡的数据最终要在宿主机上变成文件描述符和套接字。后端实现的正确性、以及它与 jailer 权限的配合,决定了客户机 I/O 会不会变成宿主机文件系统上一次越权访问。把设备砍短,是为了让这条后端可以被审计完。把它再接回复杂的存储和网络插件,表面积会从 VMM 重新长到插件里。
所以 Firecracker 最有教益的句子不是“虚拟机可以很快”,而是:隔离强度由你愿意模拟多少接口决定,启动速度由你拒绝模拟多少接口决定,这两件事可以是同一张短表。 容器选择模拟零个硬件接口、暴露整个系统调用表;传统 VMM 选择模拟一台完整电脑;Firecracker 选择只模拟函数还活得下去的那几个。选择一旦做完,性能数字和安全叙事会一起从这张表里长出来。
控制平面:谁被允许启动这一份内核
Lambda 的控制平面决定客户机内核版本、根文件系统里有哪些库、以及函数进程以什么用户启动。Firecracker 只是这条链上的 VMM。镜像构建、热路径上的快照库、按租户划分的网络 namespace,都不在 NSDI 论文的 125 毫秒里。把论文数字理解成“随便启动一个发行版只要 125 毫秒”,会漏掉控制平面预先付掉的那些成本:内核已经裁过、设备已经枚举成短表、virtio 后端已经接到平台的块存储和弹性网卡上。
jailer 的顺序是这份控制平面在宿主机上的投影。进程先进入新的 namespace,再 chroot 到一个几乎空的目录,再丢掉能力,再装上 seccomp 允许列表,最后才打开 KVM。顺序不能反:先打开 KVM 再降权,等于把最宽的接口留在权限还很高的阶段。seccomp 列表只放 VMM 还活得下去的那些系统调用。列表越短,VMM 即使被客户机输入打出内存缺陷,能拿来转身碰到宿主机其余部分的系统调用也越少。这是纵深,不是证明。它把失败时的残局从“任意系统调用”收成“短名单上的系统调用”。
网络侧通常还要再套一层。客户机看见的 virtio-net 对上宿主机的 TAP 或类似设备,再由平台的规则决定这个接口能不能出公网、能不能看见邻居租户。块设备对上的是按租户切开的快照或临时盘。设备模型短,并不自动给出多租户网络隔离;它只保证 VMM 不会再额外模拟一块可以绕过这些规则的神秘网卡。平台若在后端把所有 TAP 桥到同一广播域,短表救不了你。Firecracker 缩小的是 VMM,不是整朵云的网络策略。
和容器叠在一起,而不是互相替代
生产路径很少是“只用 Firecracker、丢掉 cgroup”。jailer 自己就在用命名空间、cgroup 和 seccomp。Lambda 的沙箱是纵深:客户代码在客户机内核里,VMM 在 jailer 里,宿主机内核只通过 KVM 和极窄设备与之见面。对照本站的 Linux 容器篇:容器把边界画在系统调用表上,microVM 把边界画在虚拟硬件上。同一台机器上两种机制同时存在,账单也同时存在。
快照把已经付过的启动功存起来。设备越少,快照越小,恢复越像一次内存拷贝而不是一次开机。SnapStart 一类产品特性依赖的正是这张短设备表。设备一多,快照体积和恢复路径一起膨胀,微型的优势被兼容性吃回去。
不适用的场景同样硬。需要 GPU 直通、复杂 PCI、热迁移博物馆、或者把通用发行版当客户机原样塞进去,都会把你送回 QEMU 或别的监视器。Firecracker 证明的是:虚拟机的历史重量来自兼容性,不是来自“有客户机内核”这一事实。密度仍低于容器:每份沙箱一份客户机内核,函数若只有几毫秒 CPU,这份固定内存就是主成本。
可观测性会变差。宿主机进程表里看见的是监视器,不是客户机里的语言运行时。排障要走串口、客户机代理,或接受沙箱是黑盒。编排也不能直接套用容器的 Pod 语义:网卡是 virtio 后端接到 tap,块设备是文件或快照,限流可能在 VMM 令牌桶而不是只在 cgroup。自建这条路径的人,等于同时当虚拟化管理员和函数平台管理员。云厂商把这些藏进服务,并不表示账单消失,只是账单换了付款人。
攻击面搬家,并不等于攻击面消失
客户机内核会有漏洞,KVM 会有漏洞,virtio 后端会有漏洞。窄边界降低的是可达代码路径的面积,不是把可信计算基删成空集。客户机内核未打补丁,等于在隔离舱里留了内鬼:它打不穿宿主机,仍能在舱内翻同事的数据——如果错误地把多个租户塞进同一客户机。Lambda 的模型是一函数一舱,密度账单里已经包含了这份重复的内核。合并租户到同一客户机,会把隔离退回容器级的共享内核故事。
速率限制被做进设备路径,为的是几千个邻居公平分享带宽和每秒输入输出。没有这些,密度会先把磁盘和网卡打死。控制面每秒创建大量微型虚拟机,意味着每秒大量根盘、网络配置和元数据。Firecracker 把数据面写小,是为了让控制面还付得起。谁只抄了 VMM、没抄控制面,会在密度还没上去时先被编排拖垮。
跨域连接
- 组合数学:设备模型是攻击路径的生成器。每增加一种设备,就增加一组寄存器、队列和配置空间,客户机输入与宿主机代码路径的组合数随之上升。Firecracker 把设备集合截成极小的基数,不是因为函数不需要功能,而是因为组合爆炸一旦开始,审计和测试只能覆盖稀疏截面。启动协议同样如此:少一个固件阶段,就少一层与内核入口之间的中间状态组合。极简是组合控制,不是美学。
- 证明:Firecracker 没有 seL4 那种精化定理,它用的是另一种较弱的证明策略——通过缩小前提来缩小需要论证的对象。设备短表、Rust 类型、jailer 的降权,都是在说:我们不证明任意 VMM 安全,我们证明(或者说服自己)这份被约束过的 VMM 更可能被人工审完。结论强度跟着前提走。把论文里的启动数字读成“虚拟化已被证明安全”,是把约束从句子里删掉了。
- 国家能力:jailer 像一套行政程序:先收缴权限,再允许进入 KVM 循环,事后也不把权限发还。Lambda 的多租户能运转,靠的不是每个租户的美德,而是平台把强制力前置到进程启动协议里。国家能力文献里反复出现的教训在这里同样成立:强制力要持续供给——seccomp 配置、内核版本、设备白名单都必须有人维护。停发这些文书,剩下的只是一个普通的 Rust 进程在跑虚拟机。
- 热力学定律:把 VMM 常驻内存压到数兆、把启动压到百毫秒,是在密度约束下做的能量与状态预算。每多模拟一个设备,就多一份常驻状态和一份每次启动都要初始化的热。无服务器的经济模型按毫秒收费,不允许把热量浪费在客户机永远不会打开的 VGA 上。热力学第一定律在这里是会计恒等式:隔离税不会消失,只会从 CPU 虚拟化那一项,转移到你仍然坚持模拟的接口上。砍接口,就是砍那一项税。
参考文献
- Agache, A. et al. Firecracker: Lightweight Virtualization for Serverless Applications. USENIX NSDI, 2020.
- Young, E. G. et al. The True Cost of Containing: A gVisor Case Study. USENIX HotCloud, 2019.(对照:用户态内核路线的账单)
- Kata Containers 架构文档与 OCI 运行时说明(2017 年起)。(对照:为容器工作流保留更多通用设备的轻量虚拟机)
- Barham, P. et al. Xen and the Art of Virtualization. ACM SOSP, 2003.(半虚拟化与设备 I/O 的前史)
- Kivity, A. et al. kvm: the Linux Virtual Machine Monitor. Linux Symposium, 2007.
延伸阅读
- Brooker, M. 等 AWS 公开演讲与博客:Lambda 隔离、快照恢复与每租户密度的运维视角。
- Rust 官方文档中关于
unsafe边界的说明——读 Firecracker 时用来判断哪些设备路径仍可能离开类型系统的保护。 - NIST SP 800-190 Application Container Security Guide (2017)。(共享内核的风险框架,可对照 microVM 收窄了哪一层)