IT 论文精读 · PAPER 67
Agache 等 · Amazon Web Services · NSDI 2020
现在有一类叫「云函数」的服务:你只上传一小段代码,不用管服务器,有人调用时它才跑起来、按毫秒计费,AWS Lambda 是其中最大的一家。2020 年亚马逊发论文公开了 Lambda 底下真正跑你代码的那个东西——Firecracker,一台被削到只剩骨架的虚拟机。它让同一台物理机能同时装下成千上万个互不信任的客户的代码,每一台还能在八分之一秒内开机。
把陌生人的代码放进自己机器里跑,历来要在安全和快之间选一个。
传统虚拟机很安全——它给每位客人发一整台「假电脑」,客人在里面折腾不出来。代价是开机要好几秒、还得占掉几百 MB;为了一个只跑 20 毫秒的函数这么干,太荒谬。
另一头是容器:不发假电脑,所有客人合用同一个操作系统,只是各自关在自己房间里,轻且开机只要毫秒。可房间的墙就是「同一个操作系统」,而它对外开着三百多个入口,历史上被撬开过不止一次。让互不信任的陌生人合用一个操作系统,亚马逊不敢。过去大家默认:只能二选一。
Firecracker 的点子是去质问这个默认:虚拟机慢,到底慢在哪?
答案是——不慢在「隔离」,而慢在它太想当一台真电脑。通用虚拟机为了「装什么系统都能跑」,忠实地伪造了一整台 PC:开机自检、光驱软驱、声卡显卡、USB 口、一整条插槽总线……客人开机时得把这一柜子假硬件逐个点名清查,几秒钟就花在这上头。可云函数要跑的只是「一段代码」,不是「任意一台古董电脑」。那就把假电脑削成只剩必需的几件。
Firecracker 发给每位客人的「电脑」上只剩四样:一块网卡、一块硬盘、一根用来吐日志的线,以及一个只有一个按键的键盘——那个键是关机。没有开机自检,没有插槽总线,连开机画面都没有:它直接把操作系统搬进内存,从第一条指令开始跑。
就像胶囊旅馆不再给每间房配厨房、车库和花园,只留床、门锁和水电——客人要的本来就只是睡一晚。房间小,一层楼塞得下几千间;打扫得快,退房到下一位入住只要一眨眼。
还有两件事顺手一起做了。一是换建材:这个「发房间」的程序用一门不容易写出漏洞的语言重写,总共约五万行代码,而老牌同类软件有上百万行——代码少,能被撬的缝就少。二是房门外再加一道锁:万一真有人钻出了房间,落脚处也不是走廊,而是一个提前被剥光权限的小铁笼。
每台这样的「微型虚拟机」额外只多花不到 5 MB 内存,从创建到跑上客户代码约 125 毫秒,一台机器每秒能造出 150 台。于是同一台物理机上可以密密麻麻住满不同客户的函数,谁也碰不到谁——安全和快,不必二选一了。它 2018 年开源,如今 Lambda 与 Fargate 都跑在上面。
诚实说一句代价:这台电脑太特化——没有显卡、没有扩展插槽,需要这些的活它干不了。
虚拟机又慢又重,不是因为隔离贵,而是因为它假装自己是一台什么都有的真电脑。把假硬件削到只剩网卡、硬盘、一根日志线和一个关机键,再用更稳的语言重写、外面套一层铁笼——隔离还是虚拟机级别的,开机却只要八分之一秒、每台只多占几 MB。云函数的「又安全又便宜」,就是这么同时拿到的。
想看它的结构图、被否决的几条路线和实测数字? → 切到精读版
Firecracker 是 AWS 为 Lambda / Fargate 自研的一个极简虚拟机监视器(VMM):它砍掉传统 VMM 为通用性而模拟的绝大部分硬件,只留 virtio 网卡、virtio 块设备、一个串口和一个只管关机的键盘控制器,用约 5 万行 Rust 写成、跑在 Linux KVM 之上——于是每台「microVM」的 VMM 内存开销 < 5 MiB、约 125 ms 就能跑到用户代码、单机每秒可创建 150 台,第一次把虚拟机级别的隔离做进了容器级别的密度与启动速度。
作者是 Alexandru Agache、Marc Brooker 等一队 AWS 工程师,论文发在 NSDI 2020;Firecracker 本身 2018 年底就已开源,代码起步时是从 Google 为 Chrome OS 写的 Rust 版 VMM crosvm 分叉出来的,此后各自演化。它上承 KVM 与 virtio 这两件把虚拟化做轻的基础设施,下启 rust-vmm 生态与 Cloud Hypervisor 等「用 Rust 拼一个自己的 VMM」的项目。与上一篇 Berkeley View 对读正好成一对:那篇讲 serverless 该是什么、缺什么,这篇讲它底下那块砖是怎么烧出来的。
Lambda 早期的隔离是靠 EC2 实例这条硬边界完成的:同一个客户账号的函数跑在专属于它的虚拟机上,不同客户之间隔着整台实例。安全上无可挑剔,但粒度太粗。一个客户哪怕只偶尔跑一个 50 毫秒的函数,也得有一台实例为他站着;实例开机以秒计,于是还要预留、预热。密度低 = 大量机器空转 = 成本,而 serverless 卖的恰恰是「按毫秒计费」的便宜。
要提密度,就得让不同客户的负载挤进同一台机器,于是必须回答:拿什么当隔离边界?论文把当时的选项摊开评了一遍:
ioctl、/proc、/sys 这些庞杂入口,历史上反复出过逃逸漏洞。把互不信任的租户压在同一个内核上,风险不可接受。论文据此列出六条硬需求:隔离、开销与密度、性能(接近裸机)、兼容性(任意 Linux 二进制)、快速切换(毫秒级创建销毁)、软分配(允许超卖)。没有现成东西同时满足,于是自己写一个。
全篇最关键的判断是:硬件虚拟化本身并不慢。自从 CPU 支持虚拟化指令、KVM 把这部分搬进内核,客机执行指令与访问内存的额外开销已经很小。真正的慢与重来自 VMM 的另一半职责——伪造一整台 PC:BIOS 自检、PCI 总线枚举、USB、软驱光驱、显卡、即插即用……客机内核开机时要把这一柜子假硬件逐个探测,秒级的开机时间大半花在这里;而这堆模拟代码同时还是 VMM 最大的一块攻击面。
而 serverless 的客机是平台自己提供的一个 Linux,上面跑客户的函数。它不需要装 Windows,不需要显卡,不需要热插拔 USB。兼容性要求从「任意操作系统」降成「任意 Linux 二进制」,减法才有下刀的空间。
于是 Firecracker 的设备模型只剩四件:virtio 网络设备、virtio 块设备、一个串口(吐日志),以及一个功能残缺的键盘控制器——它唯一的用途是让宿主按下「关机」。没有 BIOS、没有引导程序:Firecracker 直接把未压缩的 Linux 内核镜像装进客机内存,按 Linux 的启动约定跳进去执行。开机路径短到只剩「搬内核 + 认四个设备」。
代价也得说清:客机内核必须是为这套精简设备编译的(开着 virtio 驱动、关掉用不上的一大堆),客户想跑需要 GPU 或 PCI 直通的负载,这里没有。这是刻意的特化,不是遗漏。
隔离边界若只有一层,那一层就必须完美。Firecracker 干脆假设它不完美,于是叠了两层。
第一层是把 VMM 本身做小做稳:约 5 万行 Rust——对比 QEMU 的百万行 C,代码量与语言两头同时缩小攻击面(Rust 在编译期就排除了缓冲区溢出、释放后使用这类内存错误,而它们恰恰是 VMM 漏洞的主要来源)。一台 microVM 对应一个独立进程,客机的每个 vCPU 是这个进程里的一个线程;某台被攻破或崩溃,波及范围就是它自己。
第二层是 jailer:启动 Firecracker 之前先把它关进笼子——chroot 换根、namespace 隔开进程与网络视图、cgroup 限住资源、降权到非 root,最后套上一张 seccomp 白名单,只放行跑一台 microVM 真正必需的那几十个系统调用。于是攻击链变成:先要打穿客机内核,再打穿 KVM 这条虚拟机边界,打穿之后落脚的还是一个被提前剥光权限的进程。
诚实之处:KVM 与宿主内核仍在 TCB 里,这条边界不是零信任,只是被压缩到了很窄的一条接口上——KVM 暴露的那组 ioctl,比整个系统调用面小得多。
密度不只是「塞得下」,还得塞得起。函数申请了 2 GB 内存,实际常常只用几十 MB。microVM 的内存来自宿主按需分配的页面:客机没碰过的内存,宿主就不真的拿出来——于是内存与 CPU 可以超卖(论文称 soft allocation)。这一步才是把密度换成钱的地方。
代价是同机的邻居会互抢 IO。Firecracker 因此在块设备与网络设备上内置了令牌桶限速器(按固定速率往桶里投令牌,没令牌就等——既能限住长期平均速率,又容许短时突发),让上层给每台 microVM 配额。
对外则只有一个跑在 Unix socket 上的 REST API:配好内核、磁盘、网卡、vCPU 数,然后一句「启动」。Lambda 的每台 worker 上由一个叫 MicroManager 的组件调这套 API,维护一批预备好的 microVM「槽位」,请求来了就把槽位派给某个函数——同一个槽位只服务同一个客户的同一个函数,从不跨客户复用。
论文给出的三个核心数字:每台 microVM 的 VMM 内存开销小于 5 MiB、从创建到跑上用户代码约 125 ms、单机每秒可创建 150 台 microVM。这正是「隔离 + 密度 + 快速切换」三条需求的量化兑现。
对照实验把 Firecracker 与 QEMU、gVisor 摆在一起,量启动时间、内存开销与 IO 吞吐。启动与内存两项 Firecracker 明显占优——被它削掉的固件与设备模型,正是 QEMU 花时间与内存的地方。IO 上论文相当诚实:块设备与网络吞吐在当时不如 QEMU,精简的 virtio 实现让出了一部分性能,作者把它明确列为已知的待改进项,而非粉饰过去。
不过对一篇工业论文来说,最硬的证据并不是 benchmark:AWS Lambda 已全量运行在 Firecracker 上,Fargate 随后跟进。每天数以万亿计的函数调用,跑的就是这套东西——这比任何实验室数字都更能说明它扛得住。
① 一句话:把虚拟机的设备模型削到只剩 virtio 网卡、块设备、串口和一个关机键,用 5 万行 Rust 重写 VMM,于是虚拟机级别的隔离拿到了容器级别的密度与启动速度。
② 旧世界的痛:Lambda 早期一个客户账号独占一台实例,隔离干净但粒度太粗——密度低、开机以秒计、机器空转,与「按毫秒计费」的生意直接冲突。
③ 被否决的路线:容器(边界是三百多个系统调用,太宽)、语言级沙箱(限定语言,兼容性不够)、用户态内核(兼容与性能双重代价)、通用 VMM(百万行代码、秒级开机、几十 MB 常驻)。
④ 关键判断:慢的不是虚拟化,是设备模型。KVM 把 CPU 与内存虚拟化交给硬件后已经很轻,秒级开机花在 BIOS 自检与 PCI/USB 枚举上;而 serverless 的兼容性要求只是「任意 Linux 二进制」,减法才做得动。
⑤ 机制:不跑 BIOS、直接装入未压缩内核跳进去;设备只剩四件;一台 microVM 一个进程、vCPU 是线程;外面套 jailer(chroot + namespace + cgroup + seccomp 白名单 + 非 root)做第二层边界。
⑥ 运营旋钮:内存按需分页支撑超卖,块设备与网卡内置令牌桶限速治吵闹邻居;控制面是 Unix socket 上的 REST API,上层 MicroManager 管槽位,槽位从不跨客户复用。
⑦ 结果:VMM 开销 < 5 MiB、约 125 ms 到用户代码、单机每秒 150 台;启动与内存胜过 QEMU,IO 当时不如,论文照实写;最硬的证据是 Lambda 与 Fargate 的生产全量运行。
⑧ 影响:拆掉了「安全 vs 快」的假两难,坐实了 serverless 的成本模型,并带出 rust-vmm 这一批「用 Rust 拼 VMM」的生态。
⑨ 局限:没有 GPU / PCI 直通、IO 性能有欠账、冷启动大头在客户运行时(后来靠快照恢复解决)、KVM 仍在 TCB 内、侧信道挡不住、运营参数不公开、也不改变 serverless「把数据运到代码那儿」的结构性缺陷。