当开发者将工作负载部署到云计算平台时,往往并不需要考虑运行这些服务的底层硬件。在人们对“云”的理想化印象中,硬件维护和物理限制往往是无形的,然而硬件不可避免需要时不时进行维护,这可能会导致停机。为避免这样的停机时间被转嫁给我们的客户,并真正实现云的承诺,Linode提供了一种名为实时迁移(Live Migration)的工具。
借助实时迁移技术,Linode实例可在不中断服务的前提下在不同物理服务器之间移动。通过实时迁移工具移动Linode实例时,迁移过程对Linode实例中运行的进程是完全不可见的。如果一台主机的硬件需要维护,即可通过实时迁移,将该主机上的所有Linode实例无缝转移到另一台主机中。迁移操作完成后,即可开始修理物理硬件,全过程中并不产生会影响到客户的停机时间。
这几乎成为一种决定性的技术,也成为云技术和非云技术之间的转折点。本文我们将深入了解这项技术背后的细节。
和大部分新项目类似,Linode的实时迁移也是这样启动的:进行大量研究,创建一系列原型,获得同事和管理层的大量帮助。我们的第一步是调查QEMU如何处理实时迁移。QEMU是Linode使用的一种虚拟化技术,而实时迁移也是QEUM的一项功能。因此我们团队的重点是将这项技术引入Linode,而非重新发明一个类似的技术。
那么实时迁移技术到底是如何以QEMU的方式实现的?整个过程分为以下四步:
这些步骤概括介绍了QEMU实时迁移的执行过程。然而依然需要通过包含很多手工操作的方式来精确指定目标QEMU实例的启动方式。此外,上述过程中的每个操作都必须在正确的时间执行。
在分析过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端的功能相互隔离,具体行为类似于独立的系统。
从这个意义来看,我们需要处理三层内容:
目标实例启动并准备好接受传入的迁移后,目标硬件会告知源硬件开始发送数据。源端会在收到这个信号后开始进行处理,并会在软件中告知QEMU开始传输磁盘内容。软件会自主监控磁盘传输进度,借此检查传输操作是否完成,并会在磁盘传输完成后自动开始迁移内存内容。此时软件依然会自主监控内存迁移进度,并在内存迁移完毕后自动切换至割接模式。上述全过程都是通过Linode的40Gbps网络进行的,因此网络方面的操作都可以快速完成。
割接操作是实时迁移过程中最重要的一环,只有理解了它,才能完全理解实时迁移操作。
在割接点状态下,QEMU已经确认做好了所有准备,可以进行割接并在目标计算机上运行。源QEMU实例会让两端暂停运行,这意味着:
由于时间和网络请求均已停止,我们希望割接能尽量快速完成。然而为保证成功割接,还需要进行一些检查:
由于割接过程时间有限,我们希望能尽快完成上述操作。解决了这些问题后,即可继续进行割接了。源Linode实例会自动会自动收到“割接完成”信号并让目标实例运行起来。目标Linode实例会从源实例暂停时的状态恢复运行。源和目标实例上的其余内容则会被清理。如果目标Linode实例在未来某个时间需要再次进行实时迁移,则会重复执行上述步骤。
实时迁移的大部分过程都是直接实现的,但考虑到边缘案例后,该功能本身的开发也进行了大量扩展。这个项目的顺利完成很大程度上要归功于管理团队,他们坚信该工具有着极大的愿景,并提供了完成该任务所需的各项资源,另外当然也离不开坚信该项目能够成功完成的大量员工。
我们在下列这些领域遇到了很多边缘案例:
在向来宾操作系统呈现CPU方面,QEMU有不同的选项。其中一个选项可将主机CPU的型号和功能(即CPU标记)直接传递给来宾系统。通过使用该选项,来宾即可不受约束地使用KVM虚拟化系统所支持的全部能力。当Linode首次采用KVM时(当时还没有实时迁移功能),为了实现最大化性能,我们就使用了该选项。然而在开发实时迁移功能的过程中,该选项为我们造成了很多挑战。
在实时迁移的测试环境中,源和目标主机是两台完全相同的计算机。但在现实世界中,我们的硬件集群并非100%完全相同的,计算机之间的某些配置差异可能导致产生不同的CPU标记。这很重要,因为当一个程序被载入Linode的操作系统后,Linode会向该程序呈现CPU标记,为了充分利用这些标记,程序可以将软件中的特定部分载入内存。如果一个Linode实例被实时迁移到不支持该CPU标记的目标计算机,程序将会崩溃。这可能导致来宾操作系统崩溃,甚至导致Linode重启动。
我们发现有三个因素会影响到计算机的CPU标记如何呈现给来宾系统:
因此在实现实时迁移时,我们必须设法防止程序因为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
“按位与”会对比二进制的“数”,也就是两个不同数字中的“位”。该操作会从上述数字最右边的位开始向左处理:
因此完整结果的二进制表示就是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平台的工具是活跃的,还会继续努力,使其不断进化和发展。
最后,欢迎广大知友们关注Akamai,第一时间了解Akamai在Web交付性能、安全性、边缘计算等领域的最新技术成果和解决方案,以及整个行业的发展趋势。