在 EC2 嵌套虚拟化上部署 Zeroboot:亚毫秒级 VM Sandbox Benchmark 实测¶
Lab 信息
- 难度: ⭐⭐⭐ 高级
- 预估时间: 60 分钟
- 预估费用: ~$0.19(c8i.xlarge × 1hr)
- Region: ap-southeast-1
- 最后验证: 2026-03-23
背景¶
AI Agent 为什么需要快速 VM Sandbox¶
AI Agent 需要频繁执行用户生成的代码。安全性要求每次执行都在隔离环境中进行,但传统方案启动太慢:
| 方案 | 启动延迟 (p50) | 每 Sandbox 内存 | 隔离级别 |
|---|---|---|---|
| E2B | ~150ms | ~128MB | microVM |
| microsandbox | ~200ms | ~50MB | microVM |
| Daytona | ~27ms | ~50MB | Container |
| zeroboot | 0.79ms | ~265KB | KVM VM |
当 Agent 需要在一次对话中并行执行数十个代码片段时,150ms 的启动延迟会严重拖慢响应速度。zeroboot 把这个数字压到了亚毫秒级。
zeroboot 是什么¶
zeroboot 是一个基于 Firecracker 快照的 VM fork 引擎。核心思路:
- 一次性创建 Template:启动 Firecracker VM → 预加载 Python/numpy 等依赖 → 拍快照(内存镜像 + CPU 状态)
- 每次请求 fork:用
mmap(MAP_PRIVATE)对快照内存做 Copy-on-Write 映射 → 创建新 KVM VM → 恢复 CPU 状态 → 从快照断点处继续执行
关键在于 MAP_PRIVATE | MAP_NORESERVE:256MB 的 guest 内存映射瞬间完成(< 1µs),实际物理内存只在 guest 写入时按需分配(CoW page fault),所以每个 sandbox 的 RSS 只有 ~265KB。
为什么在 EC2 嵌套虚拟化上跑¶
zeroboot 需要 KVM(/dev/kvm),传统做法是租用 bare metal 实例。但成本差距巨大:
| 实例类型 | 规格 | 按需价格 | 适用场景 |
|---|---|---|---|
| c7i.metal-24xl | 96 vCPU, 192GB | $4.608/hr | 生产环境 |
| c8i.xlarge | 4 vCPU, 8GB | $0.192/hr | 开发测试/Benchmark |
成本差 24 倍。EC2 从 2024 年开始支持嵌套虚拟化(Intel 实例),让我们可以在普通虚拟实例上运行 KVM。本文验证:嵌套虚拟化下 zeroboot 的性能是否依然成立。
前置条件¶
- AWS 账号(需要 EC2 启动权限)
- AWS CLI v2 已配置
- SSH 客户端
- 基本 Linux 和虚拟化概念
核心概念¶
zeroboot 工作原理¶
┌─────────────────────────────────────────────────────────────┐
│ Template 创建(一次性,~15s) │
│ │
│ Firecracker VM 启动 → Python import numpy → 拍快照 │
│ ↓ ↓ │
│ snapshot/mem (256MB) snapshot/vmstate (CPU 状态) │
│ ↓ │
│ 载入 memfd(匿名内存文件,CoW 源) │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Fork A │ │ Fork B │ │ Fork C │
│ (~0.8ms) │ │ (~0.8ms) │ │ (~0.8ms) │
│ │ │ │ │ │
│ mmap CoW │ │ mmap CoW │ │ mmap CoW │
│ 共享快照内存 │ │ 共享快照内存 │ │ 共享快照内存 │
│ 写时复制隔离 │ │ 写时复制隔离 │ │ 写时复制隔离 │
│ ~265KB RSS │ │ ~265KB RSS │ │ ~265KB RSS │
└──────────────┘ └──────────────┘ └──────────────┘
各 fork 之间内存完全隔离(KVM 硬件级)
每次 fork 的详细流程(耗时 ~0.8ms):
KVM_CREATE_VM— 创建新虚拟机(~2µs)- 恢复 IOAPIC redirect table — 中断路由
mmap(MAP_PRIVATE, MAP_NORESERVE, fd=memfd)— CoW 内存映射(< 1µs)set_user_memory_region— 注册为 guest 物理内存- 恢复 CPU 状态(严格顺序):
CPUID → sregs → XCRS → XSAVE → regs → LAPIC → MSRs → MP_STATE - vCPU 从快照断点处恢复执行
嵌套虚拟化架构¶
在 EC2 上运行 zeroboot 涉及多层虚拟化:
L0: AWS Nitro Hypervisor(硬件层)
└─ L1: EC2 实例(c8i.xlarge, NestedVirtualization=enabled)
└─ L2: KVM / Firecracker(Template VM + fork VMs)
└─ L3: zeroboot fork(guest 执行用户代码)
嵌套虚拟化带来的挑战:L1 的 KVM 对 L2 VM 的 CPUID 和扩展寄存器(XCR0)有限制,这是本文踩坑的主要来源。
动手实践¶
Step 1: 启动 EC2 嵌套虚拟化实例¶
首先创建 Key Pair 和安全组:
# 创建 Key Pair
aws ec2 create-key-pair \
--key-name zeroboot-benchmark-key \
--key-type ed25519 \
--query 'KeyMaterial' \
--output text \
--region ap-southeast-1 > /tmp/zeroboot-benchmark-key.pem
chmod 400 /tmp/zeroboot-benchmark-key.pem
# 创建安全组(允许 SSH)
SG_ID=$(aws ec2 create-security-group \
--group-name zeroboot-benchmark-sg \
--description "Zeroboot benchmark - SSH only" \
--region ap-southeast-1 \
--query 'GroupId' --output text)
aws ec2 authorize-security-group-ingress \
--group-id $SG_ID \
--protocol tcp --port 22 \
--cidr 0.0.0.0/0 \
--region ap-southeast-1
启动实例,关键参数是 --cpu-options NestedVirtualization=enabled:
INSTANCE_ID=$(aws ec2 run-instances \
--image-id ami-0659642169bf1b4b2 \
--instance-type c8i.xlarge \
--key-name zeroboot-benchmark-key \
--security-group-ids $SG_ID \
--cpu-options NestedVirtualization=enabled \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":30,"VolumeType":"gp3"}}]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=zeroboot-benchmark}]' \
--region ap-southeast-1 \
--query 'Instances[0].InstanceId' --output text)
echo "Instance ID: $INSTANCE_ID"
# 等待实例运行
aws ec2 wait instance-running --instance-ids $INSTANCE_ID --region ap-southeast-1
# 获取 Public IP
PUBLIC_IP=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--region ap-southeast-1 \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
echo "Public IP: $PUBLIC_IP"
常见错误:忘记启用嵌套虚拟化
如果没有加 --cpu-options NestedVirtualization=enabled,/dev/kvm 不会出现。这个参数只能在启动时设置,无法后续修改——只能 terminate 重新启动。
SSH 连接并确认 KVM 可用:
ssh -i /tmp/zeroboot-benchmark-key.pem ubuntu@$PUBLIC_IP
# 确认 /dev/kvm 存在
ls -la /dev/kvm
# crw-rw---- 1 root kvm 10, 232 Mar 23 ... /dev/kvm
Step 2: 安装依赖¶
# 更新系统并安装构建工具
sudo apt update && sudo apt install -y \
build-essential \
git \
pkg-config \
libssl-dev \
curl
# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
# 确认版本
rustc --version
# rustc 1.94.0
# 下载 Firecracker v1.12.0(用于 Template 创建)
ARCH=$(uname -m)
curl -L -o firecracker-v1.12.0-${ARCH}.tgz \
https://github.com/firecracker-microvm/firecracker/releases/download/v1.12.0/firecracker-v1.12.0-${ARCH}.tgz
tar -xzf firecracker-v1.12.0-${ARCH}.tgz
sudo mv release-v1.12.0-${ARCH}/firecracker-v1.12.0-${ARCH} /usr/local/bin/firecracker
firecracker --version
# Firecracker v1.12.0
Step 3: 构建 zeroboot¶
# 克隆仓库
git clone https://github.com/zerobootdev/zeroboot
cd zeroboot
# 编译(Release 模式,~24 秒)
cargo build --release
准备 kernel 和 rootfs(这一步容易出错,详细记录):
# 下载 Firecracker CI 提供的 kernel
# vmlinux-6.1.155 (43MB, 从 Firecracker CI v1.15 artifacts 获取)
curl -L -o vmlinux-6.1.155 \
https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/x86_64/vmlinux-6.1.155
# 创建 rootfs(Ubuntu 24.04 base + Python 3.12)
# 方法:从 Ubuntu cloud image 创建 ext4 rootfs
truncate -s 2G rootfs.ext4
mkfs.ext4 rootfs.ext4
sudo mkdir -p /mnt/rootfs
sudo mount rootfs.ext4 /mnt/rootfs
# 使用 debootstrap 安装最小 Ubuntu 系统
sudo apt install -y debootstrap
sudo debootstrap --arch=amd64 noble /mnt/rootfs http://archive.ubuntu.com/ubuntu
# 安装 Python 和常用库
sudo chroot /mnt/rootfs bash -c "
apt update && apt install -y python3 python3-pip python3-numpy
"
# 编译并安装 guest agent(zeroboot 的 init 进程)
gcc -static -o /tmp/init guest/init.c
sudo cp /tmp/init /mnt/rootfs/init.py
sudo umount /mnt/rootfs
关于 rootfs
zeroboot 的 guest agent 是一个 C 语言静态编译的 PID 1 进程,监听 /dev/ttyS0(serial port)接收代码并执行。它被放在 rootfs 的 /init.py 路径(由 kernel boot_args 中的 init= 参数指定)。
Step 4: 创建 Template¶
# 创建工作目录
mkdir -p workdir
# 创建 Template(启动 Firecracker VM → 预加载 → 拍快照)
# ⚠️ 关键:boot_args 中必须添加 clearcpuid 禁用 AMX
sudo ./target/release/zeroboot template \
--kernel vmlinux-6.1.155 \
--rootfs rootfs.ext4 \
--workdir ./workdir \
--boot-args "console=ttyS0 reboot=k panic=1 pci=off clearcpuid=amx_tile,amx_bf16,amx_int8"
输出示例:
[INFO] Starting Firecracker VM for template...
[INFO] Waiting for guest to initialize (10s)...
[INFO] Taking snapshot...
[INFO] Template created in 13.89s (boot: 8.90s + snapshot)
[INFO] Snapshot state: 22843 bytes
[INFO] Snapshot memory: 512 MiB
[INFO] Loading memory into memfd...
[INFO] Template ready at ./workdir
嵌套虚拟化关键修复:禁用 AMX
在 EC2 嵌套虚拟化环境下,必须在 boot_args 中添加 clearcpuid=amx_tile,amx_bf16,amx_int8。原因:Firecracker 快照会捕获 AMX 状态(XCR0=0x602e7),但 L1 KVM 的 KVM_SET_CPUID2 不允许为 L2 VM 设置含 AMX 的 CPUID。禁用后 XCR0=0x2e7(仍含 AVX-512),与嵌套 KVM 兼容。详见下方踩坑记录。
Step 5: 运行 Benchmark¶
zeroboot 内置了完整的 benchmark 工具,包含 5 个测试阶段:
Phase 1: Pure mmap CoW
测试 mmap(MAP_PRIVATE) 在 memfd 上的 CoW 性能,10000 次迭代。这是 fork 的内存映射基础操作。
Phase 2: Full fork(KVM + CoW + CPU restore)
完整的 VM fork 流程:创建 KVM VM → CoW 内存映射 → 恢复所有 CPU 状态。1000 次迭代,测量端到端 fork 延迟。
Phase 3: Fork + exec
Fork 一个 VM 并通过 serial 发送 echo hello 命令,等待执行完成。100 次迭代,测量从 fork 到获得输出的全链路延迟。
Phase 4: Concurrent forks
分别创建 10、100、1000 个并发 fork,测量总耗时和内存占用。验证大规模并发场景的可行性。
Phase 5: Memory isolation
验证 CoW 隔离:Fork A 向特定内存地址写入 0xDEADBEEF_CAFEBABE,Fork B 读取同一地址,应读到快照原始值而非 Fork A 写入的值。
测试结果¶
Phase 1: Pure mmap CoW(10000 次)¶
| 指标 | 延迟 |
|---|---|
| P50 | 0.7 µs |
| P95 | 1.4 µs |
| P99 | 2.2 µs |
亚微秒级的 CoW 映射,说明底层 mmap 性能在嵌套虚拟化下无损——因为 mmap 是 L1 host kernel 操作,不涉及 L2 KVM。
Phase 2: Full fork — KVM + CoW + CPU restore(1000 次)¶
| 指标 | 延迟 |
|---|---|
| Min | 0.502 ms |
| P50 | 0.699 ms |
| P95 | 0.943 ms |
| P99 | 1.064 ms |
| Max | 1.219 ms |
核心指标:P50 = 0.699ms,P99 = 1.064ms。亚毫秒级 VM spawn 在嵌套虚拟化下成立。
Phase 3: Fork + exec echo hello(100 次,100% 成功率)¶
| 指标 | 延迟 |
|---|---|
| P50 | 6.309 ms |
| P95 | 6.536 ms |
| P99 | 6.790 ms |
从 fork 到执行完命令并拿到输出,~6ms。延迟主要来自 serial I/O 和 guest 内代码解析执行。
Phase 4: Concurrent forks¶
| 并发数 | 总耗时 | 每 fork 耗时 | 总 RSS | 每 fork 内存 |
|---|---|---|---|---|
| 10 | 8.9ms | 894.7µs | 3.9MB | 395.2KB |
| 100 | 80.2ms | 802.1µs | 9.9MB | 101.4KB |
| 1000 | 1094.4ms | 1094.4µs | 70.7MB | 72.4KB |
1000 个并发 VM,总共只占 70.7MB 内存。每个 sandbox 仅 72.4KB——这就是 CoW + MAP_NORESERVE 的威力。
Phase 5: Memory Isolation¶
✅ Fork B 无法读取 Fork A 写入的 0xDEADBEEF_CAFEBABE,双向隔离验证通过。
与官方数据对比¶
| 指标 | zeroboot 官方 | 本次实测(EC2 嵌套虚拟化) | 差异 |
|---|---|---|---|
| Spawn P50 | 0.79ms | 0.699ms | -11.5%(更快) |
| Spawn P99 | 1.74ms | 1.064ms | -38.9%(更快) |
| 每 sandbox 内存 | ~265KB | 72.4KB(1000 并发) | -72.7%(更省) |
| Fork + exec | ~8ms | 6.31ms | -21.1%(更快) |
| 1000 并发总耗时 | 815ms | 1094.4ms | +34.3%(略慢) |
分析¶
单次 fork 性能优于官方,可能原因:
- CPUID 处理差异:我们使用 merged CPUID(snapshot ∩ host 交集),减少了 KVM 的验证开销
- CPU 代际优势:c8i 使用 Intel Sapphire Rapids(第 4 代 Xeon),VT-x 优化更成熟
- AMX 禁用:减少了 XSAVE 区域大小,内存占用更低(从 ~265KB 降到 72.4KB)
1000 并发略慢(+34.3%),原因:
- 嵌套虚拟化下
KVM_CREATE_VM有额外的 VMX context 创建开销 - 1000 个 L2 VM 的 VMCS 管理比 L1 更复杂
- 但 overhead 在可接受范围,且单次 fork(P50 < 1ms)完全不受影响
踩坑记录¶
1. XCR0/AMX 不兼容 → clearcpuid 禁用¶
问题
Firecracker 快照捕获的 XCR0=0x602e7(含 AMX_TILE/AMX_BF16/AMX_INT8),但 EC2 嵌套虚拟化的 L1 KVM 不支持为 L2 VM 设置含 AMX 的 CPUID。KVM_SET_CPUID2 返回 EINVAL。
根因:Intel AMX 是较新的 CPU 扩展,嵌套虚拟化的 KVM 尚未完全支持其 passthrough。
解决方案:在 kernel boot_args 中添加 clearcpuid=amx_tile,amx_bf16,amx_int8,让 guest kernel 启动时不探测 AMX 特性。快照的 XCR0 变为 0x2e7(仍含 SSE、AVX、AVX-512),与 L2 KVM 兼容。
2. vmstate 解析器偏移量 → 模式匹配定位¶
问题
zeroboot 需要解析 Firecracker 的 vmstate 二进制文件提取 CPU 寄存器状态。但 vmstate 中各 section 是 variable-length(取决于 Firecracker 版本、kernel、boot_args),硬编码偏移量不可靠。
解决方案:使用锚点模式匹配:
- CPU 寄存器区域:搜索
CR0=0x80050033(Linux kernel 标准 CR0 值)作为锚点 - LAPIC:搜索
version=0x50014+spurious=0x1FF模式 - IOAPIC:搜索
0xFEC00000(标准 IOAPIC base address)
这种方式比硬编码偏移量更鲁棒,能适应不同 Firecracker 版本和配置。
3. KVM_SET_CPUID2 受限 → CPUID 交集¶
问题
嵌套虚拟化下,L1 KVM 不允许为 L2 VM 设置任意 CPUID。直接使用快照中的 CPUID 会导致 KVM_SET_CPUID2 失败。
解决方案:将 snapshot CPUID 与 host-supported CPUID 取交集(AND 各 feature bits)。这保证了:
- guest 看到的 CPU 特性都是 L2 KVM 真正支持的
- 不会因为 CPUID 声明的特性与实际不符导致
SIGILL - numpy 等在快照中已完成 SIMD detection 的库不受影响(因为交集后的特性集是子集)
4. LAPIC 偏移错误 → 模式匹配定位¶
问题
vmstate 中 LAPIC 和 IOAPIC 之间有 variable-length sections。最初尝试用 IOAPIC 的偏移量线性推算 LAPIC 位置,结果偏差 24 字节。错误的 LAPIC 状态导致 guest 中断系统失效,vCPU 死循环(无法响应 timer interrupt)。
表现:Fork 后 guest 无响应,KVM_RUN 永远不返回。
解决方案:放弃线性推算,改用独立的模式匹配(LAPIC version + spurious vector 特征值)直接定位 LAPIC 数据。每个设备的偏移量独立计算,互不依赖。
费用明细¶
| 资源 | 单价 | 用量 | 费用 |
|---|---|---|---|
| c8i.xlarge(EC2 On-Demand) | $0.192/hr | ~1 hr | $0.19 |
| EBS gp3 30GB | $0.08/GB-month | ~1 hr | ~$0.003 |
| 数据传输 | - | 极少量 | ~$0 |
| 合计 | ~$0.19 |
对比:如果用 bare metal 实例(c7i.metal-24xl, $4.608/hr),同样 1 小时的测试成本是 $4.61,贵了 24 倍。嵌套虚拟化让 benchmark 验证的成本可以忽略不计。
清理资源¶
# 终止 EC2 实例
aws ec2 terminate-instances \
--instance-ids $INSTANCE_ID \
--region ap-southeast-1
# 等待实例终止
aws ec2 wait instance-terminated \
--instance-ids $INSTANCE_ID \
--region ap-southeast-1
# 删除安全组
aws ec2 delete-security-group \
--group-id $SG_ID \
--region ap-southeast-1
# 删除 Key Pair
aws ec2 delete-key-pair \
--key-name zeroboot-benchmark-key \
--region ap-southeast-1
# 清理本地临时文件
rm -f /tmp/zeroboot-benchmark-key.pem
务必清理
Lab 完成后请执行清理步骤,避免产生意外费用。c8i.xlarge 按需价格 $0.192/hr,忘记关机一天就是 $4.61。
结论与建议¶
核心结论¶
zeroboot 在 EC2 嵌套虚拟化环境下性能验证通过。亚毫秒级 VM spawn(P50 = 0.699ms)在嵌套虚拟化的次优条件下依然成立,部分指标甚至优于官方数据。
适用场景¶
- AI Agent 代码执行:需要高频、低延迟、强隔离的代码 sandbox
- CI/CD 测试隔离:每个测试用例在独立 VM 中运行,互不影响
- 安全沙箱:KVM 硬件级隔离,比 container 更强的安全边界
嵌套虚拟化 vs Bare Metal 建议¶
| 场景 | 推荐 | 理由 |
|---|---|---|
| 开发测试/Benchmark | 嵌套虚拟化(c8i.xlarge) | 成本低 24 倍,性能足够 |
| 生产环境(低并发) | 嵌套虚拟化 | 单次 fork 性能无损 |
| 生产环境(高并发 >1000) | Bare Metal | 大规模并发 overhead 更低 |
注意事项¶
- 嵌套虚拟化需要
clearcpuid禁用 AMX,这意味着 guest 内无法使用 AMX 指令加速(但 AVX-512 仍可用) - vmstate 解析依赖 Firecracker 版本,升级 Firecracker 后可能需要重新验证
- CPUID 交集处理是嵌套虚拟化特有的要求,bare metal 上不需要