ᕕ( ᐛ )ᕗ Jimyag's Blog

Zeroboot 启动优化分析:亚毫秒级 VM 沙箱实现

· 919 字 · 约 5 分钟

概述

Zeroboot 是一个基于 KVM 的快速 VM 沙箱系统,其核心创新是使用 Copy-on-Write (CoW) 内存映射实现亚毫秒级的 VM 启动。本文档分析其优化原理和实现细节。

性能对比

指标 Zeroboot E2B 传统 VM
启动延迟 p50 0.79ms ~150ms ~1-3s
启动延迟 p99 1.74ms ~300ms ~5-10s
内存占用/沙箱 ~265KB ~128MB ~256MB+
隔离级别 硬件(KVM) 容器 硬件
Fork + exec (Python) ~8ms - -
1000 并发 fork 815ms - -

差距分析:Zeroboot 比 E2B 快约 200 倍,内存占用减少约 500 倍


核心优化原理

1. Copy-on-Write (CoW) 内存映射

传统方式:每次启动 VM 都需要分配完整内存(如 256MB),并复制模板内容。

Zeroboot 方式

1
2
3
模板内存 (memfd) ──► mmap(MAP_PRIVATE) ──► Fork A (CoW)
                 ──► mmap(MAP_PRIVATE) ──► Fork B (CoW)
                 ──► mmap(MAP_PRIVATE) ──► Fork C (CoW)

关键代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let fork_mem = unsafe {
    libc::mmap(
        ptr::null_mut(),
        snapshot.mem_size,
        libc::PROT_READ | libc::PROT_WRITE,
        libc::MAP_PRIVATE | libc::MAP_NORESERVE,  // CoW 映射
        memfd,
        0,
    )
};

效果

  • 初始状态:所有 fork 共享相同的物理内存页
  • 写入时:触发页面错误,内核只复制被修改的页面
  • 实际内存占用:仅 ~265KB(被修改的页面)

2. 快照恢复 vs 完整启动

传统 VM 启动流程

1
2
BIOS  Bootloader  Kernel  Init  Runtime  应用
( 1-3 )

Zeroboot 快照恢复

1
2
恢复 CPU 状态 → 恢复内存映射 → 运行
(约 0.8ms)

CPU 状态恢复顺序(必须严格遵循):

1
2
3
4
5
6
7
1. sregs    (设置 CR4.OSXSAVE)
2. XCRS     (扩展控制寄存器)
3. XSAVE    (FPU/SSE/AVX 状态)
4. regs     (通用寄存器)
5. LAPIC    (本地 APIC)
6. MSRs     (模型特定寄存器)
7. MP_STATE (设为 RUNNABLE)

3. 直接 KVM 操作

传统方式

1
2
用户请求 → Firecracker API → Firecracker 进程 → KVM
         (进程间通信开销)

Zeroboot 方式

1
2
用户请求 → 直接 KVM ioctl
         (零进程间通信)

关键 KVM 操作:

1
2
3
4
5
6
7
8
let kvm = Kvm::new()?;                    // 打开 /dev/kvm
let vm_fd = kvm.create_vm()?;             // KVM_CREATE_VM
vm_fd.create_irq_chip()?;                 // KVM_CREATE_IRQCHIP
vm_fd.create_pit2(kvm_pit_config::default())?;  // KVM_CREATE_PIT2
let vcpu_fd = vm_fd.create_vcpu(0)?;      // KVM_CREATE_VCPU
vcpu_fd.set_sregs(&snapshot.sregs)?;      // KVM_SET_SREGS
// ... 更多状态恢复
vcpu_fd.run()?;                          // KVM_RUN

4. 简化的 I/O 通信

传统方式 (virtio)

1
2
Guest → virtqueue → ioeventfd → worker 线程 → Unix socket → Client
        (复杂的异步处理)

Zeroboot 方式 (串口)

1
2
Guest → 16550 UART → VcpuExit::IoOut → 直接处理 → Client
        (同步简单循环)

串口处理代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
loop {
    match self.vcpu_fd.run()? {
        VcpuExit::IoOut(port, data) => {
            if port >= COM1_PORT && port <= COM1_PORT_END {
                self.serial.write(port - COM1_PORT, data[0]);
            }
        }
        VcpuExit::IoIn(port, data) => {
            if port >= COM1_PORT && port <= COM1_PORT_END {
                data[0] = self.serial.read(port - COM1_PORT);
            }
        }
        VcpuExit::Hlt => { /* yield */ }
        _ => {}
    }
}

对比

特性 virtio 串口
复杂度 高(virtqueue, ioeventfd, irqfd, 多线程) 低(同步循环)
性能 高(零拷贝) 中(端口 I/O)
可靠性 低(状态复杂) 高(简单直接)
启动开销 大(需要初始化) 小(立即可用)

完整工作流程

Phase 1: 模板创建(一次性,~15秒)

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────────────────────────┐
│  1. Firecracker 启动 VM                                      │
│  2. 内核启动,init.py 运行                                    │
│  3. 预加载运行时 (Python + numpy + pandas 等)                 │
│  4. 创建快照:                                                │
│     - 内存转储 → snapshot/mem (256MB)                        │
│     - CPU 状态 → snapshot/vmstate                           │
└─────────────────────────────────────────────────────────────┘

Phase 2: Fork(每次请求,~0.8ms)

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────────────────────────┐
│  1. KVM_CREATE_VM + KVM_CREATE_IRQCHIP + KVM_CREATE_PIT2    │
│  2. 恢复 IOAPIC 中断重定向表                                  │
│  3. mmap(MAP_PRIVATE) 快照内存                               │
│  4. 恢复 CPU 状态(按顺序)                                   │
│  5. 设置 MP_STATE = RUNNABLE                                 │
│  6. 开始执行                                                  │
└─────────────────────────────────────────────────────────────┘

Phase 3: 执行与通信

1
2
3
4
5
6
┌─────────────────────────────────────────────────────────────┐
│  Client 请求 ──► 串口输入 ──► Guest 执行 ──► 串口输出 ──► 响应  │
│                                                              │
│  Guest 通过 /dev/ttyS0 与 host 通信                           │
│  Host 在 KVM 运行循环中处理端口 I/O                           │
└─────────────────────────────────────────────────────────────┘

关键实现细节

1. memfd 共享内存

模板内存存储在 memfd 中,支持多进程共享:

1
2
3
4
5
6
pub fn create_snapshot_memfd(mem_ptr: *const u8, mem_size: usize) -> Result<i32> {
    let name = std::ffi::CString::new("zeroboot-snapshot").unwrap();
    let fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) };
    // ... ftruncate, mmap, copy ...
    Ok(fd)
}

2. IOAPIC 恢复模式

关键点:不能零初始化 kvm_irqchip,必须先 GET 再修改:

1
2
3
4
5
6
7
8
let mut irqchip = kvm_irqchip::default();
irqchip.chip_id = KVM_IRQCHIP_IOAPIC;
vm_fd.get_irqchip(&mut irqchip)?;  // 先获取当前状态
// 只修改重定向表
for i in 0..24 {
    irqchip.chip.ioapic.redirtbl[i].bits = snapshot.ioapic_redirtbl[i];
}
vm_fd.set_irqchip(&irqchip)?;

3. vmstate 解析

Firecracker 的 vmstate 是 versionize 二进制格式,偏移量随版本变化:

1
2
3
// 使用 IOAPIC 基地址作为锚点自动检测偏移
const IOAPIC_BASE_PATTERN: [u8; 8] = 0xFEC00000u64.to_le_bytes();
// 在 vmstate 中搜索锚点,推导其他字段位置

E2B vs Zeroboot:架构对比

关键认知:E2B 和 Zeroboot 都使用 Firecracker/KVM 提供硬件级隔离,但启动方式完全不同。

E2B 的启动流程

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────────────────────────┐
│  1. Client 请求 → API Gateway                                │
│  2. API → Orchestrator (gRPC)                               │
│  3. Orchestrator 启动新 Firecracker 进程                    │
│  4. Firecracker 从快照恢复 VM                               │
│  5. Envd (in-VM daemon) 准备就绪                            │
│  6. 通过 gRPC 与 Envd 通信执行代码                          │
└─────────────────────────────────────────────────────────────┘

关键开销

  • Firecracker 进程启动 (~50-80ms)
  • Unix Socket API 通信延迟
  • UFFD (Userfaultfd) 页面处理
  • Envd gRPC 通信
  • 网络配置、cgroup 设置

Zeroboot 的启动流程

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────────────────────────┐
│  1. Client 请求                                             │
│  2. 在进程中直接 fork VM                                   │
│     - KVM_CREATE_VM + irqchip                             │
│     - mmap(MAP_PRIVATE) CoW 内存映射                       │
│     - 恢复 CPU 状态(寄存器、MSRs 等)                      │
│  3. 开始执行,通过串口 I/O 通信                            │
└─────────────────────────────────────────────────────────────┘

零额外开销

  • 无需启动新进程(进程内 fork)
  • 无需 API 通信(直接 KVM ioctl)
  • 无需网络配置(仅串口 I/O)
  • 无需额外 daemon(简单事件循环)

技术差异总结

方面 E2B Zeroboot
VM 引擎 Firecracker 直接 KVM
启动方式 新 Firecracker 进程 进程内 fork
快照加载 UFFD + API CoW mmap
通信方式 gRPC (Envd) 串口 I/O
额外进程 Firecracker + Envd
网络配置 完整网络栈 无(串口)
隔离级别 KVM VM KVM VM
启动延迟 ~150ms 0.79ms

性能差异来源

  1. 进程启动:E2B 需要 fork + exec Firecracker 进程(~50ms)
  2. API 通信:E2B 通过 Unix Socket 与 Firecracker API 通信
  3. UFFD 开销:E2B 使用 Userfaultfd 处理页面错误
  4. 网络配置:E2B 需要配置网络命名空间、tap 设备等
  5. Envd 通信:E2B 通过 gRPC 与 VM 内的 Envd 通信

Zeroboot 直接在进程中操作 KVM,消除了所有中间层开销。


适用场景分析

Zeroboot 适合

  • ✅ 高并发、短生命周期的沙箱
  • ✅ 需要强隔离的多租户场景
  • ✅ AI Agent 代码执行
  • ✅ 函数即服务 (FaaS)

Zeroboot 限制

  • ❌ 单 vCPU(当前实现)
  • ❌ 无网络(仅串口通信)
  • ❌ 需要预创建模板
  • ❌ 随机数状态共享问题
  • ❌ 模板更新需重建

总结

Zeroboot 通过以下技术实现亚毫秒级 VM 启动:

  1. CoW 内存映射:避免内存复制,共享物理页
  2. 快照恢复:跳过完整启动流程,直接恢复状态
  3. 直接 KVM 操作:消除进程间通信开销
  4. 简化 I/O:使用串口替代复杂的 virtio

这些优化使得 Zeroboot 在保持硬件级隔离安全性的同时,达到了接近进程 fork 的启动速度。

#Kvm #Vm #Sandbox #Performance #Cow #Snapshot #Optimization