跳到主要内容
博客凌码Linode的实时迁移

Linode的实时迁移

Linode实时迁移

当开发人员将工作负载部署到云计算平台时,他们往往不会停下来考虑其服务所运行的底层硬件。在理想化的 "云 "形象中,硬件维护和物理限制是不可见的。不幸的是,硬件偶尔需要维护,这可能会导致停机。为了避免将这种停机时间转嫁给我们的客户,并践行云计算的承诺,Linode 实施了一种称为实时迁移的工具。

实时迁移是一项技术,允许Linode实例在物理机之间移动而不中断服务。当一个Linode通过实时迁移被移动时,过渡对该Linode的进程是不可见的。如果一个主机的硬件需要维护,实时迁移可以用来将该主机的所有Linode无缝过渡到一个新主机。在这个迁移完成后,物理硬件可以被修复,而且停机时间不会影响到我们的客户。

对我来说,开发这项技术是一个决定性的时刻,是云技术和非云技术之间的转折点。我对实时迁移技术情有独钟,因为我花了一年多的时间来研究它。现在,我可以和大家分享这个故事了。

实时迁移是如何进行的

Linode的实时迁移像大多数新项目一样开始;有大量的研究,一系列的原型,以及许多同事和经理的帮助。第一个进展是调查QEMU如何处理实时迁移。QEMU是Linode使用的一种虚拟化技术,而Live Migrations是QEMU的一个功能。因此,我们团队的重点是将这项技术带到Linode,而不是发明它。 

那么,Live Migration技术是如何以QEMU的方式实现的呢?答案是一个四步过程:

  1. 目标qemu实例以与源qemu实例上存在的完全相同的参数被启动。
  2. 磁盘被实时迁移过来。在这个转移过程中,对磁盘的任何改变也会被告知。
  3. RAM是实时迁移过来的。任何对RAM页面的改变也必须被告知。如果在这个阶段也有任何磁盘数据的变化,那么这些变化也将被复制到目标QEMU实例的磁盘上。
  4. 切换点被执行。当QEMU确定有足够多的RAM页可以放心地进行切换时,源和目的QEMU实例就会暂停。QEMU复制最后几页RAM和机器状态。机器状态包括CPU缓存和下一条CPU指令。然后,QEMU告诉目标程序开始,目标程序就从源程序停止的地方开始工作。

这些步骤从高层次上解释了如何用QEMU进行实时迁移。然而,确切地指定你希望目标QEMU实例如何启动是一个非常手动的过程。而且,这个过程中的每个动作都需要在正确的时间启动。

实时迁移是如何在Linode实施的

在看了QEMU开发者已经创建的东西之后,我们如何在Linode利用这个东西?这个问题的答案是我们团队大部分工作的地方。 

根据 "实时迁移 "工作流程的第 1 步,启动目标 QEMU 实例,以接受传入的 "实时迁移"。在实施这一步时,我们首先想到的是将当前 Linode 的配置文件在目标机器上启动。这在理论上很简单,但进一步思考后会发现情况更为复杂。尤其是,配置文件会告诉你 Linode 是如何启动的,但并不一定能描述启动后 Linode 的完整状态。例如,用户可以通过将设备热插拔到 Block Storage设备,但这不会记录在配置文件中。

为了在目标主机上创建QEMU实例,必须对当前运行的QEMU实例进行剖析。我们通过检查QMP接口对当前运行的QEMU实例进行剖析。这个界面给我们提供了关于QEMU实例是如何布置的信息。它并不提供从客户的角度看实例内部正在发生什么的信息。它告诉我们磁盘插在哪里,虚拟磁盘插在哪个虚拟化的PCI插槽上,包括本地SSD和块存储。在查询了QMP并检查和内省了QEMU实例后,建立了一个配置文件,准确地描述了如何在目的地复制这个机器。

在目标机器上,我们收到源实例的完整描述。然后我们可以在这里忠实地重新创建这个实例,但有一点不同。不同的是,目标QEMU实例在启动时有一个选项,告诉QEMU接受传入的迁移。

在这一点上,我们应该从记录实时迁移中休息一下,转而解释一下QEMU是如何实现这些壮举的。QEMU的进程树是由一个控制进程和几个工作进程组成的。其中一个工作进程负责返回QMP调用或处理实时迁移等事务。其他进程一对一地映射到客户的CPU上。客人的环境与QEMU的这一边隔离,作为它自己的独立系统行事。 

在这个意义上,我们有3个层面的工作: 

  • 第1层是我们的管理层;
  • 2层是QEMU进程的一部分,为我们处理所有这些动作;以及
  • 第3层是Linode用户与之互动的实际客户层

在目的地启动并准备好接受传入的迁移后,目的地硬件让源硬件知道,源应该开始发送数据过来。源端一旦收到这个信号就会开始,我们在软件中告诉QEMU,开始磁盘迁移。软件会自主地监控磁盘的进度,以检查它何时完成。当磁盘完成后,软件会自动切换到RAM迁移。然后软件再次自主监控RAM迁移,并在RAM迁移完成后自动切换到切换模式。所有这些都是通过Linode的40Gbps网络进行的,所以网络方面的事情是相当快的。

切换:关键部分

切换步骤也被称为实时迁移的关键部分,理解这个步骤是理解实时迁移的最重要部分。 

在切换点,QEMU已经确定它已经准备好切换并开始在目标机器上运行。源QEMU实例指示双方暂停。这意味着几件事:

  1. 时间根据客人的情况停止。如果客户机正在运行像网络时间协议(NTP)这样的时间同步服务,那么NTP将在实时迁移完成后自动重新同步时间。这是因为系统时钟会落后几秒。
  2. 网络请求停止。如果这些网络请求是基于TCP的,如SSHHTTP,将不会有感知的连接损失。如果这些网络请求是基于UDP的,如实时流媒体视频,它可能会导致一些丢帧。

因为时间和网络请求都停止了,我们希望切换尽可能快地发生。然而,有几件事我们需要先检查一下,以确保切换的成功:

  • 确保实时迁移的完成没有错误。如果有错误,我们就回滚,取消源Linode的暂停,不要再继续下去了。这一点在开发过程中特别花了很多试验和错误来解决,它是很多痛苦的来源,但我们的团队最终找到了它的底部。 
  • 确保网络要在源头关闭,在目的地正确启动。 
  • 让我们基础设施的其他部分清楚地知道这个Linode现在驻扎在什么物理机器上。 

由于切换有时间限制,我们希望迅速完成这些步骤。在解决了这些问题后,我们完成了切换。源Lanode自动获得完成的信号,并告诉目的地开始。目的地的Linode会在它离开的地方继续工作。源节点和目的地节点上的任何剩余项目都被清理掉。如果目标节点在未来某个时间点需要再次进行实时迁移,可以重复这一过程。

边缘案例概述

这个过程的大部分都是直接实施的,但实时迁移的开发被边缘案例所扩展。完成这个项目的很大功劳要归功于管理团队,他们看到了完成工具的愿景,并分配了资源来完成任务,还有那些见证项目完成的员工。

以下是遇到边缘案例的一些领域:

  • 必须建立内部工具,为Linode客户支持和硬件运营团队协调实时迁移。这与我们当时拥有和使用的其他现有工具相似,但又有足够的不同,因此需要大量的开发工作来建立它:
    • 这个工具必须自动查看数据中心的整个硬件群,并找出哪个主机应该是每个实时迁移的Linode的目的地。在进行这种选择时,相关的规格包括可用的SSD存储空间和内存分配。
    • 目的地机器的物理处理器必须与传入的Linode兼容。特别是,CPU可以有用户的软件可以利用的功能(也被称为CPU标志)。例如,一个这样的功能是AES,它提供了硬件加速的加密。实时迁移的目的地的CPU需要支持源机器的CPU标志。这被证明是一个非常复杂的边缘案例,下一节将介绍这个问题的解决方案。
  • 优雅地处理故障情况,包括终端用户的干预或实时迁移过程中的网络损失。这些故障情况将在本篇文章的后面部分详细列举。
  • 跟上Linode平台的变化,这是一个持续的过程。对于我们现在和将来在Linode上支持的每一个功能,我们必须确保该功能与实时迁移兼容。这个挑战在本篇文章的末尾有描述。

CPU标志

QEMU对如何向客户操作系统展示CPU有不同的选择。其中一个选项是将主机CPU的型号和功能(也被称为CPU标志)直接传递给客户。通过选择这个选项,来宾可以使用KVM 虚拟化系统所允许的全部不受约束的能力。当KVM 最初被Linode采用时(在Live Migrations之前),选择这个选项是为了最大化性能。然而,这一决定后来在Live Migrations的开发过程中出现了许多挑战。

在实时迁移的测试环境中,源主机和目标主机是两台相同的机器。在现实世界中,我们的硬件机群并不是100%相同的,机器之间的差异会导致不同的CPU标志的出现。这很重要,因为当一个程序在Linode的操作系统内加载时,Linode会向该程序提出CPU标志,该程序会将软件的特定部分加载到内存中,以利用这些标志。如果一个Linode被实时迁移到一个不支持这些CPU标志的目标机器上,程序将崩溃。这可能导致客户操作系统崩溃,并可能导致Linode重新启动。 

我们发现有三个因素影响到机器的CPU标志如何呈现给客人:

  • CPU之间有微小的差异,这取决于CPU的购买时间。在年底购买的CPU可能与年初购买的CPU有不同的标志,这取决于CPU制造商何时发布新硬件。Linode不断购买新硬件以增加容量,即使两个不同硬件订单的CPU型号相同,CPU标志也可能不同。
  • 不同的Linux内核可能会向QEMU传递不同的标志。特别是,实时迁移的源机器的Linux内核可能会向QEMU传递与目标机器的Linux内核不同的标志。更新源机器上的Linux内核需要重新启动,所以这种不匹配不能通过在进行实时迁移之前升级内核来解决,因为这将导致该机器上的Linodes停机。
  • 同样地,不同的QEMU版本会影响到哪些CPU标志的呈现。更新QEMU也需要重新启动机器。

因此,实时迁移需要以一种防止CPU标志不匹配导致程序崩溃的方式来实现。有两个选项可供选择:

  • 我们可以告诉QEMU来模拟CPU的标志。这将导致过去运行速度快的软件现在运行速度慢,而且没有办法调查原因。
  • 我们可以在源程序上收集一个CPU标志的列表,并在继续进行之前确保目的地也有这些标志。这比较复杂,但它可以保持我们用户的程序速度。这就是我们为实时迁移实施的选项。

在我们决定匹配源和目的地的CPU标志后,我们用一种包括两种不同方法的皮带和吊带的方法完成了这项任务:

  • 第一种方法是两种方法中比较简单的。所有的CPU标志都从源站点发送到目标硬件。当目标硬件设置新的qemu实例时,它检查以确保它至少拥有源Linode上的所有标志。如果它们不匹配,实时迁移就不会进行。
  • 第二种方法要复杂得多,但是它可以防止因CPU标志不匹配而导致的迁移失败。在启动实时迁移之前,我们创建一个具有兼容CPU标志的硬件列表。然后,从这个列表中选择一个目标机器。

这第二种方法需要快速执行,而且它带有很大的复杂性。在某些情况下,我们需要在900多台机器上检查多达226个CPU标志。编写所有这些226个CPU标志的检查将是非常困难的,而且它们必须被维护。这个问题最终由Linode的创始人Chris Aker提出的一个惊人的想法解决了。 

关键的想法是把所有的CPU标志做成一个列表,并以二进制字符串的形式表示。然后,可以用比特和操作来比较这些字符串。为了演示这种算法,我将从以下一个简单的例子开始。考虑一下这段Python 代码,该代码使用bitwise and对两个数字进行比较:

>>> 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标志。这些标志在二进制表示中的具体位置由我们数据中心的机器维护、记录和共享。 

维护这种列表表示法比维护一组假设检查CPU标志是否存在的if语句要简单和有效得多。例如,假设有7个CPU标志需要被跟踪和检查。这些标志可以存储在一个8位的数字中(留出一位供将来扩展)。一个例子的字符串可以是这样的 00111011,其中最右边的位显示AES被启用,最右边的第二位显示MMX被启用,第三位表示另一个标志被禁用,以此类推。

如下面的代码片段所示,我们可以看到哪些硬件会支持这种标志组合,并在一个周期内返回所有的匹配结果。如果我们使用一组if语句来计算这些匹配,则需要更多的周期来实现这一结果。对于一个源机器上有4个CPU标志的Live Migration例子,将需要203,400个周期来找到匹配的硬件。

实时迁移代码对源机和目的机上的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启动实时迁移。同一数据中心的最佳兼容硬件将被自动选择,迁移将开始。
  • 我们可以把一个主机上的所有Linodes的实时迁移作为一个单一的任务来启动。这个功能是在对主机进行维护之前使用的。该工具将自动为所有的Linode选择目的地,并为每个Linode协调实时迁移。
  • 我们可以指定一个需要维护的几台机器的列表,工具将自动协调所有主机上的Linodes的实时迁移。

 大量的开发时间用于使软件 "只是工作"。

失败案例

在软件中不常被谈论的一个特点是优雅地处理失败的情况。软件应该是 "只是工作"。大量的开发时间用于使软件 "正常工作",而这正是实时迁移的情况。我们花了很多时间来考虑这个工具不能工作的所有情况,并优雅地处理这些情况。下面是其中的一些情况,以及如何处理这些情况:

  • 如果一个客户想从他们的Linode的一个功能中访问,会发生什么? Cloud Manager?例如,用户可以重新启动 Linode 或将Block Storage 卷附加到它。
    • 答复::客户有权力这样做。实时迁移会被打断,不能继续进行。这个解决方案是合适的,因为实时迁移可以在以后尝试进行。
  • 如果目的地Linode无法启动,会发生什么? 
    • 答案是:让源硬件知道,并设计内部工具来自动选择数据中心的另一块硬件。同时,通知运营团队,让他们调查原目的地的硬件。这种情况曾在生产中发生过,并由我们的实时迁移实施来处理。
  • 如果你在迁移过程中失去了网络,会发生什么?
    • 答案是:自主监测实时迁移的进度,如果在最后一分钟内没有任何进展,就取消实时迁移,并让运营团队知道。这在测试环境之外还没有发生过,但我们的实施方案已经为这种情况做好了准备。
  • 如果互联网的其他部分关闭了,但源和目的硬件仍在运行并进行通信,而且源或目的Linode运行正常,会发生什么情况?
    • 解答:如果实时迁移不在关键部分,请停止实时迁移:如果实时迁移不在关键部分,请停止实时迁移。然后稍后再尝试。
    • 如果你处于关键部分,继续进行实时迁移。这一点很重要,因为源Linode已经暂停了,而目标Linode需要启动才能恢复操作。
    • 在测试环境中对这些情况进行了模拟,并发现规定的行为是最佳的行动方案。

跟上变化的步伐

在经历了数十万次成功的实时迁移后,人们有时会问一个问题:"实时迁移何时结束?"实时迁移是一项技术,其使用范围随着时间的推移而扩大,并不断得到完善,因此标记项目的结束不一定是直接的。回答这个问题的一个方法是考虑这个项目的大部分工作何时完成。答案是:对于可靠的、可信赖的软件来说,这项工作在很长一段时间内都不会完成。

随着时间的推移,为Linodes开发新的功能,必须进行工作以确保这些功能与Live Migrations的兼容性。当引入一些功能时,没有新的Live Migrations开发工作要做,我们只需要测试Live Migrations仍能按预期工作。对于其他功能,在新功能开发的早期,Live Migrations的兼容性工作被标记为一项任务。

就像所有的软件一样,总有一些更好的实施方法是通过研究发现的。例如,从长远来看,更模块化的Live Migrations集成方法可能会提供更少的维护。或者,有可能将实时迁移的功能融合到较低级别的代码中,这将有助于在未来的Linode功能中实现开箱即用。我们的团队考虑了所有这些选项,为Linode平台提供动力的工具是活的实体,将继续发展。


注释

留下回复

您的电子邮件地址将不会被公布。 必须填写的字段被标记为*