1. 从一台闲置物理机说起:为什么我要折腾 Firecracker 和 KVM
手里有一台闲置的物理服务器,配置不算差,32 核 64G,本来想拿来跑点轻量级的任务。最开始的想法很朴素——装个系统,开几个虚拟机,把资源切一切就完事了。结果真动手才发现,传统虚拟化方案在这类场景下有点"杀鸡用牛刀":启动一个完整虚拟机要等十几秒甚至更久,内存开销也大,每个实例动辄几百 MB 起步。我需要的其实是那种"秒起秒停、密度极高、隔离性又够用"的东西。
这就是我接触Firecracker的起点。它本质上是一个用 Rust 写的microVM(微虚拟机)管理器,最早是为了支撑函数计算这类需要极快冷启动、极高部署密度的场景而设计的。而它底层依赖的核心技术,正是KVM——Linux 内核自带的虚拟化模块。简单说,KVM 负责提供硬件级别的虚拟化能力,Firecracker 则在这个能力之上,把"虚拟机"做得尽可能小、尽可能快。
这篇文章适合谁看?如果你满足下面任意一条,那接下来的内容应该对你有用:想搞清楚 KVM 和 Firecracker 到底是什么关系;准备在自己的服务器上跑 microVM 但不知道从哪下手;听说过virtio但一直没弄明白它在虚拟化里扮演什么角色;或者你只是单纯好奇,为什么现在很多云厂商的底层都在往 microVM 这个方向走。我会从原理讲到实操,把踩过的坑和验证过的参数都摊开来说,尽量让你看完就能自己动手。
需要先说明一点:Firecracker 不是要取代 KVM,它俩是上下层关系。KVM 是内核里的"发动机",Firecracker 是装在这台发动机上的"轻量化车身"。理解了这一层,后面很多设计选择就顺理成章了。
2. KVM 到底做了什么:把内核变成一台虚拟化发动机
2.1 KVM 的本质是一个内核模块
很多人第一次听到 KVM,会以为它是一个独立的软件或者一个完整的虚拟化平台。其实不是。KVM 全称是 Kernel-based Virtual Machine,它最核心的形态就是 Linux 内核里的一个模块。加载之后,内核就多了一项能力:可以创建和运行虚拟机。
它的工作方式是这样的——用户态的程序(比如 QEMU、Firecracker)通过/dev/kvm这个字符设备跟内核打交道,发出"我要创建一台虚拟机""我要给这台虚拟机分配内存""我要让这台虚拟机的 CPU 跑起来"这类指令。KVM 收到指令后,借助 CPU 硬件提供的虚拟化扩展(Intel 的 VT-x 或者 AMD 的 AMD-V)来真正执行。所以 KVM 本身并不模拟硬件,它做的是"把物理 CPU 的虚拟化能力暴露给用户态程序"。
这就解释了一个常见疑问:为什么装了 KVM 之后,还得配一个 QEMU 才能跑虚拟机?因为 KVM 只负责 CPU 和内存的虚拟化,它不管磁盘、网卡、键盘鼠标这些外设。外设的模拟工作,传统上是由 QEMU 来完成的。QEMU 负责"造"出一台完整的机器,KVM 负责让这台机器的 CPU 跑得接近原生速度。两者配合,才是一套完整的方案。
2.2 硬件虚拟化扩展是性能的分水岭
KVM 的性能优势,根源在于硬件虚拟化扩展。在没有这些扩展的年代,虚拟化靠的是"二进制翻译"——把虚拟机里的指令一条条翻译成宿主机能执行的指令,开销很大。有了 VT-x / AMD-V 之后,CPU 本身就支持"进入虚拟机模式"和"退出虚拟机模式",虚拟机里的大部分指令可以直接在物理 CPU 上跑,只有遇到敏感操作时才"退出"到宿主机由 KVM 处理。
这个"退出"(VM Exit)是理解虚拟化性能的关键。每一次 VM Exit 都意味着上下文切换,是有成本的。所以虚拟化优化的核心思路之一,就是尽量减少 VM Exit 的次数。后面讲 virtio 的时候你会看到,virtio 的设计目标之一正是这个。
你可以用一条命令确认自己的机器是否支持硬件虚拟化:
grep -E -c '(vmx|svm)' /proc/cpuinfo返回值大于 0 就说明支持(vmx 是 Intel,svm 是 AMD)。如果返回 0,那要么是 CPU 不支持,要么是 BIOS 里没开启虚拟化选项。我遇到过好几次"明明 CPU 支持却跑不起来"的情况,最后都是 BIOS 里那个开关没打开。
2.3 KVM 的能力边界在哪里
KVM 很强,但它不是万能的。它有几个明确的边界,理解这些边界才能理解为什么需要 Firecracker 这样的东西。
第一,KVM 本身不提供设备模型。你光有 KVM,虚拟机是没有磁盘、没有网卡的,这些都得靠用户态程序补上。第二,KVM 不负责虚拟机的生命周期管理,创建、启动、暂停、销毁这些编排逻辑都在用户态。第三,KVM 的接口是相对底层的 ioctl 调用,直接拿它写程序门槛不低。
正因为这些边界,用户态才出现了各种"VMM"(Virtual Machine Monitor,虚拟机监视器)。QEMU 是功能最全的那个,几乎能模拟任何设备;Firecracker 则是另一个极端——只保留最必要的功能,把体积和启动时间压到极致。它们都站在 KVM 的肩膀上,只是取舍不同。
3. Firecracker 的取舍哲学:为什么它能把启动时间压到毫秒级
3.1 从"功能齐全"到"只留必要"
传统 QEMU 的设计目标是"什么都能模拟"——从古老的 IDE 硬盘到各种奇葩的 PCI 设备,它都能给你造出来。这种通用性带来了巨大的代码量和复杂的设备初始化流程,代价就是启动慢、内存占用高。一台完整虚拟机启动动辄十几秒,内存开销轻松上到几百 MB。
Firecracker 的思路完全反过来。它问了一个问题:如果我只服务一类场景——比如跑无状态的函数、跑容器沙箱——那我到底需要模拟哪些设备?答案是少得可怜:一个块设备(用来放 rootfs)、一个网络设备、一个串口(用来输出日志)、一个时钟。其他的统统不要。
这个取舍带来的效果是惊人的。Firecracker 的代码量只有几万行(相比 QEMU 的百万行级别),编译出来的二进制很小,启动一台 microVM 的时间可以做到 125 毫秒以内,内存开销可以压到 5MB 以下。这意味着同一台物理机上可以塞进成百上千个 microVM,而且每个都能在眨眼间启动。
3.2 microVM 不是"小虚拟机"那么简单
"microVM"这个词容易让人误解,以为只是把普通虚拟机缩小了。其实它的设计理念有本质区别。
普通虚拟机的假设是"长期运行、功能完整",所以启动慢一点、占内存多一点可以接受。microVM 的假设是"生命周期极短、数量极多",所以每一毫秒的启动时间、每一 MB 的内存都要抠。这种假设直接影响了架构决策:Firecracker 去掉了 BIOS/UEFI 的完整启动流程,直接用 Linux 内核的PVH或ELF引导方式;去掉了 PCI 总线的复杂枚举,改用 MMIO(内存映射 I/O)来暴露设备;甚至连设备的数量都写死在代码里,不做动态发现。
我实测过,用 Firecracker 启动一个最小化的 Linux microVM,从发出启动命令到串口打印出登录提示,稳定在 150 毫秒左右。同样的镜像用 QEMU 启动,要 3 到 5 秒。这个差距在需要频繁创建销毁实例的场景里,是数量级的区别。
3.3 安全隔离:microVM 相比容器的优势
有人会问:既然要轻量,为什么不直接用容器?容器启动也是毫秒级,密度也高。答案在隔离性。
容器共享宿主机的内核,一个容器里的内核漏洞可能影响到其他容器甚至宿主机。microVM 则不同,每个 microVM 有自己独立的内核,通过 KVM 的硬件虚拟化边界与宿主机隔离。这个隔离强度接近传统虚拟机,但启动速度和资源开销又接近容器。这就是 microVM 的独特定位——它试图同时拿到"容器的轻"和"虚拟机的隔离"。
Firecracker 在这方面还做了额外的加固:它用一个极简的jailer进程把 microVM 关进命名空间和 cgroup 里,限制它能看到的文件系统、能用的系统调用、能占的资源。即使 microVM 被攻破,攻击者也被困在一个层层设限的笼子里。这套组合拳是它在多租户场景下被广泛采用的重要原因。
4. virtio:让 microVM 的 I/O 不再成为瓶颈
4.1 全虚拟化设备的性能陷阱
如果 Firecracker 老老实实模拟一块真实的网卡,会怎样?虚拟机会以为自己插了一块真网卡,每次发数据都要读写网卡的寄存器。这些读写操作会触发 VM Exit,陷入到宿主机由 Firecracker 处理,处理完再返回虚拟机。一次网络收发可能要触发几十次 VM Exit,性能惨不忍睹。
这就是"全虚拟化"设备的通病——因为要模拟真实硬件的寄存器行为,导致大量陷入和模拟开销。解决办法是让虚拟机"知道"自己在虚拟化环境里,用一种双方约定好的、更高效的方式通信。这就是virtio的由来。
4.2 virtio 的核心:共享内存加通知机制
virtio 的本质是一套标准化的"半虚拟化"设备接口。它定义了几种虚拟设备(网卡、块设备、控制台等)的通信规范,核心机制是virtqueue(虚拟队列)。
virtqueue 是一块宿主机和虚拟机都能访问的共享内存区域,里面放着一圈"描述符",每个描述符指向一块数据缓冲区。虚拟机要发数据时,把数据放进缓冲区,在 virtqueue 里填一个描述符,然后"通知"宿主机。宿主机处理完后,再"通知"虚拟机。整个过程不需要模拟任何硬件寄存器,只需要读写共享内存和发送轻量级通知。
这个设计把原来几十次 VM Exit 压缩到一两次,性能提升是数量级的。而且因为接口是标准化的,虚拟机里用通用的 virtio 驱动就行,不需要为每种虚拟化平台写专门的驱动。
4.3 Firecracker 对 virtio 的裁剪
Firecracker 用了 virtio,但不是全盘照搬。它只实现了三类 virtio 设备:virtio-net(网络)、virtio-block(块存储)、virtio-vsock(主机与虚拟机间的套接字通信)。而且每个设备只支持一个队列,去掉了多队列、去掉了复杂的特性协商。
这种裁剪是有意为之。多队列能提升吞吐,但增加了复杂度和启动开销;Firecracker 的目标场景是大量小实例,单队列足够用。特性协商能兼容更多设备,但 Firecracker 只跟自己配合,不需要那么灵活。这种"够用就好"的思路贯穿整个项目,也是它能做到极简的原因。
有一点要提醒:因为 Firecracker 的 virtio 实现是裁剪过的,某些依赖特定 virtio 特性的驱动或配置可能不工作。我在配virtio-net的时候,一开始想开多队列提升性能,结果发现根本不支持,只能退回单队列。这不是 bug,是设计选择。
5. 动手搭一套:从零跑起第一个 microVM
5.1 环境检查与依赖准备
动手之前,先把地基确认好。第一步是确认 KVM 可用:
ls -l /dev/kvm如果这个设备文件存在且你有读写权限,说明 KVM 就绪。如果不存在,检查 CPU 虚拟化是否开启、内核模块是否加载:
lsmod | grep kvm正常情况下应该能看到kvm和kvm_intel(或kvm_amd)两个模块。没有的话手动加载:
sudo modprobe kvm sudo modprobe kvm_intel # Intel 平台第二步是准备 Firecracker 二进制。从官方发布页下载对应架构的版本,解压后放到/usr/local/bin或者你习惯的目录。我建议同时下载一份对应版本的jailer,后面做隔离会用到。
第三步是准备内核和 rootfs。Firecracker 需要一个未压缩的 Linux 内核镜像(vmlinux格式,不是bzImage)和一个 ext4 格式的 rootfs。官方提供了一套演示用的镜像,拿来练手最省事。自己编译内核的话,记得开启CONFIG_VIRTIO_MMIO和对应的 virtio 驱动,否则 microVM 起来后看不到任何设备。
5.2 配置文件的字段逐个拆解
Firecracker 通过一个 JSON 配置文件来描述 microVM 的形态。下面是一份最小可用的配置,我逐字段说明:
{ "boot-source": { "kernel_image_path": "/path/to/vmlinux", "boot_args": "console=ttyS0 reboot=k panic=1 pci=off" }, "drives": [ { "drive_id": "rootfs", "path_on_host": "/path/to/rootfs.ext4", "is_root_device": true, "is_read_only": false } ], "machine-config": { "vcpu_count": 2, "mem_size_mib": 512 }, "network-interfaces": [ { "iface_id": "eth0", "host_dev_name": "tap0", "guest_mac": "AA:FC:00:00:00:01" } ] }boot_args里的pci=off很关键,它告诉内核不要去枚举 PCI 总线,因为 Firecracker 用的是 MMIO 设备,没有 PCI。console=ttyS0让内核日志输出到串口,方便你观察启动过程。panic=1表示内核 panic 后 1 秒重启,调试时有用。
machine-config里的vcpu_count和mem_size_mib决定了 microVM 的规格。这里有个经验:microVM 的内存不要设太小,512MB 是比较稳妥的起点。我试过设 128MB,结果某些 rootfs 加载驱动时就 OOM 了。CPU 数量按实际负载来,跑轻量任务 1 到 2 个 vCPU 足够。
网络部分需要宿主机先创建一个 tap 设备:
sudo ip tuntap add dev tap0 mode tap sudo ip addr add 172.16.0.1/24 dev tap0 sudo ip link set tap0 up然后在宿主机开启转发和 NAT,microVM 才能访问外网。这一步容易漏,漏了的话 microVM 能起来但上不了网。
5.3 启动、连接与验证
配置写好之后,启动 Firecracker:
firecracker --api-sock /tmp/firecracker.sock --config-file vm_config.json如果一切正常,你会看到内核启动日志从串口刷出来,最后停在登录提示。这时候可以用screen或socat连上串口交互:
sudo socat -,raw,echo=0 unix-connect:/tmp/firecracker.sock不过更常见的做法是通过 Firecracker 的 API 来管理。它启动后会监听一个 Unix socket,你可以用 HTTP 请求动态配置和启动 microVM。这种方式更适合自动化——先启动一个空的 Firecracker 进程,然后通过 API 依次下发配置、启动实例、查询状态。很多上层编排工具就是这么跟它交互的。
验证 microVM 是否正常工作,我一般看三件事:串口有没有正常输出、网络能不能通、磁盘能不能读写。三个都过了,这套环境就算搭稳了。
6. 那些文档里不会写的坑
6.1 内核格式不对导致启动直接失败
这是新手最容易踩的坑。Firecracker 要的是vmlinux(ELF 格式的未压缩内核),而很多发行版默认给你的是bzImage(压缩过的引导镜像)。拿bzImage去启动,Firecracker 会直接报错退出,而且错误信息不一定直观。
解决办法是从内核源码编译时选vmlinux,或者用工具把bzImage解压出来。我一般直接编译,虽然慢一点但最省心。编译时记得把 virtio 相关的驱动编进内核(不是编成模块),因为 microVM 启动早期就需要这些驱动来挂载 rootfs。
6.2 rootfs 权限和格式的隐形要求
rootfs 必须是 ext4 格式,而且 Firecracker 进程要有读写这个文件的权限。我遇到过权限没问题但就是挂载失败的情况,排查半天发现是 rootfs 镜像本身有问题——用dd创建的空文件没有真正格式化。正确的做法是:
dd if=/dev/zero of=rootfs.ext4 bs=1M count=512 mkfs.ext4 rootfs.ext4然后用 loop 设备挂载上去,把系统文件拷进去。这一步如果偷懒用现成的镜像,要注意镜像里的/etc/fstab和 init 配置是否适配 microVM 环境,很多通用镜像会在这里出问题。
6.3 网络不通的排查顺序
microVM 网络不通,按这个顺序查基本能定位:先看宿主机 tap 设备是否 up、IP 是否配好;再看宿主机ip_forward是否开启;然后看 NAT 规则(iptables 或 nftables)是否正确;最后进 microVM 看网卡是否拿到 IP、路由是否正确。
我踩过最隐蔽的一个坑是guest_mac和宿主机上其他设备的 MAC 冲突,导致 ARP 表混乱。给每个 microVM 分配唯一 MAC 是个好习惯,别图省事用默认值。
6.4 资源限制没设好导致宿主机被拖垮
microVM 虽然轻,但数量多了照样能把宿主机吃干。一定要用 cgroup 限制每个 microVM 的 CPU 和内存上限,用jailer把它关进独立的命名空间。我见过没做限制的环境,某个 microVM 内存泄漏,把整台宿主机拖到 OOM。Firecracker 本身提供了 API 来设置这些限制,配合jailer使用效果最好。
7. 这套组合适合什么、不适合什么
Firecracker 加 KVM 这套方案,优势场景很明确:需要大量短生命周期实例、对启动速度和密度要求极高、同时又要比容器更强隔离性的场合。函数计算平台、CI/CD 的构建沙箱、多租户的代码执行环境,都是它的主场。
但它不适合所有场景。如果你需要模拟复杂的硬件、需要 GPU 直通、需要运行对设备有特殊要求的负载,那还是得回到 QEMU 这类功能完整的方案。Firecracker 的极简是优点也是限制,选它之前先想清楚自己的需求边界。
我个人在实际使用中的体会是:microVM 这套东西的价值不在于单台性能有多强,而在于它改变了"实例"的成本结构。当启动一台虚拟机的成本低到可以忽略时,很多以前不敢想的设计就变得可行了——比如为每个请求起一个独立隔离环境,用完即销毁。这种思路上的解放,比单纯的性能数字更有意思。