头图

当开发者将工作负载部署到云计算平台时,往往并不需要考虑运行这些服务的底层硬件。在人们对 “云” 的理想化印象中,硬件维护和物理限制往往是无形的,然而硬件不可避免需要时不时进行维护,这可能会导致停机。为避免这样的停机时间被转嫁给我们的客户,并真正实现云的承诺,Linode 提供了一种名为实时迁移(Live Migration)的工具。

借助实时迁移技术,Linode 实例可在不中断服务的前提下在不同物理服务器之间移动。通过实时迁移工具移动 Linode 实例时,迁移过程对 Linode 实例中运行的进程是完全不可见的。如果一台主机的硬件需要维护,即可通过实时迁移,将该主机上的所有 Linode 实例无缝转移到另一台主机中。迁移操作完成后,即可开始修理物理硬件,全过程中并不产生会影响到客户的停机时间。

这几乎成为一种决定性的技术,也成为云技术和非云技术之间的转折点。本文我们将深入了解这项技术背后的细节。

实时迁移的工作原理

和大部分新项目类似,Linode 的实时迁移也是这样启动的:进行大量研究,创建一系列原型,获得同事和管理层的大量帮助。我们的第一步是调查 QEMU 如何处理实时迁移。QEMU 是 Linode 使用的一种虚拟化技术,而实时迁移也是 QEUM 的一项功能。因此我们团队的重点是将这项技术引入 Linode,而非重新发明一个类似的技术。

那么实时迁移技术到底是如何以 QEMU 的方式实现的?整个过程分为以下四步:

  1. 启动目标 QEUM 实例,该实例的各项参数与需要迁移的源 QEUM 实例完全相同。
  2. 对磁盘进行实时迁移。数据传输过程中,对磁盘内容进行的任何更改也会提交至目标磁盘。
  3. 对内存数据进行实时迁移。迁移过程中, 内存内容的任何变化也会提交至目标内存。如果这一过程中磁盘内容也出现了变化,相关变化同样会被提交至目标 QEUM 实例的磁盘中。
  4. 执行割接点。当 QEMU 确认有足够多的内存页可以放心进行割接后,源和目标 QEMU 实例将会暂停。QEMU 会复制最后几页内存数据和机器状态,机器状态包括 CPU 缓存和下一条 CPU 指令。随后,QEMU 会让目标开始运行,这样目标实例就可以从源实例停止时的状态恢复运行了。

这些步骤概括介绍了 QEMU 实时迁移的执行过程。然而依然需要通过包含很多手工操作的方式来精确指定目标 QEMU 实例的启动方式。此外,上述过程中的每个操作都必须在正确的时间执行。

图片

Linode 实现实时迁移的方式

在分析过 QEMU 开发者已经实现的技术后,我们该考虑具体用怎样的方式将其实给 Linode。这个答案恰恰是我们工作的重中之重。

在实时迁移工作流程的第 1 步,需要启动目标 QEMU 实例以接受传入的实施迁移连接。在实现这一步时,我们最初的想法是拿到当前 Linode 实例的配置文件,随后将其应用到目标计算机。理论上这应该很简单,但进一步思考就会发现,实际情况要复杂很多。尤其是,配置文件虽然可以告诉我们 Linode 实例是如何启动的,但并不一定可以完整描述启动后的 Linode 实例的完整状态。例如,用户可以在 Linode 实例启动完毕后通过热插拔的方式连接块存储设备,但这种情况并不会记录到配置文件中。

为了在目标主机上创建 QEMU 实例,必须对当前运行的 QEMU 实例进行剖析。我们通过检查 QMP 接口的方式对运行中的 QEMU 实例进行剖析,该接口为我们提供了与 QEMU 实例布局情况有关的丰富信息,但它无法帮助我们从来宾系统的视角了解实例内部正在发生的事情。例如,对于本地 SSD 和块存储,它只能告诉我们磁盘链接到哪里,以及虚拟磁盘连接到哪个虚拟化 PCI 插槽上。在查询 QMP 以及检查并分析了 QEMU 接口后,可以构建一个 Profile 来描述如何在目标位置创建一个完全相同的实例。

在目标计算机上,我们将收到完整的描述信息,借此了解源实例到底是什么样,随后就可以在目标位置忠实重建这个实例,但此时还有一个差异。这个差别主要在于,目标 QEMU 实例在启动时使用了一个选项,该选项可以让 QEMU 接受传入的迁移。

至此,实时迁移的记录过程已经基本结束,接下来需要看看 QEMU 是如何实现这些操作的。QEMU 进程树由一个控制进程和多个工作进程组成,其中一个工作进程负责返回 QMP 调用或处理实时迁移等任务,其他进程需要一对一映射至来宾 CPU。来宾环境与 QEMU 端的功能相互隔离,具体行为类似于独立的系统。

从这个意义来看,我们需要处理三层内容:

  • 第 1 层是管理层;
  • 第 2 层是 QEMU 进程的一部分,负责处理所有操作;
  • 第 3 层是实际的来宾层,负责与 Linode 用户进行交互。

图片

目标实例启动并准备好接受传入的迁移后,目标硬件会告知源硬件开始发送数据。源端会在收到这个信号后开始进行处理,并会在软件中告知 QEMU 开始传输磁盘内容。软件会自主监控磁盘传输进度,借此检查传输操作是否完成,并会在磁盘传输完成后自动开始迁移内存内容。此时软件依然会自主监控内存迁移进度,并在内存迁移完毕后自动切换至割接模式。上述全过程都是通过 Linode 的 40Gbps 网络进行的,因此网络方面的操作都可以快速完成。

割接:关键环节

割接操作是实时迁移过程中最重要的一环,只有理解了它,才能完全理解实时迁移操作。

在割接点状态下,QEMU 已经确认做好了所有准备,可以进行割接并在目标计算机上运行。源 QEMU 实例会让两端暂停运行,这意味着:

  1. 来宾系统被 “时停”。如果来宾系统正在运行时间同步服务(如 NTP),NTP 会在迁移完成后自动重新同步时间。这是因为系统时钟会产生几秒钟的落后。
  2. 网络请求停止。如果网络请求是 TCP 请求(如 SSH 或 HTTP),基本上不会产生可感知的连接中断;如果网络请求是 UDP 请求(如流媒体视频),可能会导致少量丢帧。 

图片

由于时间和网络请求均已停止,我们希望割接能尽量快速完成。然而为保证成功割接,还需要进行一些检查:

  • 确保实时迁移顺利完成不出错。如果出错则要进行回滚,解除源 Linode 实例的暂停状态,不再进一步执行其他操作。开发过程中,我们在这方面进行了大量实验并解决了很多错误,虽然这为我们造成了很多头疼的问题,但最终都顺利解决了。
  • 确保关闭源实例的网络,并在目标实例上正确连接。
  • 让我们的其余基础设施清楚得知迁移后的 Linode 实例是通过哪台物理计算机运行的。

由于割接过程时间有限,我们希望能尽快完成上述操作。解决了这些问题后,即可继续进行割接了。源 Linode 实例会自动会自动收到 “割接完成” 信号并让目标实例运行起来。目标 Linode 实例会从源实例暂停时的状态恢复运行。源和目标实例上的其余内容则会被清理。如果目标 Linode 实例在未来某个时间需要再次进行实时迁移,则会重复执行上述步骤。

边缘案例概述

实时迁移的大部分过程都是直接实现的,但考虑到边缘案例后,该功能本身的开发也进行了大量扩展。这个项目的顺利完成很大程度上要归功于管理团队,他们坚信该工具有着极大的愿景,并提供了完成该任务所需的各项资源,另外当然也离不开坚信该项目能够成功完成的大量员工。

我们在下列这些领域遇到了很多边缘案例:

  • 通过开发内部工具为 Linode 客户支持人员和硬件运维团队进行与实时迁移有关的协调工作。这些工具与我们当时正在使用的其他同类工具较为类似,但也有些许差异,我们为此投入了大量开发工作:

    • 该工具必须能自动检查数据中心内部的所有硬件设施,进而确定哪台主机可以成为每个需要迁移的 Linode 实例的最佳目标。在进行这种选择和决策时,考虑的相关规格包括可用 SSD 存储空间及内存分配情况等。
    • 目标计算机的物理处理器必须与传入的 Linode 实例相兼容。尤其是 CPU 必须具备用户所运行的软件必不可少的某些功能(我们将其称之为 CPU 标记)。例如,AES 就是这样的一种功能,该功能提供了基于硬件加速的加密能力。实时迁移的目标计算机 CPU 必须支持与源计算机相同的 CPU 标记。我们发现这是一种极为复杂的边缘用例,下文将介绍我们所采用的方法。 
  • 优雅地处理故障,包括最终用户的干预或实时迁移过程中网络连接丢失等情况。下文还将详细介绍这些信息。
  • 紧跟 Linode 平台自身的变化,而这是一个持续不断的长期过程。对于 Linode 平台当前和未来所支持的每个功能,我们都需要确保这些功能可与实时迁移兼容。详细信息请继续阅读下文。

CPU 标记

在向来宾操作系统呈现 CPU 方面,QEMU 有不同的选项。其中一个选项可将主机 CPU 的型号和功能(即 CPU 标记)直接传递给来宾系统。通过使用该选项,来宾即可不受约束地使用 KVM 虚拟化系统所支持的全部能力。当 Linode 首次采用 KVM 时(当时还没有实时迁移功能),为了实现最大化性能,我们就使用了该选项。然而在开发实时迁移功能的过程中,该选项为我们造成了很多挑战。

图片

在实时迁移的测试环境中,源和目标主机是两台完全相同的计算机。但在现实世界中,我们的硬件集群并非 100% 完全相同的,计算机之间的某些配置差异可能导致产生不同的 CPU 标记。这很重要,因为当一个程序被载入 Linode 的操作系统后,Linode 会向该程序呈现 CPU 标记,为了充分利用这些标记,程序可以将软件中的特定部分载入内存。如果一个 Linode 实例被实时迁移到不支持该 CPU 标记的目标计算机,程序将会崩溃。这可能导致来宾操作系统崩溃,甚至导致 Linode 重启动。

我们发现有三个因素会影响到计算机的 CPU 标记如何呈现给来宾系统:

  • 取决于购买时间,不同 CPU 之间会存在一些细微差异。取决于 CPU 制造商何时发布新硬件,年底购买的 CPU 可能与年初购买的具备不同标记。为了扩容,Linode 会持续购买新硬件,但就算两个硬件订单购买了同款 CPU,它们的 CPU 标记也可能存在差异。
  • 不同的 Linux 内核可能会向 QEMU 传递不同的标记。尤其是,参与实时迁移的源计算机的 Linux 内核可能会向 QEMU 传递与目标计算机的 Linux 内核不同的标记。为源计算机更新 Linux 内核必须重启动,因此无法在实时迁移之前通过升级内核版本的方式解决这种不匹配问题,因为这会导致计算机上的 Linode 实例停机。
  • 同理,不同的 QEMU 版本也会影响所呈现的 CPU 标记。更新 QEMU 同样需要重启动计算机。

因此在实现实时迁移时,我们必须设法防止程序因为 CPU 标记的不匹配而崩溃。可行的选项有两个:

  • 让 QEMU 模拟 CPU 标记。但这可能导致原本快速运行的软件运行速度变慢,并且完全无法调查原因。
  • 收集源计算机的 CPU 标记列表,确保目标计算机具备完全相同的标记,随后再进行迁移。这种方式更复杂,但不会影响用户程序的运行速度。我们最终选择了这种方式。

在决定对源和目标的 CPU 标记进行匹配后,我们使用下列两种方法的组合最终实现了目标:

  • 第一种方法更简单。将源硬件的所有 CPU 标记发送给目标硬件,当目标硬件设置新的 QEMU 实例时,会通过检查来确保自己至少拥有和源 Linode 实例相同的标记。如果不匹配,将不进行实时迁移。
  • 第二种方法更复杂,但可以避免因为 CPU 标记不匹配导致的迁移失败。在发起实施迁移前,我们会对具备可兼容 CPU 标记的硬件创建一个列表,随后从该列表中选择硬件来创建目标计算机。

第二种方法必须能快速执行,并且让我们的工作变得更复杂。某些情况下,我们需要针对超过 900 台计算机检查最多 226 个 CPU 标记。为所有这 226 个 CPU 标记编写检查代码本就很困难,而这些代码还需要不断进行维护。但 Linode 的创始人 Chris Aker 提出的一个惊人想法最终解决了这个问题。

方法的关键在于为所有 CPU 标记创建一个列表,并将其表示为一个二进制字符串。随后,可以使用 Bitwise and(“按位与”)运算来对比字符串。我们可以用下面这个简单的例子来演示这个算法。下面这段 Python 代码可以使用 “按位与” 对比两个数:

>>> 1 & 1
1
>>> 2 & 3
2
>>> 1 & 3
1

要理解为何 “按位与” 运算能产生这样的结果,首先需要将数字用二进制形式表示。一起看看十进制的 “2” 和 “3” 在用二进制形式表示的情况下,“按位与” 是如何处理的:

>>> # 2: 00000010
>>> # &
>>> # 3: 00000011
>>> # =
>>> # 2: 00000010

“按位与” 会对比二进制的 “数”,也就是两个不同数字中的 “位”。该操作会从上述数字最右边的位开始向左处理:

  • “2” 和 “3” 最右侧(第一)位分别是 “0” 和 “1”,0 & 1 的按位与结果为 “0”。
  • “2” 和 “3” 右数第二位都是 “1”,1 & 1 的按位与结果为 “1”。
  • 这两个数的所有其他位都是 “0”,0 & 0 的按位与结果为 “0”。

因此完整结果的二进制表示就是 00000010,也就是十进制的 “2”。

对于实时迁移,CPU 标记完整列表会表示为一个二进制字符串,其中每一位都代表一个标记。如果一个位为 “0”,代表对应的标记不存在;如果某个位为 “1”,则代表标记存在。例如,一个位可以代表 AES 标记,另一个位可以代表 MMX 标记。这些标记在二进制字符串中的位置会维护并记录在案,随后用于我们数据中心内的所有计算机。

相比维护一组 if 语句来检查某个 CPU 标记是否存在,这种列表的维护工作无疑更简单也更高效。例如,假设总共需要追踪并检查 7 个 CPU 标记,这些标记可以存储在一个 8 位数字中(多出的一位供未来进行扩展)。例如这样的字符串可能类似于 00111011,最右侧的一位代表 AES 已启用,右数第二位代表 MMX 已启用,右数第三位代表其他标记已启用,以此类推。

在下文的代码片段中,我们可以看到哪些硬件支持这些标记的组合,这段代码可以在一个周期内返回所有匹配的结果。如果用一组 if 语句进行对比,则需要更多周期才能获得所需结果。如果进行实时迁移的源计算机包含 4 个 CPU 标记,这种情况下就需要 203400 个周期才能找到匹配的硬件。

实时迁移操作会在源和目标计算机上针对 CPU 标记字符串执行 “按位与” 操作。如果两个计算机的 CPU 标记字符串运算结果相等,意味着目标计算机是兼容的。该过程可参考下列 Python 代码片段:

>>> # The b'' syntax below represents a binary string
>>>
>>> # The s variable stores the example CPU flag 
>>> # string for the source:
>>> s = b'00111011'
>>> # The source CPU flag string is equivalent to the number 59:
>>> int(s.decode(), 2)
59
>>> 
>>> # The d variable stores the example CPU flag 
>>> # string for the source:
>>> d = b'00111111'
>>> # The destination CPU flag string is equivalent to the number 63:
>>> int(d.decode(), 2)
63
>>>
>>> # The bitwise and operation compares these two numbers:
>>> int(s.decode(), 2) & int(d.decode(), 2) == int(s.decode(), 2)
True
>>> # The previous statement was equivalent to 59 & 63 == 59.
>>>
>>> # Because source & destination == source, 
>>> # the machines are compatible

请注意,在上述代码片段中,目标计算机比源计算机支持更多的标记。此时可以认为目标计算机是兼容的,因为源的所有 CPU 标记都已包含在目标计算机中,这一点可以由 “按位与” 运算提供保证。

我们的内部工具可以使用上述算法得到的结果为可兼容的硬件构建一个列表。该列表会展示给我们的客户支持和硬件运维团队,这些团队可以使用我们的内部工具来编排不同的运维任务:

  • 使用该工具为特定 Linode 实例挑选兼容性最高的硬件。
  • 在不指定目标的情况下发起 Linode 实例实时迁移。随后,同一数据中心内兼容性最高的硬件会被自动选中并立即开始迁移。
  • 通过一个任务,对一台主机上的所有 Linode 实例发起实时迁移。针对主机执行维护任务前通常会执行这样的操作。我们的工具会自动为所有实例选择目标,并为每个实例进行必要的编排。
  • 将需要维护的多台计算机指定为一个列表,我们的工具可以跨越所有主机,自动为所有实例编排实时迁移。

仅仅为了让软件能够 “跑起来”,就需要进行大量的开发工作……

失败处理

软件领域有一个话题很少被人讨论:优雅地处理失败。软件至少应该能够 “跑起来”。为了实现这一点,往往需要进行大量开发工作,实时迁移功能的开发也是如此。我们花了很多时间考虑如果该工具无法正常运行要怎么办,以及该如何优雅地处理这种情况。我们考虑了很多场景,并确定了具体的应对方式:

  • 如果客户希望从 Cloud Manager 访问 Linode 的某项功能该如何处理?例如用户可能会重启动 Linode 或为实例连接 Block Storage Volume。

    • 解决方法:客户完全可以这样做。实时迁移会被打断,无法继续处理。这种处理方法是合适的,因为实时迁移可以稍后重试。
  • 目标 Linode 启动失败怎么办?

    • 解决方法:通知源硬件,通过专门设计的内部工具在数据中心内自动选择另一个硬件。此外还将通知运维团队,以便调查失败的目标硬件。这种情况曾在生产环境中发生过,我们的实时迁移可以顺利应对。
  • 如果迁移过程中网络连接丢失该怎么办?

    • 解决方法:自主监测实时迁移进度,如果过去一分钟内没有产生任何进展,将会取消实时迁移并通知运维团队。这种情况在测试环境之外从未发生过,但我们也针对这种场景做好了充分准备。
  • 如果互联网的其他部分断开了,但源和目标硬件依然在运行和通信,并且源和目标 Linode 实例都在正常运行,此时会发生什么情况?

    • 解决方法:如果实时迁移未进行到关键环节,将停止实时迁移,并在稍后重试。
    • 如果已进行到关键环节,则将继续迁移。这一点很重要,因为源 Linode 已被暂停,目标 Linode 需要处于启动状态才能继续恢复操作。

这些场景都已在测试环境中进行了模拟,我们认为上述行为也是不同情况下的最佳应对措施。

紧跟技术变化的步伐

成功进行了数十万次实时迁移后,我们不免会考虑:“实时迁移的开发工作何时才能结束?” 随着时间推移,实时迁移这项技术的使用范围会越来越广,并且会不断完善,因此该项目似乎会永远持续下去。回答这个问题的一种方法是考虑该项目的大部分工作什么时候会结束。答案也很简单:为了获得可靠、可信赖的软件,我们的工作还将持续很久。

随着时间推移,Linode 会增加新的功能,我们也许要继续努力保证实时迁移可以兼容这些功能。引入某些新功能时,可能无需围绕实时迁移执行新的开发工作,但我们可能依然需要测试该功能是否可以按照预期正常工作。对于某些功能,则可能需要在开发的早期阶段,针对实时迁移进行必要的兼容性测试和相关工作。

和其他几乎所有软件类似,对于同一件事,通过不断研究,总能发现更好的实现方法。例如,从长远来看,为实时迁移功能开发更多模块化的集成方法,无疑可以降低维护负担。或者我们甚至可能将实时迁移的相关功能纳入到底层代码中,从而使其成为 Linode 一项拆箱即用的功能。

我们的团队已经考虑过所有这些选项,并且坚信驱动 Linode 平台的工具是活跃的,还会继续努力,使其不断进化和发展。


Akamai
1 声望1 粉丝

Akamai 支持并保护网络生活。全球各大优秀公司纷纷选择 Akamai 来打造并提供安全的数字化体验,为数十亿人每天的生活、工作和娱乐提供助力。 我们横跨云端和边缘的计算平台在全球广泛分布,不仅能让客户轻松开发...