土法炼钢兴趣小组的算法知识备份

容器 vs microVM:Firecracker 凭什么 125ms 启动

目录

这个系列到现在已经实现了一个容器运行时。但有一个根本性的问题我们一直回避:namespace 隔离到底有多强?

答案是:不够强

容器共享内核。一个内核漏洞就能从容器逃逸到宿主机。2019 年的 CVE-2019-5736(runc 逃逸),2022 年的 CVE-2022-0185(user namespace 堆溢出),2024 年的多个 eBPF 逃逸漏洞——都证明了这一点。

所以 AWS 做了 Firecracker:一个极简的 microVM,用硬件虚拟化代替 namespace 隔离。每个 Lambda 函数跑在自己的虚拟机里,有自己的内核。125ms 启动,5MB 内存开销。

这篇文章拆解两种隔离模型的本质差异。


一、隔离模型对比

容器 (namespace)             microVM (KVM)
┌─────────────────┐         ┌─────────────────┐
│  Container A    │         │  VM A            │
│  ┌───────────┐  │         │  ┌───────────┐  │
│  │ App       │  │         │  │ App       │  │
│  ├───────────┤  │         │  ├───────────┤  │
│  │ libc      │  │         │  │ libc      │  │
│  └───────────┘  │         │  ├───────────┤  │
│                 │         │  │ Guest     │  │
├─────────────────┤         │  │ Kernel    │  │
│  Host Kernel    │ ← 共享   │  └───────────┘  │
│  (单点故障)     │         ├─────────────────┤
└─────────────────┘         │  KVM/Host Kernel │
                            └─────────────────┘

关键区别:

维度 容器 microVM
隔离机制 namespace + cgroup + seccomp 硬件虚拟化 (VT-x/AMD-V)
内核共享 是(单点故障) 否(每个 VM 独立内核)
攻击面 整个宿主内核 syscall 接口 VMX 指令集 + virtio 设备
启动时间 < 100ms ~125ms (Firecracker)
内存开销 ~几 MB(共享内核) ~5MB + guest kernel
适用场景 可信工作负载、开发环境 多租户、不可信代码

二、KVM:Linux 内核里的 hypervisor

Linux 内核从 2.6.20(2007)开始内置 KVM(Kernel-based Virtual Machine)。KVM 把 Linux 内核变成一个 Type-1 hypervisor。

用 KVM 创建虚拟机只需要几个 ioctl:

#include <linux/kvm.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

int main(void) {
    // 1. 打开 KVM 设备
    int kvm_fd = open("/dev/kvm", O_RDWR);

    // 2. 创建虚拟机
    int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);

    // 3. 分配 guest 内存
    void *mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_ANONYMOUS, -1, 0);

    struct kvm_userspace_memory_region region = {
        .slot = 0,
        .guest_phys_addr = 0x1000,
        .memory_size = 0x1000,
        .userspace_addr = (unsigned long)mem,
    };
    ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region);

    // 4. 创建 vCPU
    int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);

    // 5. 设置寄存器,加载 guest 代码
    struct kvm_regs regs;
    ioctl(vcpu_fd, KVM_GET_REGS, &regs);
    regs.rip = 0x1000;  // guest 入口地址
    regs.rflags = 0x2;
    ioctl(vcpu_fd, KVM_SET_REGS, &regs);

    // 把 guest 代码复制到 guest 内存
    // ...

    // 6. 运行!
    for (;;) {
        ioctl(vcpu_fd, KVM_RUN, NULL);
        // 处理 VM exit
    }
}

这就是一个最简的虚拟机。当然,要跑 Linux 内核需要设置更多东西:GDT、页表、中断控制器……但核心就是这几个 ioctl。


三、Firecracker 的极简设计

传统虚拟机(QEMU)模拟完整 PC:BIOS、ACPI、PCI 总线、IDE/SATA 控制器、VGA 显卡……这些对服务器容器毫无意义,但 QEMU 启动时必须初始化它们。

Firecracker 的做法:全部砍掉

组件 QEMU Firecracker
BIOS/UEFI 无(直接 Linux boot protocol)
ACPI 表
PCI 总线 无(用 virtio-mmio)
显卡 有(VGA)
USB
音频
块设备 IDE/SCSI/virtio-blk-pci virtio-blk-mmio
网络 e1000/virtio-net-pci virtio-net-mmio
串口 16550A 精简版串口

virtio-mmio 是关键:它跳过 PCI 总线枚举,设备直接映射到内存地址。guest kernel 启动时不需要扫描 PCI 总线,直接知道设备在哪。

启动时间拆解

测试环境:以下数据来自 AWS i3.metal 实例(Intel Xeon E5-2686 v4, 64 vCPU, 关闭超线程),Linux 5.10, Firecracker v1.1。不同硬件和内核版本结果会有差异——在 ARM Graviton2 上,guest kernel boot 阶段约快 15%。

Firecracker 的 125ms 启动时间花在哪?

VMM 初始化(创建 KVM VM、设置内存):  ~5ms
加载 guest kernel 到内存:              ~10ms
Guest kernel boot (到 init):           ~80ms
init 进程启动:                         ~30ms
───────────────────────────────────────────
总计:                                  ~125ms

瓶颈在 guest kernel boot。Firecracker 为此做了优化: - 使用精简的内核配置(去掉不需要的驱动) - 支持 kernel boot 参数 reboot=k panic=1(异常直接退出,不挂起) - 共享内存的 balloon driver 按需分配

代码量对比

QEMU Firecracker
语言 C Rust
代码行数 ~200 万 ~5 万
安全漏洞历史 数百个 CVE 个位数

Firecracker 用 Rust 写不是为了性能——它为了安全。更少的代码 + 内存安全语言 = 更小的攻击面。

为什么这对 VMM 特别重要?VMM 直接处理 guest 的 virtio 请求——本质上是在解析不可信输入。QEMU 的 CVE 历史里,大量是设备模拟代码的缓冲区溢出和 use-after-free。Rust 的所有权模型在编译期消除了这两类问题。Firecracker 的 unsafe 代码块被严格限制在 KVM ioctl 调用和 MMIO 地址映射(约占总代码 2%),每个 unsafe 块都有注释说明为什么需要以及不变量是什么。


四、性能对比

启动时间

方案 冷启动时间
Docker (runc) ~300ms(含镜像加载)
Docker (runc, 镜像已缓存) ~100ms
crun ~50ms
Firecracker ~125ms
QEMU (最小配置) ~800ms
Cloud Hypervisor ~100ms

Firecracker 的 125ms 与容器在同一量级。对 Lambda 这样的 serverless 场景,这个启动时间完全可接受。

内存开销

方案 最小内存开销
容器 ~2MB(共享宿主内核)
Firecracker ~5MB VMM + guest kernel 内存
QEMU ~30MB+

Firecracker 的 5MB 开销包括 VMM 进程本身和 virtio 队列。guest kernel 最少需要约 20MB,但可以用 balloon driver 按需回收。

I/O 性能

这是 microVM 相对容器的最大劣势:

容器: app → syscall → host kernel → 设备
VM:   app → syscall → guest kernel → virtio → VM exit → host kernel → 设备

每次 I/O 操作,microVM 多了一次 guest kernel 处理和一次 VM exit。VM exit 的代价是 ~1-5μs,对高频小包场景(比如 Redis)影响显著。

Firecracker 通过 vhost-net 减少 VM exit:网络包直接在内核空间从 host 转发到 guest 的 virtio 队列,跳过 VMM 进程的用户态。


五、安全模型对比

容器的攻击面

容器进程通过系统调用直接与宿主内核交互。即使有 seccomp 过滤(只允许 ~260 个 syscall),每个 syscall 都是潜在的攻击入口。内核的 syscall 处理代码有数百万行。

容器进程 → seccomp filter → 宿主内核 (数百万行代码)
                                    ↓
                              内核漏洞 = 逃逸

microVM 的攻击面

VM 进程与 KVM 交互,KVM 处理的是 VMX 指令集(Intel VT-x),比 syscall 接口小得多。virtio 设备的攻击面也远小于整个 syscall 接口。

Guest 进程 → Guest 内核 → VM exit → KVM (数千行代码)
                                         ↓
                                   KVM 漏洞 = 逃逸

KVM 的代码量约 5 万行,远小于整个内核的 syscall 处理路径。

Kata Containers:两全其美?

Kata Containers 把 OCI 容器接口和 microVM 隔离结合起来:外部看起来是容器(兼容 Kubernetes),内部是 microVM:

kubelet → containerd → kata-runtime → Firecracker/QEMU/Cloud Hypervisor
                                            ↓
                                      每个 Pod 一个 VM

代价是更高的资源开销和更慢的启动时间。但对多租户场景(如公有云),这是值得的。


六、选择指南

场景 推荐方案 原因
开发环境 容器 快、轻、够用
CI/CD 容器 速度优先
单租户生产 容器 + seccomp 性能好,安全足够
多租户 SaaS microVM (Firecracker) 不可信代码需要硬隔离
Serverless microVM AWS Lambda 的选择
边缘计算 容器或 WASM 资源受限

没有银弹。容器和 microVM 是不同的权衡点。

下一篇,我们回到容器的实际性能问题 — 容器网络性能真相

相关阅读


By .