内核虚拟化:实时迁移QEMU-KVM虚拟机

本文翻译于:Live Migrating QEMU-KVM Virtual Machines

实时迁移QEMU-KVM虚拟机

虚拟机的实时迁移是虚拟化领域一个持续受关注的话题:虚拟机的规模不断增大(更多的虚拟CPU、更多的内存),对虚拟机的运行时间要求也变得越来越严格(在虚拟机从一个主机迁移到另一个主机时不能出现长时间的暂停)。

本讨论将从早期QEMU/KVM虚拟化管理程序中实时迁移的简单设计开始,介绍其经过调整和优化的过程,以及当前的状态和未来的发展方向。文章将探讨实时迁移的实际工作原理,以及其中必须遵循的约束条件,以及为满足最新要求而不断需要新思路的设计。

虚拟化

virt-stack

让我们从虚拟化的概览开始,如上图。用户通过几种可用的接口之一与虚拟机进行交互,例如virt-manager、oVirt、OpenStack或Boxes。这些软件进而与libvirt进行交互,libvirt提供了一个与虚拟机管理相关的、与虚拟化管理程序无关的API。libvirt还具有用于与各种块/存储和网络配置进行交互的API。

对于QEMU/KVM虚拟机,libvirt使用QEMU提供的API与QEMU进行通信。在主机上创建的每个虚拟机都有自己的QEMU实例。客户机作为QEMU进程的一部分运行。主机的每个客户机vCPU在top(1)和ps(1)输出中被视为一个单独的线程。

QEMU与Linux接口进行交互,特别是与Linux内核中的KVM模块进行接口交互,以直接在物理硬件上运行虚拟机(而不是由QEMU模拟)。

QEMU

  • 创建机器

  • 设备仿真代码

    • 模拟真实设备

    • 特别的:半虚拟化

  • 使用来自主机内核的几个服务

    • 用于来宾控制的KVM

    • 网络

    • 磁盘IO

QEMU是实际创建虚拟硬件的软件,虚拟机的操作系统运行在这些虚拟硬件之上。所有这些虚拟硬件都是由代码创建的,取决于QEMU的配置。客户操作系统看到的所有设备,如键盘、鼠标、网络卡等,都是QEMU项目中的代码实例。许多这些设备都基于发布的用于现有物理硬件的规范,例如e1000系列网络设备。未经修改的客户操作系统可以通过它们已经附带的驱动程序来识别这些设备。

除了模拟真实硬件外,QEMU还创建了一些专门用于虚拟化用例的设备。对于QEMU/KVM虚拟机,我们使用virtio框架来创建这些设备。我们有virtio-net网络设备、virtio-blk块设备、virtio-scsi SCSI设备、virtio-rng随机数生成设备等。半虚拟化设备的好处在于它们是针对虚拟化而设计的,因此通常比模拟真实设备更快速、更容易管理。

QEMU还与其他项目进行交互,例如提供客户端BIOS服务的SeaBIOS。

为了运行,QEMU使用了主机Linux内核的几个服务,例如使用KVM API进行客户控制,使用主机的网络和存储设施等。

KVM

  • 遵循"专一做一件事,做到最好"的原则

  • Linux内核模块

  • 将硬件虚拟化的功能暴露给用户空间

  • 为QEMU虚拟化软件提供所需的功能

    • 比如跟踪客户机修改的页面

KVM是一个小型的内核模块,它为Linux内核启用了硬件虚拟化功能。这段代码负责将Linux内核转换为一个虚拟化管理程序(hypervisor)。KVM的编写遵循典型的Linux或UNIX风格:做一件事,并且做到最好。它将内存管理、进程调度等决策留给了Linux内核。这意味着对Linux内存管理器的任何改进都会立即使得虚拟化管理程序受益。

KVM通过ioctls向用户空间暴露其API,而QEMU是这些服务的用户之一。KVM向用户空间暴露的功能之一是跟踪客户机内存区域中自上次请求此类数据以来客户机已修改的页面。这个特性在这里被强调,因为在实时迁移过程中使用该特性,下面将会对此进行详细说明。

实时迁移/热迁移

  • 从一个QEMU进程中提取客户机状态,并在客户机运行时将其传输到另一个进程中。

  • 在此过程中,客户机不应意识到其所处的环境正在发生变化,换句话说,客户机不参与此过程。尽管如此,客户机可能会注意到性能下降。

  • 这种方法对于负载平衡、硬件/软件维护、节能、快照等方面非常有用。

实时迁移意味着在客户机仍在运行时,将其从一个虚拟化管理程序(hypervisor)/ QEMU进程移动到另一个,并使其在另一个上运行。客户机继续正常工作,甚至不会意识到虚拟化管理程序的更改正在生效。实时迁移通常涉及将整个客户机从一个物理主机迁移到另一个物理主机。这意味着涉及许多变量:网络带宽、网络延迟、存储可用性等。

在客户机必须停止以便新的虚拟化管理程序接管并开始运行客户机时,存在一个小窗口。这可能导致客户机应用程序性能下降,这是客户机对迁移过程唯一的暴露。

实时迁移有许多用途:在服务器群中平衡负载、关闭一些主机进行硬件或软件维护而保持客户机运行、节能:如果群集中的一些主机负载较轻,并且可以处理更多的虚拟机,而不会影响服务,则将虚拟机迁移到负载较轻的服务器并关闭未使用的服务器。还可以对虚拟机进行快照以重放特定流量或负载,以检查负载的可重复性或虚拟机内部软件调试。虽然快照的这种用法与实时迁移无关,但用于制作快照的代码在QEMU内部与实时迁移代码共享。

QEMU布局

qemu-decomposed.svg

现在让我们来看看迁移过程涉及的内容。有一些状态我们需要按原样迁移,也就是不解释内存区域内容的任何内容 - 整个客户机。在迁移代码中,客户机被视为一个二进制大块,我们只是将其内容发送到目的地。这是上图中标记为灰色的区域。

图表最左侧的部分是客户机可见设备状态。这是我们通过自己的协议发送的QEMU内部状态。这包括已向客户机公开的每个设备的设备状态。

图表最右侧的部分是在迁移过程中不涉及的QEMU状态,但在开始迁移之前正确设置这些状态非常重要。源和目标主机上的QEMU设置必须完全相同。通过在两个主机上使用类似的QEMU命令行来实现这一点。由于QEMU命令行可能因涉及的各种选项而变得非常笨重,因此我们使用libvirt来为我们解决这个问题。libvirt确保两个QEMU进程为迁移正确设置。

在接下来的部分中,我们将更详细地探讨这三个部分。

正确的配置

  • 共享存储

    • NFS 设置

  • 主机时间同步

    • 无法强调这一点的重要性!

  • 网络配置

  • 主机 CPU 类型

  • 客户机类型

    • 特别是在跨 QEMU 版本迁移时

    • ROM 大小

在源主机和目标主机上获得相同的设置对于迁移成功至关重要。在相同方式配置两个虚拟机听起来很容易,但有些细节并不明显。

虚拟机使用的存储必须是共享的,并且对源主机和目标主机都可访问。此外,共享方式很重要,即使用的文件系统必须配置为禁用缓存(否则,源中的数据可能在目标开始运行并访问存储之前尚未到达存储,导致客户机看到数据损坏)。

两台服务器的挂钟时间必须匹配。NTP 可以帮助实现这一点,但配置正确的时区并确保两台主机上的时间相同至关重要。我们曾收到过几个错误报告,迁移最初成功,但后来客户机变得迟钝或完全停止运行;而将问题追溯到主机上的时间差有时会非常困难。Windows 客户机特别容易受到此类配置错误的影响。

两台主机上的网络配置必须匹配;如果客户机之前与某些服务器通信,则迁移后其对这些服务器的访问必须保持不变。防火墙配置也对此起作用。此外,最好将存储网络和迁移网络分开:迁移将更快速,可用带宽越多,因此最好将存储 IO 排除在该网络之外。

主机 CPU 类型必须匹配。源上为客户机公开的指令集必须完全可用于目标上的客户机。这意味着从较新一代处理器迁移到较旧一代处理器可能不起作用,除非注意启动具有受限指令集的 VM。操作系统(以及一些应用程序)在启动时查询 CPU 支持的指令。之后,它们将根据需要使用所有可用的指令。因此,在选择要向客户机公开的 CPU 模型时必须小心,这是服务器群中最常见的分母。

QEMU 维护着向客户机公开的机器类型列表。这是由为客户机公开的各种设备的兼容性代码组成的。这是必要的,以确保 QEMU 随着错误修复和功能的提升不断发展,同时保持与先前 QEMU 版本的错误兼容性,以确保迁移继续工作。此版本化的另一个方面是由 QEMU 向客户机提供的各种 ROM,如 BIOS、iPXE ROM 等。这些 ROM 也必须在源和目标主机之间保持兼容。

实时迁移的阶段

  • 实时迁移分为三个阶段

  • 阶段1:将所有RAM标记为脏<ram_save_setup()>

  • 第2阶段:自上次迭代以来一直发送脏RAM页面 <ram_save_iterate()>

    • 达到某个低水位线或条件时停止

  • 阶段3:停止客户机,转移剩余的脏RAM,设备状态<migration_thread()>

    • 在目标qemu上继续执行

现在让我们讨论实际的迁移过程。这部分涉及图表中的中间部分和最左侧部分。迁移过程分为三个阶段。前两个阶段处理图表中的灰色区域,而最后一个阶段则处理图表中的灰色区域和最左侧区域。

我们从阶段1开始,将灰色区域中的每一页标记为“脏”或“需要迁移”。然后,我们将所有标记为脏的页面发送到目标主机。这就是阶段1的全部内容。

还记得我们之前指出的 KVM 向用户空间提供的服务吗?就是用户空间(如 QEMU)能够找出自上次请求数据以来已更改的页面。这就是我们如何保持仅发送自上次迭代以来客户机更改的页面的方式。这就是阶段2。我们会在此阶段停留,直到达到某些条件。我们稍后会讨论这些不同的条件是什么。很容易推断出,在实时迁移客户机时,大部分时间都是在这个阶段花费的。

阶段3是我们转向非实时或离线迁移的阶段:客户机实际上被暂停,并且不再继续进行。然后,我们传输所有剩余的脏内存和设备状态:图表中最左侧的区域。

灰色区域对于 QEMU 来说只是不透明的数据,按原样传输。设备状态由 QEMU 中的特殊代码处理,稍后将进行讨论。

处理每个阶段的 QEMU 源代码中的函数名是 migration_thread()。migration_thread() 函数处理整个迁移过程,并调用其他函数,因此这是开始理解迁移代码的地方。

从实时状态过渡到离线状态

  • 之前:

    • 剩余需要迁移的脏页面少于50页

    • 连续2个迭代没有进展

    • 已经过了30个迭代

  • 现在:

    • 管理员可配置的客户机停机时间

    • 需要了解剩余页面数和可用带宽

    • 主机策略:比如主机必须在5分钟内关闭,在此期间内迁移所有虚拟机

何时从第2阶段过渡到第3阶段是一个重要的决定:在第3阶段期间,客户机会被暂停,因此希望在第3阶段尽可能少地迁移页面,以减少停机时间。

QEMU 中最初的迁移实现相当简单:如果在第2阶段剩余需要迁移的脏页面少于50页,我们将转入第3阶段。或者当连续的一定次数的迭代没有向减少脏页面数量的目标取得任何进展时。

这种方法最初效果不错,但后来必须添加了几个新的限制。在 KVM 上运行其客户的工作负载的客户必须提供一些 SLA,包括最大可接受的停机时间。因此,我们在代码中添加了一些可调整参数,使得从第2阶段过渡到第3阶段的条件可以由客户和主机管理员配置。

客户管理员可以指定最大可接受的停机时间。在我们的第2阶段代码中,我们检查每次迭代中客户端生成的脏页面数量以及传输页面所需的时间,这给我们提供了网络带宽的估计。根据此带宽估计和当前迭代的脏页面数量,我们可以计算出传输剩余页面所需的时间。如果此时间在可接受或配置的停机时间限制内,则过渡到第3阶段。否则,我们继续处于第2阶段。当然,我们仍然有其他可调参数,指定在第2阶段尝试收敛的持续时间。

还有主机管理员可配置的参数。一个说明为何需要这些可调参数的有趣用例是由日本一位客户在 2013 年 KVM 论坛的小组讨论中提出的。日本的数据中心会收到地震警报;在某些情况下,数据中心会在预定的时间失去电源,比如从警报时间算起的30分钟后。服务提供商在此30分钟的时间内需要将所有的虚拟机从一个数据中心迁移到另一个数据中心,否则所有的虚拟机将因为停电而丢失。在这种情况下,客户的 SLA 的优先级较低。

QEMU 中的其他迁移代码

  • 发送数据的通用代码

    • tcp,unix,fd,exec,rdma

  • 序列化数据的代码

    • 段开始/停止

  • 设备状态

在 QEMU 中还有与迁移相关的一些代码:针对用于发送/接收迁移数据的传输机制(如 TCP 或 UNIX 套接字、本地文件描述符或 RDMA)编写了特定的代码。此外,还有一个名为 'exec' 的功能,通过该功能,QEMU 中的数据可以在发送到目标之前被传输到主机上的其他进程。这对于压缩出站数据或加密出站数据非常有用。在目标位置,必须应用反向过程进行解压缩或解密。对于基于 UNIX 套接字、fd 或 exec 的协议,需要一个在两端都管理迁移的更高级别的程序。libvirt 在这方面表现出色,我们依赖 libvirt 的能力来处理迁移。

此外,还有与数据序列化相关的代码;代码指示部分或页面的起始和结束位置等。

还有与设备相关的代码,我们将在下面讨论。

VMState

  • 描述性设备状态

  • 每个设备都不需要模板代码

  • 每个设备不需要相同的保存和加载代码

    • 这很容易出错

设备状态,即图表中最左侧的块,对于每个设备都是高度特定的。块设备将具有仍需刷新到磁盘的在途数据的列表和计数。此外,它的状态的一部分是客户端已配置或选择使用设备的方式。网络设备类似地也将有其自己的状态。几乎所有向客户端公开的设备都有一些需要迁移的状态。设备状态的迁移发生在将所有脏内存发送到目标之后的第3阶段。

直到不久前,每个设备都必须自己处理其状态的发送和接收部分。这意味着在 QEMU 中的所有设备之间存在大量的代码重复。VMstate 是新添加的基础架构,使得状态的发送和接收部分与设备无关。这可以通过一个例子来最好地说明:

VMState例子

  • e1000 device

  • e482dc3ea e1000: port to vmstate

  • 1 file changed,

  • 81 insertions(+),

  • 163 deletions(-)

让我们来看看将 e1000 设备从旧的状态迁移处理方式转换为 VMstate 的提交。插入了 81 行代码,删除了 163 行代码。这是一次胜利。

更新设备

  • 有时设备会出现导致迁移失败

  • 一个想法是提升版本号

    • 这会导致更高版本对较低版本的依赖

    • 难以将修复补丁应用到稳定版或下游版本

作为常规开发过程的一部分,会发现并修复错误,以及添加新功能。当新更改涉及设备状态时,必须非常小心以保持向后迁移兼容性。也就是说,从旧的 QEMU 版本迁移到新的 QEMU 版本应该继续工作。换句话说,新的 QEMU 版本应该继续接受来自旧的 QEMU 版本的迁移流,而不管状态的更改如何。

处理这种更改的经典方法是为每个设备在传输时进行版本化。源 QEMU 不需要担心目标运行的是哪个版本。纯粹由目标 QEMU 解释发送到它的各种格式。

目标 QEMU 对等的加载函数检查正在发送的设备状态的版本。根据版本,将传入的数据存储到目标设备结构中的相应状态变量中。这意味着所有与版本相关的复杂性存在于迁移过程的目标端。

当然,目标 QEMU 的版本低于源 QEMU,无法知道正在发送的内容,在这种情况下迁移将失败。

但是这种版本控制方案存在问题:想象一下 e1000 设备的状态是版本 2。现在我们添加了一个新功能,需要增加状态,将版本提升到 3。现在,如果我们发现版本 2 存在错误,我们将不得不修复该错误,并将版本更新为 4。然而,将这样的修复补丁回溯到稳定版本或不想引入新功能,而只想回溯修复的下游版本是困难的。没有办法回溯修复补丁,并保持版本号与上游一致。将版本提升到 4 是不正确的,因为版本 3 的更改还没有回溯。

发生的变化

  • 客户机变得更加庞大

    • 更大的RAM

      • 这意味着要花费大量时间传输页面

  • 更多的vCPUs

    • 这意味着活跃的虚拟机持续不断地使页面变脏

现在我们继续讨论一些 QEMU 迁移代码中最近的工作。随着 KVM 的成熟和越来越多的客户将其用于处理 KVM 虚拟化程序上的企业负载,我们看到了更大的虚拟机:分配更多内存,并向虚拟机暴露了更多 vCPU。大内存的虚拟机意味着在第二阶段需要传输的数据量更大;这反过来意味着遵循停机要求变得更加困难。更多的 vCPU 意味着虚拟机执行更多工作,并且会使更多的内存区域变脏,导致第二阶段的时间更长。

新特性

  • 自动收敛(autoconverge)

  • XBZRLE

  • 迁移线程(migration thread)

  • 迁移位图(migration bitmap)

  • RDMA

  • 块迁移(block migration)

Autoconverge:此设置可以暂停一些 vCPU,以使虚拟机在迁移的第二阶段不会取得太多进展,从而减慢迁移速度。这为脏页数量减少到我们的低水位条件提供了机会,以便转换到第三阶段。暂停虚拟机 vCPU 可以通过几种方式来实现:通过重新调整主机上的虚拟机 vCPU 线程(每个 vCPU 在主机上只是一个常规线程),或通过 cgroups 限制整个虚拟机作为整体所获得的主机 CPU 时间,或者完全不从 QEMU 代码中调度特定虚拟机 vCPU。

XBZRLE:此功能缓存了在上一次迭代中发送到目标的所有页面。对于要传输的每个页面,我们将当前页面与缓存中的页面进行比较。然后,只发送这些页面中字节的差异。这在某些工作负载中显著减少了通过网络传输的数据量,从而加快了迁移过程。

迁移线程:最初,QEMU 代码只有一个线程用于所有 QEMU 活动,并为虚拟机 vCPU 单独提供线程。这意味着 QEMU 中的任何活动都会阻塞所有其他活动。在迁移情况下,发送和接收数据时,QEMU 中无法发生任何其他活动。这也意味着与 QEMU 交互的应用程序无法查询 QEMU 在迁移进度中的进展情况。这种缺乏报告对管理员来说并不是一种愉快的体验,他们必须等待第二阶段的迭代完成,然后才能知道是否在取得任何进展。将迁移代码分离为自己的线程使得 QEMU 可以与迁移线程一起运行,并且一些瓶颈得到了缓解。

迁移位图:以前,每个脏页使用一个字节来跟踪。对于大内存虚拟机来说,这意味着位图变得太大,甚至比 CPU 缓存还大,并且在第二阶段的每个迭代中进行页行走操作变得非常缓慢。另一个缺点是,当由于特定主机上的低内存条件而触发迁移时,通过为迁移元数据分配更多内存来恶化情况。我们现在使用一个比特表示每个虚拟机页面,这将大大减少脏页位图的大小;以使其很好地适应 CPU 缓存,并且该过程不会变得太慢。

RDMA 传输:在源主机和目标主机之间使用更快的互连方式,如 Infiniband 设备,可以确保迁移数据的传输比以太网快得多。RDMA 代码仍然相当新,并且没有经过太多的测试,因此这是潜在贡献者可以寻找改进的地方。

块迁移:除了迁移状态外,一些客户需要使用非共享的块存储。在这种情况下,甚至存储也需要与虚拟机状态一起迁移。这些块数据庞大,对网络带宽以及完成迁移过程所需的时间都造成了很大的负担。

准备好的计划

  • postcopy

  • 可调试性

迁移代码中有大量工作正在进行,其中一个功能,后复制(postcopy),专注于使迁移的停机时间非常短。

我们之前描述的迁移过程是“预复制”模型:所有数据都被复制到目标主机上,然后再切换到目标主机上执行客户操作。在后复制模型中,我们尽快切换到目标主机:这意味着我们传输设备状态和一次脏页迭代,然后切换到目标主机上开始执行。当客户在其RAM中引用某个区域,而在目标主机上不存在相应页面时,远程页面故障会将页面从源主机传输到目标主机。很容易理解为什么这种方法在实现收敛速度上非常快。但它引入了额外的故障点:如果其中一个主机崩溃,或两者之间的网络链接中断,整个客户端都会丢失,因为其状态分布在两个主机上,无法再次调和。在预复制方案中,仅源主机的丢失意味着客户永远丢失。

整个迁移过程的可调试性一直是一个痛点:传输格式不具备自描述性;迁移失败会显示“设备加载失败;迁移中止”等信息,而不提供有关哪个设备的哪个部分加载失败以及原因的提示。现在正在进行一些工作来改善这一点,这是潜在贡献者可以参与的领域。这也是一个很好的机会,通过改进代码来学习代码。

未来的工作

  • 完成vmstate的转换

  • 自描述的传输格式

目前尚未有人着手完成vmstate转换工作。遗憾的是,并非所有设备都已转换为vmstate,其中最大的问题是virtio设备。完成这项工作至关重要,以确保代码的统一性。此外,还有一个静态检查器程序,可以自动检查各种QEMU版本之间的迁移兼容性,但仅适用于vmstate数据,而virtio设备目前无法从中受益。

另外,正如前文所述,自描述的传输格式将有助于调试以及在各种偏移处查看数据流、跳过部分等操作。目前还无法实现这一点。

这是关于QEMU-KVM虚拟机的实时迁移工作方式的简要概述。关于此处提到的各种优化的性能数据以及有关这些功能本身的更多讨论,可以在QEMU列表的电子邮件归档中找到。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值