使用 KVM 运行嵌套客户机

嵌套客户机是指在一个客户机内部运行另一个客户机(可以是基于 KVM 的,也可以是不同的虚拟机管理程序)。最简单的例子是一个 KVM 客户机,它反过来运行在另一个 KVM 客户机上(本文档的其余部分基于此示例构建)

        .----------------.  .----------------.
        |                |  |                |
        |      L2        |  |      L2        |
        | (Nested Guest) |  | (Nested Guest) |
        |                |  |                |
        |----------------'--'----------------|
        |                                    |
        |       L1 (Guest Hypervisor)        |
        |          KVM (/dev/kvm)            |
        |                                    |
.------------------------------------------------------.
|                 L0 (Host Hypervisor)                 |
|                    KVM (/dev/kvm)                    |
|------------------------------------------------------|
|        Hardware (with virtualization extensions)     |
'------------------------------------------------------'

术语

  • L0 – 0 级;裸机主机,运行 KVM

  • L1 – 1 级客户机;在 L0 上运行的 VM;也称为“客户机虚拟机管理程序”,因为它本身能够运行 KVM。

  • L2 – 2 级客户机;在 L1 上运行的 VM,这是“嵌套客户机”

注意

上面的图表是按照 x86 架构建模的;s390x、ppc64 和其他架构的嵌套设计可能有所不同。

例如,s390x 总是在裸机上运行 LPAR(逻辑分区)虚拟机管理程序,从而添加了另一层,并在嵌套设置中产生至少四个级别 — L0(裸机,运行 LPAR 虚拟机管理程序)、L1(主机虚拟机管理程序)、L2(客户机虚拟机管理程序)、L3(嵌套客户机)。

本文档将对所有架构都坚持使用三级术语(L0、L1 和 L2);并且主要关注 x86。

用例

在以下几种情况下,嵌套 KVM 可能会很有用,仅举几例

  • 作为开发人员,您想在不同的操作系统 (OS) 上测试您的软件。与其从云提供商租用多个 VM,不如使用嵌套 KVM 租用足够大的“客户机虚拟机管理程序”(1 级客户机)。这反过来允许您创建多个嵌套客户机(2 级客户机),运行不同的操作系统,您可以在其上开发和测试您的软件。

  • “客户机虚拟机管理程序”及其嵌套客户机的实时迁移,用于负载平衡、灾难恢复等。

  • VM 镜像创建工具(例如,virt-install 等)通常运行它们自己的 VM,用户希望这些工具在 VM 内部工作。

  • 一些操作系统在内部使用虚拟化来提高安全性(例如,让应用程序在隔离的环境中安全运行)。

启用“嵌套”(x86)

从 Linux 内核 v4.20 开始,默认情况下为 Intel 和 AMD 启用 nested KVM 参数。(尽管您的 Linux 发行版可能会覆盖此默认值。)

如果您运行的 Linux 内核版本早于 v4.19,要启用嵌套,请将 nested KVM 模块参数设置为 Y1。要在重启后保留此设置,您可以将其添加到配置文件中,如下所示

  1. 在裸机主机 (L0) 上,列出内核模块并确保 KVM 模块

    $ lsmod | grep -i kvm
    kvm_intel             133627  0
    kvm                   435079  1 kvm_intel
    
  2. 显示 kvm_intel 模块的信息

    $ modinfo kvm_intel | grep -i nested
    parm:           nested:bool
    
  3. 为了使嵌套 KVM 配置在重启后保持不变,请将以下内容放入 /etc/modprobed/kvm_intel.conf 中(如果该文件不存在,请创建它)

    $ cat /etc/modprobe.d/kvm_intel.conf
    options kvm-intel nested=y
    
  4. 卸载并重新加载 KVM Intel 模块

    $ sudo rmmod kvm-intel
    $ sudo modprobe kvm-intel
    
  5. 验证是否启用了 KVM 的 nested 参数

    $ cat /sys/module/kvm_intel/parameters/nested
    Y
    

对于 AMD 主机,该过程与上述相同,只是模块名称为 kvm-amd

启动嵌套客户机 (x86)

一旦您的裸机主机 (L0) 配置为嵌套,您应该能够启动 L1 客户机,如下所示

$ qemu-kvm -cpu host [...]

上述操作将按原样将主机 CPU 的功能传递给客户机,或者为了更好地实现实时迁移兼容性,请使用 QEMU 支持的命名 CPU 型号。例如

$ qemu-kvm -cpu Haswell-noTSX-IBRS,vmx=on

然后,客户机虚拟机管理程序将随后能够运行具有加速 KVM 的嵌套客户机。

启用“嵌套”(s390x)

  1. 在主机虚拟机管理程序 (L0) 上,在 s390x 上启用 nested 参数

    $ rmmod kvm
    $ modprobe kvm nested=1
    

注意

在 s390x 上,内核参数 hpagenested 参数互斥 — 即,为了能够启用 nested必须禁用 hpage 参数。

  1. 必须向客户机虚拟机管理程序 (L1) 提供 sie CPU 功能 — 使用 QEMU,可以通过使用“主机直通”来完成此操作(通过命令行 -cpu host)。

  2. 现在可以在 L1(客户机虚拟机管理程序)中加载 KVM 模块

    $ modprobe kvm
    

使用嵌套 KVM 进行实时迁移

将其中包含实时嵌套客户机的 L1 客户机迁移到另一个裸机主机,在 Linux 内核 5.3 和 QEMU 4.2.0 及更高版本中(适用于 Intel x86 系统)以及 s390x 的旧版本上均可正常运行。

在 AMD 系统上,一旦 L1 客户机启动了 L2 客户机,在 L2 客户机关闭之前,不应再迁移或保存 L1 客户机(请参阅 QEMU 关于“savevm”/“loadvm”的文档)。在 L2 客户机运行时尝试迁移或保存和加载 L1 客户机将导致未定义的行为。您可能会在 dmesg 中看到 kernel BUG! 条目,内核“oops”或完全内核崩溃。此类迁移或加载的 L1 客户机不再被认为是稳定或安全的,必须重新启动。即使在 AMD 系统上,仅配置为支持嵌套而实际上未运行 L2 客户机的迁移 L1 客户机也应正常工作,但一旦启动客户机,可能会失败。

迁移 L2 客户机始终有望成功,因此即使在 AMD 系统上,所有以下方案都应正常工作

  • 将嵌套客户机 (L2) 迁移到同一裸机主机上的另一个 L1 客户机。

  • 将嵌套客户机 (L2) 迁移到不同裸机主机上的另一个 L1 客户机。

  • 将嵌套客户机 (L2) 迁移到裸机主机。

从嵌套设置报告错误

调试“嵌套”问题可能需要筛选 L0、L1 和 L2 中的日志文件;这可能导致错误报告者和错误修复者之间来回繁琐的沟通。

  • 请提及您正在使用“嵌套”设置。如果您正在运行任何类型的“嵌套”,请说明。不幸的是,需要指出这一点,因为在报告错误时,人们往往会忘记提及他们正在使用嵌套虚拟化。

  • 确保您实际上正在 KVM 上运行 KVM。有时,人们没有为他们的客户机虚拟机管理程序 (L1) 启用 KVM,这会导致他们使用纯仿真或 QEMU 称之为“TCG”运行,但他们认为他们正在运行嵌套 KVM。因此,将“嵌套 Virt”(也可能意味着,KVM 上的 QEMU)与“嵌套 KVM”(KVM 上的 KVM)混淆。

要收集的信息(通用)

以下不是详尽的列表,而是一个很好的起点

  • L0 的内核、libvirt 和 QEMU 版本

  • L1 的内核、libvirt 和 QEMU 版本

  • L1 的 QEMU 命令行 — 当使用 libvirt 时,您可以在此处找到它:/var/log/libvirt/qemu/instance.log

  • L2 的 QEMU 命令行 — 如上所述,当使用 libvirt 时,获取完整的 libvirt 生成的 QEMU 命令行

  • L0 中的 cat /sys/cpuinfo

  • L1 中的 cat /sys/cpuinfo

  • L0 中的 lscpu

  • L1 中的 lscpu

  • L0 的完整 dmesg 输出

  • L1 的完整 dmesg 输出

要收集的 x86 特定信息

以下两个命令,x86infodmidecode,在大多数 Linux 发行版上应该都以相同的名称可用。

  • 来自 L0 的 x86info -a 的输出

  • 来自 L1 的 x86info -a 的输出

  • 来自 L0 的 dmidecode 的输出

  • 来自 L1 的 dmidecode 的输出

需要收集的 s390x 特定信息

除了前面提到的通用细节外,还建议收集以下信息

  • 来自 L1 的 /proc/sysinfo; 这也将包括来自 L0 的信息。