2026 年 6 月 18 日 · 15 分钟阅读 · #containers #homelab #kvm #microvm #proxmox #qemu #virtualization

多年来我一直在运行一个混合的 Proxmox 集群——四个节点的能力差异极大,从 Atom x5-Z8350(2 GB RAM,一台 z83ii,已因多年作为基准 torture 设备服役后下线)到 i7-12700(128 GB,borg,我的主 homelab 服务器)。

今年,在编写 agentbox 以及围绕 agentic sandboxes 的各种炒作过程中,我厌倦了 LXC 容器与完整虚拟机之间永恒的折衷,于是构建了 pve-microvm——一个 Debian 软件包,它将 QEMU 的 microvm 机器类型作为 Proxmox VE 中一级托管的 guest 添加。

这不是一个快速的 hack。实际上第一个版本确实是,但它已经走得比那远得多,而且肯定比我预期的更远。

它现在附带一个自定义内核,修补 Perl 内部以提供 Proxmox Web UI 集成,并且由于我对非主流操作系统的习惯性兴趣,截至撰写本文时已支持 21 种 guest OS 类型,从 Debian 到 NetBSD 再到 Plan9

是的,我完全是自找麻烦要在 microVM 中运行 Plan9,而且确实可以运行。

找到合适的平衡

经过几轮集群清理和迁移后,它现在是我日常运行 Gitea、Caddy 反向代理、迷你防火墙以及帮助我清理这篇文章的 AI agent 的日常驱动。

Proxmox 开箱即用提供了两个主要选项:

  • LXC 容器立即启动,共享宿主机内核,效率极高。但它们并不隔离——一个容器中的内核漏洞会危及所有内容。你无法运行不同的操作系统。你无法轻松地在其中嵌套 Docker,而不最终与 fuse-overlayfs 体操搏斗。某些工作负载(需要自定义内核模块或激烈使用 CAP_SYS_ADMIN 的任何内容)根本不适合。

  • 完整 VM 通过 KVM/VT-x 提供硬件隔离,但它们会引导 SeaBIOS 或 OVMF,懒洋洋地走出床铺,逐步通过 GRUB,探测大量模拟的遗留设备(IDE 控制器、VGA、USB 集线器、PCI 桥),通常需要 5-10 秒才能到达登录提示。每个 VM 都携带整个模拟芯片组在内存中的开销。

我想要的是 VM 的安全边界加上容器的启动特性。QEMU 的 microvm 机器类型——最初是为 Firecracker 式工作负载开发的——剥离了所有这些。没有 BIOS,没有 GRUB,没有遗留设备。直接内核引导到仅 virtio 的最小环境。结果:不到 300 毫秒即可引导到完全联网的 guest,带有 QEMU agent,运行在自己的 KVM 硬件隔离边界内。

Comparison of Standard VM, microVM, and LXC Container isolation and boot characteristics
Comparison of Standard VM, microVM, and LXC Container isolation and boot characteristics

现在,我要明确说明:我没有生成数百个这样的实例。我有 Azure 来做这件事——但我确实想运行 Gitea Actions worker,硬件资源非常有限,并且厌倦了某个特定 VM 反复引导所花费的时间……

它实际做了什么

pve-microvm 是一个单一的 .deb,在安装时修补 Proxmox 的 qemu-server Perl 模块。当你在 VM 配置中设置 machine: microvm 时,标准的 config_to_command 函数会委托给我的 MicroVM.pm,它构建一个(几乎)完全不同的 QEMU 命令行:

qemu-system-x86_64 -M microvm,x-option-roms=off,pit=off,pic=off,\
  isa-serial=on,rtc=on,acpi=on,pcie=on \
  -kernel /usr/share/pve-microvm/vmlinuz \
  -initrd /usr/share/pve-microvm/initrd \
  -append "console=ttyS0 root=/dev/vda rw quiet" \
  -device virtio-blk-pci-non-transitional,drive=drive-scsi0 \
  -device virtio-net-pci-non-transitional,netdev=net0 \
  ...

没有芯片组模拟。没有 PCI 桥。没有 VGA。guest 获得一个串行控制台(PVE 的 xterm.js 本地连接到它),virtio 块设备和一个 virtio 网络接口。一切都通过 PCIe 传输,使用非过渡(仅现代)virtio 设备,而不是 microvm 最初设计的 MMIO 传输——原因我稍后会提到。

How pve-microvm integrates with Proxmox VE internals
How pve-microvm integrates with Proxmox VE internals

该软件包提供:

  • 一个微小的(12MB)预构建 Linux 6.12.22 内核,从 x86_64_defconfig 编译,带有最小 overlay——virtiovsockvirtiofs9p,以及 Docker 所需的模块(overlayvethbridgenetfilterBPF),因为……我很务实。
  • 一个 1 MB 的 initrd,探测 virtio 设备,通过标签或设备路径找到根文件系统,并在约 150 毫秒内执行 switch_root
  • pve-microvm-template——从 12 种支持的 OCI 基础镜像中的任何一种构建根文件系统,可选 SSH、Docker 和 guest agent
  • pve-oci-import——直接将 OCI 镜像拉取到 PVE 管理的磁盘中
  • Web UI 扩展——“创建 µVM”按钮、机器类型下拉菜单、隐藏不相关设置的条件面板,以及资源树中的琥珀色闪电图标
  • 一个 systemd 服务(pve-microvm-early.service),确保在引导时 pvedaemon 启动前应用补丁——这对 onboot=1 的 VM 至关重要

引导序列

就像空气动力学一样,大部分速度来自于消除一切非严格必要的东西。标准 VM 将大部分引导时间花费在固件和引导加载程序上,因此 microVM 跳过了所有这些。

Boot timeline comparison between microVM and standard VM
Boot timeline comparison between microVM and standard VM

SmolBSD(使用 virtio-mmio 传输的 NetBSD guest)在 31 毫秒内引导。带有 Docker 和 QEMU agent 的完整 Debian 在不到 8 秒内准备就绪——其中大部分时间是首次引导期间的 apt 包安装。后续引导始终达到 300 毫秒,即使在我简陋的硬件上也是如此。

当有人要求我添加 SmolBSD 支持时,我陷入了一个有趣的 rabbit hole:

SmolBSD 可以使用 virtio-mmio 而 Linux guest 不能,这是有原因的。QEMU microvm 机器类型可以通过两种传输携带其 virtio 设备:它最初构建的简陋 MMIO 接口,或 PCIe。MMIO 是两者中更轻量的——没有 PCIe 主机桥,没有 ACPI——这就是 NetBSD guest 将自己削减到 31 毫秒的方式。

但在 QEMU 10.x 上,MMIO 路径(据我所知)对 Linux guest 存在设备探测 bug:只有 virtio-blk 绑定,网络、串行和 balloon 设备由于某种原因从未被其驱动程序声明。NetBSD 正确探测 MMIO 并且完全没问题;Linux(至少我使用的内核)不是这样。

因此,对于每个 Linux guest,我都会回退到 PCIe,使用非过渡(仅现代)virtio 设备,这会可靠地绑定所有设备。代价是大约 50 毫秒的额外启动时间——相对于 300 毫秒的引导,我会毫不抱怨地接受。

我认为上述实际上是我内核配置中的一个 bug,但我真的没有时间(甚至可能没有合适的硬件)来解决它——这是我希望更多人查看并贡献补丁的事情。

一个内核,许多 guest

这种直接内核引导技巧有一个容易被忽视的故意后果:内核并不存在于 guest 内部。它位于 Proxmox 宿主机的 /usr/share/pve-microvm/vmlinuz,而 guest 磁盘仅保存根文件系统——用户空间,没有 /boot,没有 GRUB,没有 per-guest 内核包,没有自己的 initramfs

这也意味着没有“引导安装程序 ISO 并点击通过”的路径,因此 rootfs 直接从 OCI 镜像使用 pve-microvm-template 构建(Debian、Alpine、Fedora、Rocky、Amazon Linux 等)。在奇怪的情况下,我们使用 qm importdisk 导入准备好的 ext4/raw 磁盘。你不是安装一个 OS——你是在组装一个根文件系统。

将内核与 rootfs 解耦是使这在规模上运行有趣的原因。节点上的每个 Linux microVM 都引导相同的 vmlinuz——一个内核,从 stock x86_64_defconfig 构建一次,带有 microvm overlay,因此你可以完全在一个地方审计和更新它:在宿主机上放置一个新的 vmlinuz,重启 guest,完成。没有 guest ever 会从 apt 升级中拉取损坏的内核,因为没有 guest内核可升级,而且 rootfs 镜像保持很小且完全与内核无关。

容器风格的内核一致性,VM 风格的隔离。

我在运行什么

在我的集群上,我现在已经有相当数量的这些实例。随便说四个:

  • gitea(VM 114,在 Intel N5105 上)——裸机 Gitea,使用 SQLite、Caddy HTTPS、本地 actions runner、Avahi 发现。2 核,2 GB RAM,32 GB 磁盘。引导时间约 3 秒,主要是因为 Gitea 会做大量 housekeeping。
  • smith(VM 9022,在我的 i7 上)——主要的 piclaw agent,管理集群、发布 piclaw 并通常监控一切。2 核,6 GB RAM,48 GB 磁盘。在内部运行 Docker,并且在整个集群中有 3 个较小、易失的兄弟实例,它们不运行 Docker 但有不同的角色(CI/CD、用于测试升级的擦除并重新安装 agent 实例等)。
  • exo(VM 9021,在我的 i7 上)——跨多台机器运行 LLM 的分布式推理协调器。仅 CPU,2 GB 根。
  • virtualdsm(VM 300,tnas)——在 microVM 中运行的 Synology DSM,带有 Docker,在 Terramaster NAS 硬件内部。是的,我知道我很奇怪,但当我的 Synology 出问题时需要它,我还没有把它 nuked。它使用 stock Debian 内核而不是我的自定义内核,因为 DSM 需要特定的模块路径。

我还有一个休眠的 9Front(Plan9)实例,以及一小群 OpenWrt、OPNsense、OSv unikernels、gokrazy Go appliances,以及作为标准 Proxmox 备份归档的各种 Alpine/Fedora/Rocky/Amazon Linux 配置。21 种 guest OS 类型不是理论上的——每一种都已引导并验证,有时 smith 会解冻一个来进行回归测试。

z83ii(那台古老的 Atom x5-Z8350,2GB RAM)作为基准测试平台非常宝贵,因为如果一个 microVM 可以在一台无风扇的 2016 年 Atom 上引导并有用地运行(总系统内存 2 GB),它就可以在任何地方运行。而且它可以运行六个,直到它开始变慢……

配置看起来像什么

microVM 配置没有什么魔法——它是一个普通的 qm guest,带有特定的 machine 类型和内核命令行。下面是 gitea(上面的 VM 114)在 /etc/pve/qemu-server/114.conf 中的样子:

agent: 1
args: -kernel /usr/share/pve-microvm/vmlinuz -append "console=ttyS0 root=/dev/vda rw quiet"
boot: order=scsi0
cores: 2
machine: microvm
memory: 2048
name: gitea
net0: virtio=BC:24:11:00:6E:01,bridge=vmbr0
onboot: 1
scsi0: local-lvm:vm-114-disk-0,size=32G
serial0: socket
tags: microvm
vga: serial0

仅 microvm 特定的行是 machine: microvm、携带内核及其 cmdline 的 args,以及 serial0: socket / vga: serial0 将控制台连接到 xterm.js。其他一切——核心、内存、vmbr0 上的 virtio NIC、local-lvm 上的 scsi0 磁盘、onboot、guest agent——都与你为任何 Proxmox VM 编写的内容完全相同。

注意 args 中没有 -initrdMicroVM.pm 在看到提供的内核时会自动注入它,以及 balloonvsock 和(当配置时)virtiofs 设备。这就是重点——microVM 是一个普通 guest,碰巧引导宿主机提供的内核,而不是一个你必须学习新工具来管理的特殊对象。

网络

microVM 连接网络的方式与任何其他 Proxmox guest 完全相同。接口规范有点冗长(PCIe 总线上的 virtio-net-pci-non-transitional 设备),它会落在你指向它的任何 Linux 桥接和 VLAN 上:

qm set 900 --net0 virtio,bridge=vmbr0           # single NIC
qm set 900 --net0 virtio,bridge=vmbr0,tag=100   # tagged onto VLAN 100

因为它是普通的 KVM guest,标准 PVE 防火墙适用——per-VM nftables 规则的工作方式与任何 VM 相同,这是当你运行不受信任的代码时真正重要的部分。

在 guest 内部,网络由 systemd-networkd 处理,而不是 cloud-init:默认 DHCP(在 Type=ether 上匹配,因此在克隆时无需 MAC 固定即可生存),或 /etc/microvm-static-net 的一行静态地址。早期版本依赖 cloud-init 来实现这一点,我发现它太脆弱;将其移至 systemd-networkd 使克隆可靠,我不再需要一半时间调试模板。

网络隔离,当前围绕 agent 隔离炒作的主流,已是一个已解决的问题,我没有兴趣在包内重新解决,因为我认为 Proxmox 自己的 SDN 已经正确地做到了这一点——一个简单的 VLAN zone,每个信任域有一个单独的 VNet,将不受信任的 guest 保留在自己的段上,没有到 LAN 的路径,如果我曾经在家庭 LAN 上 bothered(好吧,我考虑过……但没有时间),一个带有指定出口节点的 VXLAN zone 将允许我通过单个防火墙 choke point 引导 egress。

microVM 只是落在你指向 net0 的任何 VNet 上,因此策略存在于 Proxmox 中,而不是我必须 babysit 的一次性规则集中。

还有一个非网络路径用于宿主机/guest 管道:每个 guest 获得一个 vsock CID(VMID + 1000),我用它进行 SSH-agent 转发(将宿主机密钥放入 guest,而无需在网络上暴露它们)以及 virtiofs/9p 目录共享,因为……我忘了。我知道我曾经需要它,即使现在我的大多数 microvm 实例只是做 SMB 挂载(这在 Docker 和 LXC 下做起来很痛苦)

NIC 数量……相当合理。我目前允许每个 guest 六个 virtio 接口(net0-net5),理由是它们是我所有宿主机上物理接口最大数量的两倍。它只在 MicroVM.pm 中定义,非常少数需要更多的人欢迎调整它(如果你认为你需要十二个,你几乎肯定想要 VLAN 而不是更多接口)。

存储和迁移

存储……很简单?每个 PVE 后端都工作,因为磁盘只是一个 virtio-blk-pci 设备,而 Proxmox 给它提供与任何 VM 相同的路径。LVM 和 LVM-thin、ZFS、Ceph/RBD、NFS 和 CIFS、普通目录存储——都很好,快照、链接和完整克隆、vzdump 备份和 qm importdisk 的行为与其他地方完全相同。microvm 是一个正常的 PVE guest,碰巧引导方式不同。

迁移是唯一有点问题的地方。a) 我在任何硬件上都无法进行实时迁移(没有一台是企业级的),b) 由于当前的 QEMU microvm 机器类型根本没有实现它,真正的实时迁移也不可行。

不过,离线迁移(即在节点间重新分配实例)可以正常工作,而且因为 microvm 引导时间远低于 1 秒,你可以运行快速的 HA 重定位循环——停止、迁移、启动——前提是你的磁盘位于共享存储上。

我在共享 CIFS 上在节点间移动一个小 guest 时测量到大约两秒。不是无缝的 VM 移动,但对于大多数用途可能足够好,而且据我所知,ha-manager 以与任何 guest 相同的方式驱动它。

与 LXC 相比:何时选择什么

不过,我仍然运行 LXC 容器。当以下情况时,它们是正确的选择:

  • 你信任工作负载(或它是自己的代码)
  • 你需要零引导开销
  • 你想要从宿主机直接访问文件系统
  • 你想要稍微更少的内存分配开销(哦,如果你真的幸运的话,还有共享页缓存)

当以下情况时,MicroVM 获胜:

  • 你需要实际的内核隔离——运行不受信任的代码、不同的内核版本、嵌套 Docker、任何具有激进 CAP_ 要求的内容
  • 你想要一个可重现的镜像,可以快照、备份(vzdump 工作)、离线迁移和克隆(也支持链接克隆)
  • 工作负载对内核做了一些不寻常的事情(BPF 程序、自定义 netfilter 规则、内核模块),而你不希望它泄漏到你的宿主机(这就是为什么我的大多数隧道现在都落在 microvm 中)
  • 你正在运行非 Linux guest——NetBSD、Plan 9、基于 FreeBSD 的防火墙——这些根本不能作为 LXC 运行
  • 你想要一个在你完成命令前就已经启动的 VM。

在现代硬件上,LXC 和 microVM 之间的开销差异出奇地小。在 borg(那台 i7-12700)上,最小 microvm 的空闲内存占用约为 40 MB,对于大多数工作负载,hypervisor 的 CPU 开销无法测量。

你在引导时设置的内存数字实际上只是一个上限:据我所知,KVM 仍然只支持 guest 实际触及的页,而宿主机的 same-page merging 会在节点上的每个 microvm 之间对相同页进行去重——内核、libc、共享基础镜像层——因此就像在成熟的云 hypervisor 上一样,实际成本跟踪工作集,而不是名义分配。

令我尴尬的是,我很长一段时间甚至没有 bother 进行实时调整,但最近我为内核添加了 balloon 支持,带有 free-page-reportingdeflate-on-oom。PVE 的 balloon target 驱动正确的 auto-ballooning,而 virtio-mem 设备提供真正的细粒度热添加和热移除,因此,这再次像普通 VM 一样工作。

尴尬的部分

现在来说说问题……

第一个相当明显:我在修补别人的产品,尽管我几十年前经历了 Perl 4 到 Perl 5 的过渡,而且现在有 Codex 帮助,修补 Perl 内部仍然很脆弱——每次 qemu-server 升级都可能破坏我的设置。

我通过 a) 不盲目运行升级和 b) 使用 dpkg 触发器(包在升级后自动重新修补)来缓解这一点,但这仍然是一个等待发生的 race condition——我遇到过部分 PVE 升级导致系统处于 pvedaemon 甚至无法编译修补模块的状态。

然后是奇怪的故障模式——一次升级有一个关于根设备的小问题:

  • 根在 root=/dev/vda 找到,因为 guest 在 microvm 机器类型下引导,virtio-blk 在 PCIe 上
  • 但如果 guest 曾经在标准芯片组下启动(半应用的补丁,或 onboot=1 的 VM 在宿主机重启后 early-boot 服务重新修补之前启动),同一磁盘枚举为 /dev/sda,找不到根,VM 看起来好像丢失了文件系统。

数据始终存在,只是在错误的路径上。我已经以一种 dpkg 触发器和 early-boot 服务排序试图缓解它的方式修补了这个特定的扭曲,因此 initrd 现在在 /dev/vda 缺失时回退到 /dev/sda——但直到(如果 ever)Proxmox 原生支持 microVM,偶尔会出现小问题。

包版本不匹配可能会级联。我通过艰难的方式学到了这一点——我一个节点上的部分 apt upgrade 导致 libpve-cluster-perllibpve-network-perl 处于不兼容版本,这破坏了所有 Perl 模块加载,导致 pvedaemon 无法启动,导致 VM 无法管理。

所以……在有此设置的 PVE 节点上,始终执行完整的 dist-upgrade,绝不执行部分升级。

另一个(对某些人来说)问题是:没有 VGA,没有图形控制台——串行控制台是你唯一的接口。如果引导过程中出现问题,你会在终端上阅读内核 panic。这对服务器来说没问题,对面向桌面的 guest 不太好,而且 Plan9 真的不喜欢它。

下一个问题是根本没有 USB——没有控制器,没有 passthrough,总线上什么都没有——所以我必须让 Web UI 在你将 guest 切换到 microvm 的那一刻就隐藏这些选项。如果你想运行 Zigbee 控制器(这也是我的家庭自动化东西仍然在 LXC 中的原因),这可能是一个 deal-breaker。

内核是有主见的。我的自定义 6.12.22 内核只包含我需要的内容,其他什么都没有。如果你的工作负载需要我没有包含的模块,你需要重新构建它或使用 stock Debian 内核(它可以正常工作,但大 3 倍,引导更慢)。

最后,GPU 和 PCI passthrough 是禁用的,而不是不可能的。这是我希望有更多钱硬件的人真正深入研究的事情,因为我实际上在早期测试中通过并工作了一个 RTX 3060。

最终,我决定为了简单起见暂时禁用 GPU 支持——包今天会剥离 hostpci*,并且没有 vIOMMU 管道连接,因此默认关闭。

没有任何东西阻止任何人添加它回来(除了 QEMU microvm 关于最小 ECAM PCIe 总线和 IOMMU 设置的一些注意事项),而且我真的很想能够进行正确的 PCI 和 vIOMMU 测试——我只是没有多余的硬件,而且我选择专注于运行 agent 而不是追逐加速器 passthrough。

内部:补丁策略

该包修改了 PVE Perl 栈中的三个文件:

  • Machine.pm——扩展机器类型正则表达式以接受 microvm 作为有效值
  • QemuServer.pm——在 config_to_command 顶部添加 use PVE::QemuServer::MicroVM 导入和委托检查:如果 VM 有 machine: microvm,则完全移交给我的模块
  • MicroVM.pm——完整的命令构建器,安装在 /usr/share/perl5/PVE/QemuServer/MicroVM.pm

由于我对可恢复性很执着,补丁是可逆的(pve-microvm-patch revert),原始文件已备份。dpkg 触发器系统(interest-noawait qemu-server)确保在 PVE 更新后自动重新应用。early-boot 服务确保在任何 VM 自动启动前补丁已生效,防止我上面提到的 sda/vda 混淆:

pve-microvm-early.service (Before=pvedaemon.service pve-guests.service)
    └── /usr/share/pve-microvm/pve-microvm-patch apply

入门

如果你读到这里,你要么是一个勇敢的人,要么错过了GitHub 链接,所以我在这里只提供简要版本:

# 在任何 PVE 9.x 节点上:
wget https://github.com/rcarmo/pve-microvm/releases/latest/download/pve-microvm_0.3.12-1_all.deb
dpkg -i pve-microvm_0.3.12-1_all.deb

# 创建一个 Debian microVM 模板:
pve-microvm-template --vmid 9000 --storage local-lvm --profile standard

# 将其克隆到真实 VM:
qm clone 9000 100 --name my-microvm --full
qm set 100 --machine microvm --memory 1024 --cores 2
qm start 100

模板构建大约需要 60 秒(拉取 OCI 镜像、安装包、写入根文件系统)。之后,如果使用链接克隆,克隆几乎是即时的。

接下来

除了更多的测试(我只是没有硬件、时间甚至专注力,因为像我的大多数项目一样,我只是想使用这个东西),剩下的主要是润色:也许一个更好的配置层、默认不联网并为不受信任的 guest 设置 egress allow-lists(对于偏执狂)、如果我 ever 正确连接 vIOMMU 的话的 GPU passthrough,以及最终对 ARM 基础的 PVE 节点(我过去运行过,我认为它们的 QEMU 版本也可能有 microvm 支持)的 AArch64 支持。

当然,也应该有人 upstream 这个。我非正式地联系了 Proxmox 的几个人,他们告诉我应该加入一个 mailing-list 来讨论这个问题,但 a) 那太 90 年代了,b) 我只是没有时间做更多的事情来为自己维护它。而且是的,我知道这必须通过整个企业 QA 管道(我经常遇到这种情况,相信我)。

源代码在 github.com/rcarmo/pve-microvm,补丁相当小,它不会破坏 Proxmox 本身,因为 99% 的功能来自 QEMU/KVM,所以……我只是字面上的把它送出去。

但如果你正在运行 homelab,熟悉 Linux 并想要具有实际隔离的容器速度 VM,这可能就是你自……好吧,从统计上讲,也许从未寻找过的东西——但你现在可以拥有它!