Hotdry.

Article

用 Holos 实现 Docker Compose 风格的 KVM 虚拟机编排

深入解析 Holos 项目:如何用类 Docker Compose 的 YAML 语法管理 QEMU/KVM 虚拟机,实现 GPU 透传、健康检查与自动化运维。

2026-04-20systems

在容器化浪潮席卷基础设施的今天,虚拟机并没有退出历史舞台,而是以更加灵活的形态与容器共存。对于需要完整操作系统隔离、硬件透传或运行遗留工作负载的场景,KVM 仍然是不可替代的选择。然而,传统的 KVM 管理依赖 libvirt、XML 配置、复杂的网络桥接和层层抽象,运维门槛高不说,单台主机上的多虚拟机编排也缺乏像 Docker Compose 那样直观的管理体验。Holos 项目的出现正是为了填补这一空白 —— 它让用户用熟悉的 Docker Compose 语法来定义和管理 QEMU/KVM 虚拟机栈。

Holos 的核心设计理念

Holos 的定位非常明确:不引入 Kubernetes 式的复杂控制平面,也不依赖 libvirt 和 XML,而是以最直接的方式在单台主机上编排多个虚拟机实例。它的核心抽象是「VM as a Service」—— 每个服务对应一台拥有独立内核边界、独立 qcow2 快照和独立 cloud-init 种子数据的虚拟机。这种设计理念与 Docker Compose 的服务模型高度契合:用户只需编写一份 holos.yaml 文件,即可描述整个虚拟化栈的拓扑结构、资源配置和网络关系。

从技术实现来看,Holos 直接调用 qemu-system-x86_64,不经过任何中间层。这意味着每一台虚拟机都是独立的 QEMU 进程,拥有自己的 VirtIO 设备模型(网卡、块设备、console 等)和独立的启动参数。项目明确列出了主机层面的依赖:/dev/kvm 必须可用、qemu-system-x86_64 和 qemu-img 是必备工具、cloud-localds 或 genisoimage 用于生成 cloud-init 种子镜像、以及可选的 mkosi(用于从头构建基础镜像)。这些要求相对轻量,让 Holos 能够在大多数 Linux 发行版上快速部署。

YAML 配置语法:从容器语法到虚拟机语义的映射

Holos 的配置文件名为 holos.yaml,其语法刻意模仿 Docker Compose,以确保熟悉容器编排的用户能够零门槛上手。顶层结构包含 name(项目名称)、services(服务列表)和可选的 volumes(命名数据卷)。每个 service 代表一台虚拟机,最简配置只需要指定 image 即可运行,例如 image: ubuntu:noble 会自动拉取 Ubuntu Noble 的云镜像并创建虚拟机。

资源配额通过 vm 块定义:vcpu 指定虚拟 CPU 数量、memory_mb 指定内存大小。默认值相对保守 —— 单个 vCPU 和 512MB 内存 —— 适合轻量级工作负载,但这些参数可以根据实际需求灵活调整。例如,一个机器学习推理服务可能需要 8 个 vCPU 和 16GB 内存,一个数据库实例可能需要 2 到 4 个 vCPU 和 4 到 8GB 内存。Holos 不会对这些数值做硬性限制,用户拥有完全的裁量权。

cloud_init 块是 Holos 实现自动化配置的关键机制。它直接映射到 cloud-init 的标准模块:packages 用于指定系统包列表、write_files 用于注入文件内容、runcmd 用于执行启动命令。这正是 Holos 与传统虚拟机管理工具的核心区别 —— 它不是在虚拟机启动后手动 SSH 进去配置,而是通过云原生的方式在首次启动时完成所有初始化工作。一个典型的 web 服务配置可能包含安装 nginx、写入站点配置文件、以及启用并启动 nginx 服务,整个过程在虚拟机启动后的首次 boot 中自动完成。

健康检查与依赖等待:服务间编排的核心能力

Holos 实现了与 Docker Compose 完全一致的 depends_on 语义,但增加了一个重要的增强:健康检查 gating。当一个服务声明了 healthcheck 配置时,所有依赖它的服务不会在该服务通过健康检查之前启动。这解决了虚拟化场景中的一个经典难题 —— 如何在不知道服务何时真正可用的情况下保证启动顺序。

健康检查的实现机制非常巧妙:它通过 SSH(Holos 为每个项目自动生成并注入的密钥)连接到虚拟机内部执行探测命令。healthcheck 块支持所有标准字段:test 指定探测命令(可以是列表形式的 exec 格式或字符串形式的 shell 格式)、interval 指定探测间隔、retries 指定重试次数、start_period 指定容器启动后的初始化时间窗口、timeout 指定单次探测的超时时间。例如,一个 PostgreSQL 服务的健康检查可能配置为 test: ["pg_isready", "-U", "postgres"],探测间隔 2 秒,最多重试 30 次,初始等待期 10 秒,单次超时 3 秒。只有当 pg_isready 返回成功状态后,依赖该数据库的服务才会被允许启动。

这种设计对于构建多层架构尤为关键。想象一个典型的微服务栈:后端数据库、中间层 API 服务、前端 Nginx 反向代理。如果没有健康检查机制,API 服务可能在数据库尚未完成初始化时就开始连接,导致启动失败或反复重试。Holos 的 healthcheck + depends_on 组合让这种依赖关系可以被声明式地描述,并且自动得到保障。项目还提供了 HOLOS_HEALTH_BYPASS=1 环境变量,允许在 CI 环境或不需要 SSH 探测的场景下跳过实际检查。

GPU 透传:高性能计算工作负载的支持

对于需要 GPU 加速的工作负载 —— 深度学习推理、图形渲染、硬件视频编解码 ——Holos 提供了完整的 PCI 设备透传支持。配置方式非常直接:在服务的 devices 块中指定 pci 地址即可。例如,一个机器学习推理服务可以配置为:

services:
  ml:
    image: ubuntu:noble
    vm:
      vcpu: 8
      memory_mb: 16384
    devices:
      - pci: "01:00.0"
      - pci: "01:00.1"

这里的 "01:00.0" 和 "01:00.1" 通常分别对应 GPU 核心和 GPU 音频设备。用户可以通过 holos devices --gpu 命令查询主机上可用的 PCI 设备和 IOMMU 组信息,以确定正确的设备地址。

Holos 在 GPU 透传场景下做了几个重要的自动化工作:首先,当检测到 PCI 设备配置时,它会自动启用 UEFI 启动模式(使用 OVMF 固件);其次,它会自动设置 kernel-irqchip=on 以兼容 NVIDIA GPU;第三,它为每个虚拟机实例创建独立的 OVMF_VARS 副本,避免 EFI 变量冲突;第四,如果有自定义的 VBIOS ROM 文件,可以通过 rom_file 字段指定。这些细节如果手动配置往往容易出错,Holos 将它们自动化处理。

当然,GPU 透传仍然需要用户在主机层面做好准备工作:BIOS 中启用 IOMMU、内核启动参数中添加 intel_iommu=on 或 amd_iommu=on、以及将目标 GPU 绑定到 vfio-pci 驱动。这些是虚拟化基础设施层面的要求,超出 Holos 的管理范围,但在完成主机配置后,Guest 端的配置就变得非常简单。

网络与存储:去中心化的轻量方案

Holos 在网络层面的设计同样体现了「无 libvirt」的理念。默认情况下,每个虚拟机配备两块网卡:一块 user-mode 网络用于实现主机端口转发(类似 Docker 的默认网络),一块 socket multicast 网络用于实现虚拟机之间的 L2 互联。这种设计使得同一项目内的所有虚拟机可以通过服务名相互访问,无需手动配置网桥或 NAT。IP 地址在内部的 10.10.0.0/24 网段自动分配,/etc/hosts 文件通过 cloud-init 自动生成,因此服务名解析是开箱即用的。

对于需要静态 IP 的场景,Holos 支持在配置中显式指定 IP 地址,或者通过 cloud-init 的网络配置模块自行定义更复杂的网络拓扑。总体而言,Holos 刻意简化了网络层面的复杂度 —— 它不试图替代成熟的网络方案,而是为单主机多 VM 场景提供一个足够用的默认配置。

存储方面,Holos 支持两种模式:bind mount 和命名卷。bind mount 使用 Docker 风格的 ./source:/target:ro 语法,将宿主机目录映射到虚拟机内部,适合配置文件或静态资源的共享。命名卷则在顶层 volumes 块中声明,存储路径位于 state_dir/volumes/<project>/<name>.qcow2,会在 holos down 时保留(只删除符号链接,不删除实际数据),这对于数据库等需要持久化存储的服务尤为重要。命名卷以 VirtIO 块设备的形式呈现,虚拟机内部通过稳定的 serial ID 识别(格式为 virtio-vol-<name>),cloud-init 会在首次启动时自动执行 mkfs.ext4 并写入 /etc/fstab,整个过程完全自动化。

运维生命周期:优雅关闭与重启持久化

生产环境中的虚拟机管理不可避免地涉及启动和停止操作。Holos 实现了与 Docker Compose 一致的停止语义:当执行 holos stopholos down 时,运行时首先通过 QMP(QEMU Machine Protocol)发送 system_powerdown 命令,等效于在物理机上按下电源按钮;然后等待 stop_grace_period 指定的时长(默认 30 秒)让虚拟机自行关机;如果虚拟机在此期间没有响应 QMP,运行时才会降级到发送 SIGTERM,最终发送 SIGKILL。这种优雅关闭机制对于需要刷盘或清理连接的应用至关重要 —— 例如数据库服务可能需要时间将内存中的数据刷新到磁盘。

另一个生产级特性是 reboot survival。通过 holos install --enable 可以将当前项目注册为 systemd 用户单元,机器重启后会自动恢复运行状态。系统级安装需要 --system 参数,生成的 unit 文件位于 /etc/systemd/system/。卸载操作通过 holos uninstall 完成,且具有幂等性 —— 重复执行不会报错。

适用场景与局限性

Holos 最适合以下场景:单台物理服务器上需要运行多个相互依赖的虚拟机服务、团队熟悉 Docker Compose 语法且希望用相同方式管理 VM、需要硬件透传(GPU、PCIe 设备)但不想引入 Proxmox 或 libvirt 的复杂性、以及需要 cloud-init 自动化配置而非手动 SSH 运维。项目的非目标也很明确:不支持多主机集群、不支持 live migration、不包含服务网格或覆盖网络功能、也不提供 Kubernetes 式的调度器和 CRD。它的设计哲学是「让 KVM 在单主机上像容器一样易用」,而非构建一个分布式虚拟化平台。

从工程实践角度看,Holos 代表了一种「基础设施即代码」的思路延伸 —— 不仅容器可以声明式管理,虚拟机同样可以。它降低了虚拟化的使用门槛,让开发者能够用熟悉的工具链管理完整隔离的操作系统实例,在需要更强隔离或硬件访问的场景下提供了介于纯容器和传统虚拟化之间的折中选择。

资料来源:Holos 项目 GitHub 仓库(https://github.com/zeroecco/holos)

systems