使用 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) 上测试您的软件。使用嵌套 KVM 可以让您租用足够大的“访客管理程序”(1 级访客),而不是从云提供商那里租用多个 VM。这反过来又允许您创建多个运行不同操作系统的嵌套访客(2 级访客),您可以在这些访客上开发和测试您的软件。

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

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

  • 某些操作系统在内部使用虚拟化来实现安全性(例如,让应用程序安全地隔离运行)。

启用“嵌套” (x86)

从 Linux 内核 v4.20 起,Intel 和 AMD 的 nested KVM 参数默认启用。(尽管您的 Linux 发行版可能会覆盖此默认设置。)

如果您运行的是 v4.19 之前的 Linux 内核,要启用嵌套,请将 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 参数互斥——即,要启用 nestedhpage 参数必须禁用。

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

  2. 现在 KVM 模块可以加载到 L1(访客管理程序)中

    $ modprobe kvm
    

使用嵌套 KVM 进行实时迁移

在 Linux 内核 5.3 和 QEMU 4.2.0 for Intel x86 系统中,以及更早版本的 s390x 系统中,将 L1 访客(其中包含一个实时嵌套访客)迁移到另一台裸机主机是可行的。

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

迁移 L2 访客始终有望成功,因此以下所有场景即使在 AMD 系统上也应该有效:

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

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

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

报告嵌套设置中的错误

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

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

  • 确保您实际正在 KVM 上运行 KVM。有时人们没有为他们的访客管理程序 (L1) 启用 KVM,这导致他们使用纯模拟或 QEMU 称之为“TCG”的方式运行,但他们认为他们正在运行嵌套 KVM。因此,这混淆了“嵌套虚拟化”(也可能意味着 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 中的信息