MIT 6.824 - The Design of a Practical System for Fault-Tolerant Virtual Machines

介绍

和一般描述的应用级别的主从备份不同,本文描述的是虚拟机的主从备份。主从备份作为一种常见的容错实现手段,当主节点异常时,从节点能取代主节点从而保证系统依然可用。作为从节点,它的状态必须尽可能的与主节点随时保持一致,这样当主节点异常时从节点能马上取代主节点,而客户端也不会感知到异常,同时也没有数据丢失。其中一种同步主从节点状态的方式是持续将主节点的所有修改发送给从节点,这里的修改包括 CPU、内存以及 IO 设备。然而,采用这种同步方式需要大量的网络带宽,尤其是发送内存的修改。

另一种只需要耗费少量带宽的方式是状态机(state machine)同步。该方法将主从同步抽象为确定性状态机(deterministic state machine)同步问题,在确定性状态机模型下,对于两个初始状态一样的状态机来说,按照相同的顺序执行相同的一系列输入指令后,最后的状态也一定是相同的。然而,对于大部分的服务来说,存在某些非确定性的操作,例如生成一个随机数,这时候就需要额外的协调使得主从间依然是同步的,即从节点也要生成一模一样的随机数。不过,处理这种情况所需要维护的额外信息相比于主节点状态的修改(主要是内存的修改)来说不值一提。

对于物理机来说,随着主频的增加,同步主从间的确定性操作也愈发困难。然而对于运行在 hypervisor 上的虚拟机来说却非常适合实现状态机同步。一个虚拟机本身就可以看做一个明确的状态机,它的所有操作就是被虚拟化的机器的操作(包括所有的设备)。和物理机一样,虚拟机也存在一些非确定性的操作(例如读取当前时间或者发送一个中断),所以也需要发送额外的信息给从节点来保证主从同步。因为 hypervisor 掌管着虚拟机的执行,包括发送所有的输入给被虚拟化的机器,所以它能捕获到执行非确定性操作的所有需要的信息,从而能正确的在从节点上执行重放操作。

因此,基于状态机同步的主从同步方式可以在不需要修改硬件的情况下在廉价的硬件上实现,使得容错技术适用于最新的微处理器。另外,对带宽较低的要求使得长距离的虚拟机主从同步成为了可能。例如,可以在跨校园间不同的物理机上做主从同步,相比于同大厦内的主从同步更为可靠。

目前在 VMware vSphere 4.0 平台上已经实现了这种容错技术,该平台能高效完整的虚拟化 x86 架构的机器。因为 VMware vSphere 实现了一个完全的 x86 虚拟机,所以可以自动的对任何 x86 的操作系统和应用提供容错支持。通过确定性重放(deterministic replay),系统可以记录下主节点的执行并且确保能在从节点执行相同的操作。VMware vSphere Fault Tolerance (FT) 在此基础之上增加了额外的功能和协议来支持构建一个可完全容错的系统。除了对硬件的容错外,当主节点异常时,系统能自动的在本地集群中启动一台可用的从节点来接管主节点。在该篇论文发表的时候,确定性重放技术和 VMware FT 仅支持单核的虚拟机。受限于严重的性能问题,多核虚拟机的重放支持仍在进行中,因为在多核场景下,几乎每一个对共享内存的访问都是一个非确定性的操作。

BressoudSchneider 针对惠普的 PA-RISC 平台的虚拟机容错做了个原型实现。VMware 的实现与其类似,不过出于性能的考虑做了些根本的修改以及调研了一些其他实现方案。另外,为了能构建一个高效、可用的容错系统来支持用户的企业级应用,VMware 还设计和实现了许多其他组件以及解决一些实际的问题。和大多数实际的系统要解决的问题一样,这里的容错针对的是 fail-stop 的异常,即在造成外部可见的不正确的行为前可被监测到的异常,例如磁盘空间不足、网络无法连通等等,而诸如应用程序的 bug 或者人为失误等则不属于 fail-stop 异常,系统也无法进行容错。

基础设计

alt

上图展示了支持容错的虚拟机的基本配置。对于每一台需要支持容错的虚拟机(primary VM),系统会在其他物理机上同时运行一台备份虚拟机(backup VM),备份虚拟机和主虚拟机会保持同步,并执行和主虚拟机相同的指令,不过会存在一定的延迟。这两台虚拟机被称为处于 virtual lockstep 状态。同时,虚拟机连接着相同的共享存储,输入和输出都可以被主从虚拟机访问。不过,只有主虚拟机才会暴露在网络中,所以所有的网络输入都只会发送给主虚拟机。同样的,其他所有的输入(例如键盘和鼠标输入)也都只会发送给主虚拟机。

主虚拟机收到的所有输入都会通过 logging channel 发送给从虚拟机。对系统来说,主要的输入负载就是网络和磁盘。为了保证从虚拟机能和主虚拟机执行相同的非确定性操作,还需要发送一些额外的信息给从虚拟机。从结果上来说,从虚拟机会始终执行和主虚拟机相同的操作。不过,所有从虚拟机的输出都会被 hypervisor 丢弃,只有主虚拟机的输出才会返回给客户端。后面会提到,主从虚拟机间的通信会遵循一个特定的协议,包括从虚拟机对消息的确认,来保证当主虚拟机异常时不会发生数据丢失。

为了监测主虚拟机或者从虚拟机是否发生异常,系统会通过和主从虚拟机间的心跳以及 logging channel 的流量来判断。另外,系统必须保证在任一时间只有一台主虚拟机或者从虚拟机作为对外执行的入口,即使主虚拟机和从虚拟机间失联发生脑裂的场景。

确定性重放的实现

在前面提到过,主从虚拟机的同步可以抽象为确定性状态机同步问题。如果两个确定性状态机以相同的初始状态启动,并且按照相同的顺序执行相同的输入,那么这两个状态机会经历相同的状态流转并输出相同的结果。一台虚拟机会有一系列的输入,包括网络包,磁盘读取,以及键盘和鼠标输入。而非确定性的事件(例如虚拟中断(virtual interrupts))和非确定性的操作(例如读取当前处理器的时钟周期数)也会影响虚拟机的内部状态。这就给重放执行一台运行着任意操作系统和任意服务的虚拟机带来了3个挑战:

  1. 需要正确的捕捉到主虚拟机的所有输入和非确定性的操作
  2. 需要正确的将输入和非确定性操作在从虚拟机上重放
  3. 不能影响系统性能

另外,x86 处理器中很多复杂的操作往往伴有副作用,因此也是非确定性的操作,如何捕捉到这些非确定性的操作并正确的在从虚拟机上重放也是一个挑战。

VMware vSphere 平台为 x86 虚拟机提供了上述的重放功能。确定性重放技术会将主虚拟机的所有输入和所有可能的非确定性操作写入到日志中。从虚拟机就可以读取日志并执行和主虚拟机一样的操作。对于非确定性的操作来说,系统会写入一些额外的信息来保证重放时生成相同的虚拟机状态和输出。对于非确定性的事件例如计时器或者 IO 完成中断,在事件发生时所执行的指令也会记录在日志中。在重放时,事件会和指令一同出现在指令流中。借助联合 AMDIntel 开发的 hardware performance counter(一组特殊寄存器用来记录硬件相关事件发生的次数)和其他技术,VMware 确定性重放技术能高效的记录和重放非确定性的事件。

BressoundSchneider 在其实现中提到将虚拟机的执行以 epoch 为单位进行切分,其中所有的非确定性操作例如中断都放在 epoch 的最后。这个想法是出于批量处理的考虑,因为单独将每一个中断和中断发生时对应的指令重放执行代价较大。不过 VMware 的实现足够高效使得不需要借助 epoch 来实现确定性重放,每一个中断都能被准确的记录并伴随着发生中断时的指令一起执行。

FT 协议

VMware FT 使用确定性重放技术将主虚拟机的执行流记录到日志中,不过主虚拟机并不是将日志写入到磁盘上,而是通过 logging channel 将日志发送给从虚拟机。从虚拟机能实时的读取日志并将其重放,从而执行和主虚拟机一样的操作。然而,双方在 logging channel 的通信必须遵循 FT 协议来保证容错。其中一条基本的要求是:

Output Requirement:当主虚拟机异常,从虚拟机接管执行时,从虚拟机的输出必须和先前主虚拟机已经发送给客户端的输出一致。

当异常发生时(主虚拟机异常,从虚拟机接管执行),从虚拟机的执行可能会和在没有异常发生时主虚拟机的执行不同,因为在执行时会有很多非确定性的操作。然而,只要从虚拟机的输出满足 Output Requirement,则在主从切换时就不会有外部可见的状态或者数据丢失,而客户端也不会感知到中断或者服务的不一致。

通过主虚拟机的延迟输出,保证从虚拟机确认收到了所有日志后,主虚拟机才将输出返回给客户端来实现 Output Requirement。一个先决的条件是主虚拟机在执行输出操作前,从虚拟机必须已经收到所有的日志。这些日志能保证从虚拟机执行到主虚拟机最新的执行点。然而,假设当主虚拟机刚开始执行输出操作时发生了异常,此时发生了主从切换,从虚拟机必须先将未处理完的日志进行重放,然后才能 go live(不再执行重放,接管成为主虚拟机)。如果在这之前从虚拟机 go live,可能会有一些非确定性的事件(例如计时器中断)在从虚拟机执行输出操作前改变了执行的路径。

针对上述的要求,最简单的实现 Output Requirement 的方式是为每一条输出操作创建一条特殊的日志,从而可以通过以下规则来保证 Output Requirement

Output Rule:在从虚拟机确认收到输出操作对应的日志前,主虚拟机不能执行输出操作。

如果从虚拟机收到了所有的日志,包括输出操作对应的日志,那么从虚拟机就能重放出和主虚拟机在执行输出操作时一模一样的状态,当主虚拟机异常时,从虚拟机就能恢复到主虚拟机执行输出操作前一致的状态。相反的,如果从虚拟机在没有收到日志前就接管了主虚拟机的操作,那么它的状态就和主虚拟机不一致,从而导致最终的输出也不一致。

注意 Output Rule 并没有要求在从虚拟机确认收到输出日志前停止主虚拟机的执行。这里只是延迟了主虚拟机的输出,它依然可以执行其他指令。因为操作系统以异步中断的方式来通知非阻塞网络和磁盘输出的完成,主虚拟机可以在这期间轻易的执行其他指令,而不用阻塞等待。相反的,在其他的一些实现中,在从虚拟机确认收到输出日志前,主虚拟机必须完全停止等待。

alt

上图展示了 FT 协议的要求。主虚拟机到从虚拟机的箭头表示日志的发送,从虚拟机到主虚拟机的箭头表示日志的确认。所有异步事件,输入和输出的操作都必须发送给从虚拟机并得到确认。只有当从虚拟机确认了某条输出操作的日志后,主虚拟机才能执行输出操作。所以只要遵循了 Output Rule,从虚拟机在接管执行时就能保持和主虚拟机一致的状态。

不过在异常发生时,VMware FT 并不能保证所有的输出只发送一次。在缺少两阶段提交的帮助下,从虚拟机不能知晓主虚拟机在发送某条输出之前还是之后发生了异常。不过,网络协议(包括常见的 TCP 协议)在设计时就已经考虑了包的丢失和重复包的情况,所以这里无需特殊处理。另外,在主虚拟机异常时发送给主虚拟机的输入也有可能丢失,因此从虚拟机也会丢失这部分的输入。不过,即使在主虚拟机没有异常的情况下,网络包本身就有可能丢失,所以这里同样也不需要特殊处理,不管是网络协议、操作系统还是应用程序,在设计和编写时本身已经考虑到了包丢失的情况。

监测和响应异常

之前提到过,当主虚拟机或者从虚拟机发生异常时,双方都必须能快速响应。当从虚拟机异常时,主虚拟机会进入 go live 模式,即不再记录执行日志,以常规的方式执行。当主虚拟机异常时,从虚拟机也会进入 go live 模式,不过相比于主虚拟机略微复杂些。因为从虚拟机在执行上本身就落后于主虚拟机,在主虚拟机异常时,从虚拟机已经收到和确认了一部分执行日志,但是还没有执行重放,此时从虚拟机的状态和主虚拟机还是不一致的。所以,从虚拟机必须先将暂存的日志进行重放,当所有重放都执行完成后,从虚拟机就会进行 go live 模式,正式接管主虚拟机(此时缺少一个从虚拟机)。因为此时这台从虚拟机不再是从虚拟机,被虚拟化的操作系统所执行的输出操作都会发送给客户端。在这个转换期间,可能也需要某些设备执行一些特定的操作来保证后续输出的正确性。特别是对于网络输出来说,VMware FT 会自动的将新的主虚拟机的 MAC 地址在网络中广播,使得物理交换机知道最新的主虚拟机的地址。另外,后文会提到新的主虚拟机可能会重新发起一些磁盘 IO 操作。

有很多种方式来监测主虚拟机和从虚拟机的异常。VMware FT 通过 UDP 心跳来监测启用了容错的虚拟机是否发生了异常。另外,VMware FT 还会监控 logging channel 中的流量,包括主虚拟机发送给从虚拟机的日志,以及从虚拟机的消息确认回执。因为操作系统本身存在时钟中断,所以理论上来说 logging channel 中的流量应该是连续不断的。因此,如果监测到 logging channel 中没有流量了,那么就可以推断出某台虚拟机发生了异常。如果没有心跳或者 logging channel 中没有流量超过一段指定的时间(近似几秒钟),那么系统就会声明这台虚拟机发生了异常。

然而,这种异常监测机制可能会引发脑裂问题。如果从虚拟机不再接收到来自主虚拟机的心跳,那么有可能说明主虚拟机发生了异常,但也有可能只是双方间的网络断开。如果此时从虚拟机进入 go live 模式,由于此时主虚拟机依然存活,就有可能给客户端造成数据损坏或其他问题。因此,当监测到异常时必须保证只有一台主虚拟机或者从虚拟机进入 go live 模式。为了解决脑裂问题,VMware FT 借助了虚拟机所连接的共享存储。当主虚拟机或者从虚拟机希望进入 go live 模式时,它会向共享存储发起一个原子性的 test-and-set 操作。如果操作成功,那么当前虚拟机可以进入 go live 模式,如果操作失败,说明已经有其他虚拟机先进入了 go live 模式,所以当前虚拟机就将自己挂起。如果虚拟机访问共享存储失败,那么它会一直等待直到访问成功。如果共享存储由于网络问题造成无法访问,那么虚拟机本身也做不了什么因为它的虚拟磁盘就挂载在共享存储上。所以使用共享存储来解决脑裂问题不会带来其他可用性问题。

最后一个设计的点是如果虚拟机发生了异常,使得某台虚拟机进入了 go live 模式,那么 VMware FT 会自动在其他物理机上启动一台新的备份虚拟机。

FT 的实际实现

上节主要描述了 FT 的基础设计和协议。然而,为了构建一个可用,健壮和自动化的系统,还需要设计和实现很多其他的组件。

启动和重启 FT 虚拟机

其中一个至关重要的组件是如何启动一台有着和主虚拟机一模一样状态的备份虚拟机。这个同时也会在当某台虚拟机异常需要重新启动一台备份虚拟机时用到。因此,这个组件必须能够在主虚拟机处于任意状态的时候复制一台一模一样的备份虚拟机(而不仅仅是初始状态)。另外,这个启动操作还不能影响到主虚拟机的执行,因为这有可能影响到当前所有连接着的客户端。

对于 VMware FT 来说,它借用了 VMware vSphere 平台已有的 VMotion 的功能。VMware VMotion 能以极小的代价将一台运行中的虚拟机迁移到另一台机器上——虚拟机的暂停时间一般在一秒内。VMware FTVMotion 做了些改动使得在不销毁当前虚拟机的情况下,在远程服务器上复制一台和当前虚拟机一模一样的虚拟机。即修改版的 FT VMotion 做的是虚拟机的复制而不是迁移。FT VMotion 也会同时建立一条 logging channel,源虚拟机作为主虚拟机就会将执行日志写入到 logging channel,而复制后的虚拟机作为从虚拟机就会读取日志开始重放执行。和常规的 VMotion 一样,FT VMotion 也能将对主虚拟机的暂停控制在一秒以内。因此,对某台运行中的虚拟机开启 FT 功能非常简单,且没有破坏性。

另一个启动一台备份虚拟机要考虑的点是选择在哪台物理机上启动。支持容错的虚拟机运行在某个可访问共享存储的集群中,所以本质上虚拟机可以运行在任意一台机器上。这个灵活性使得 VMware vSphere 可以轻易的为一台或多台异常的虚拟机启动新的备份虚拟机。VMware vSphere 实现了一套集群服务来管理集群中的资源。当虚拟机发生异常主虚拟机需要一台新的虚拟机来维持容错时,主虚拟机会通知集群服务需要一台新的备份虚拟机。此时集群服务会根据资源利用率和其他条件来决定在哪台机器上重启新的备份虚拟机,然后会由 FT VMotion 创建一台新的备份虚拟机。一般来说,VMware FT 可以在发生异常的几分钟内恢复某台虚拟机的冗余功能,而不会造成任何中断。

管理 Logging Channel

在管理 logging channel 的流量上有很多有趣的实现细节。在 VMware 的实现里,hypervisor 为主虚拟机和从虚拟机维护了一大块日志缓冲区。在主虚拟机执行时,它将执行日志发送到日志缓冲区中,类似的,从虚拟机从日志缓冲区中消费日志。每当主虚拟机的日志缓冲区中有数据,系统就会将其发送到 logging channel 中,相应的在另一边则会将其放入到从虚拟机的日志缓冲区中。当从虚拟机从 logging channel 中读取到日志并将其放入日志缓冲区后,它就会向主虚拟机发送一个已读消息的回执。VMare FT 就可以根据这个已读回执决定哪些输出操作可以执行了。下图展示了这个过程:

alt

当从虚拟机从日志缓冲区读取不到任何日志时(日志缓冲区为空),它就会暂停执行直到下一条日志到达。因为从虚拟机不和外界交互,这个暂停对客户端没有任何影响。类似的,如果主虚拟机往日志缓冲区中写日志发现日志缓冲区满时,它必须停止执行直到日志缓冲区被消费。这个暂停保证了主虚拟机以一个可控的速度生产执行日志。不过,这个暂停会影响客户端的响应,直到主虚拟机可以继续写日志并恢复执行。所以,在实现时必须考虑如何尽量避免主虚拟机的日志缓冲区写满。

其中一个原因造成主虚拟机的日志缓冲区写满是因为从虚拟机执行的太慢从而造成消费日志太慢。一般来说,从虚拟机必须以和主虚拟机记录执行日志一样的速度来执行重放。幸运的是,在 VMware FT 的实现下,记录执行日志和重放所需要的时间基本是相同的。不过,如果从虚拟机所在的机器存在和其他虚拟机资源竞争(资源超卖),不管 hypervisor 的虚拟机调度多么高效,从虚拟机都有可能得不到足够的 CPU 和内存资源来保证和主虚拟机一样的速度执行重放。

除了主虚拟机日志缓冲区满造成的不可控暂停外,还有一个原因也要求主从虚拟机间的状态不能差太远。当主虚拟机异常时,从虚拟机必须尽快的将所有的执行日志进行重放,达到和主虚拟机一样的状态,然后接管主虚拟机向客户端提供服务。结束重放的时间基本上等于异常发生时从虚拟机落后主虚拟机的时间,所以从虚拟机进入 go live 模式需要的时间就基本上等于检测出异常的时间加上当前从虚拟机落后的时间。因此,从虚拟机不能落后主虚拟机太多(大于一秒),否则这会大大增加故障切换的时间。

因此,VMware FT 有另一套机制来保证从虚拟机不会落后主虚拟机太多。在主从虚拟机间的通信协议里,还会发送额外的信息来计算两者间的执行时间差。一般来说这个时间差在100毫秒以内。如果从虚拟机开始明显落后主虚拟机(例如大于1秒),那么 VMware FT 会通知调度器降低主虚拟机的 CPU 资源配额(初始减少几个百分点)来延缓主虚拟机的执行。VMware FT 会根据从虚拟机返回的落后时间来不断调整主虚拟机的 CPU 资源配额,如果从虚拟机一直落后,那么 VMware FT 会逐渐减少主虚拟机的 CPU 资源配额。相反的,如果从虚拟机开始赶上了主虚拟机的执行速度,那么 VMware FT 会逐渐增加主虚拟机的 CPU 资源配额,直到两者的执行时间差到达一个合理的值。

不过在实际场景中减慢主虚拟机执行的速度非常少见,一般只会发生在系统承受极大负载的情况下。

FT 虚拟机上的操作

另一个实际中要考虑的问题是处理针对主虚拟机的一系列控制操作。例如,如果主虚拟机主动关机了,从虚拟机也需要同步关机,而不是进入 go live 模式。另外,所有对主虚拟机的资源修改(例如增加 CPU 资源配额)都必须应用到从虚拟机上。针对这些操作,系统会将其转化为特殊的执行日志发送到 logging channel,从虚拟机收到后也会将其正确的重放。

一般来说,大部分对虚拟机的操作都只应该在主虚拟机上发起。然后 VMware FT 会将其转化为日志发送给从虚拟机来进行重放。唯一可以独立的在主虚拟机和从虚拟机上执行的操作是 VMotion。即主虚拟机和从虚拟机都可以独立的被复制到其他机器上。VMware FT 保证了在复制虚拟机时不会将其复制到一台已经运行了其他虚拟机的机器上,因为这无法提供有效的容错保证。

复制一台主虚拟机要比迁移一台普通的虚拟机复杂些,因为从虚拟机需要先和主虚拟机断开连接,然后之后在合适的时间和新的主虚拟机建立连接。从虚拟机的复制有类似的问题,不过也略微复杂些。对于普通的虚拟机迁移来说,系统会要求当前所有进行中的磁盘 IO 在切换前执行完成。对于主虚拟机来说,它可以等待所有进行中的磁盘 IO 的完成通知。而对于从虚拟机来说,它不能简单的在某个时间让所有的磁盘 IO 都结束,因为从虚拟机还在执行重放,也需要等待重放涉及的磁盘 IO 完成。而另一方面,主虚拟机在执行时有可能一直伴有磁盘 IOVMware FT 有一个特有的方法来解决这个问题,当从虚拟机在执行切换前,会通过 logging channel 向主虚拟机发送请求要求暂时结束所有的磁盘 IO,主虚拟机收到请求后会天然的将其转化为执行日志发送到 logging channel,从虚拟机的磁盘 IO 也因此会伴随着重放而终止。

磁盘 IO 的实现问题

VMware 在实现时遇到了一些和磁盘 IO 相关的问题。首先,因为磁盘操作是非阻塞的所以可以并行执行,同一时间访问同一块磁盘区域就会导致非确定性结果。另外,磁盘 IO 会通过 DMADirect memory access) 来直接访问虚拟机的内存,因此同一时间的磁盘操作如果访问到了同一块内存页也会导致非确定性结果。VMware 通过监测这样的 IO 竞争(实际场景中极少)并强制让其在主虚拟机和从虚拟机中串行执行来解决这个问题。

第二,磁盘操作访问内存可能和应用程序访问内存产生竞争,因为磁盘操作可以通过 DMA 直接访问内存。例如,如果应用程序正在访问某个内存块,而此时磁盘也在写入这个内存块,则会发生非确定性的结果。虽然这种情况同样很少,但也仍然需要能够监测并解决。其中一种解决方案是当磁盘正在访问某个内存页时暂时对该页设置页保护。当应用程序尝试访问页保护的内存页时,会触发一个陷阱(trap)使得操作系统能够暂停执行直到磁盘操作完成。不过,修改 MMUMemory management unit)的页保护机制代价较大,VMware 借助 bounce buffer 来解决这个问题。bounce buffer 是一块和磁盘操作正在访问的内存一样大小的临时缓冲区。磁盘的读操作被修改为磁盘先将数据复制到 bounce buffer 中,然后虚拟机读取 bounce buffer 中的数据,而且只有在磁盘 IO 完成后才会将 bounce buffer 中的数据复制到虚拟机内存中。类似的,对于磁盘写操作,数据会先写到 bounce buffer 中,磁盘再将数据从 bounce buffer 中复制到磁盘上。使用 bounce buffer 的情况下会降低磁盘的吞吐,不过实际中还没有发现造成可见的性能损耗。

第三,还有些问题发生于主虚拟机正在执行某些磁盘 IO(未完成),然后主虚拟机异常,从虚拟机接管执行。对于从虚拟机来说,它并不知道这些磁盘 IO 是否已发送给磁盘或者已成功执行。另外,由于这些磁盘 IO 还没有在从虚拟机上发起过,所以也不会相应的 IO 完成的通知,但是在操作系统的角度指令已经发出了,但是从虚拟机上的操作系统也收不到 IO 完成的通知,最终会终止或者重置这个过程。在这种情况下,系统会为每一个 IO 操作发送一个失败的通知,即使某个 IO 操作实际成功了而返回失败也是可以接受的。然而,由于从虚拟机上的操作系统可能不能很好的响应 IO 失败的通知,所以在从虚拟机 go live 阶段会重新发起这些 IO 请求。因为系统已经消除了并发 IO 间的竞争,所以这些 IO 操作可以重新发起即使它们之前已经成功执行了(操作是幂等的)。

网络 IO 的实现问题

VMware vSphere 为虚拟机的网络提供了很多的性能优化。部分优化基于 hypervisor 能异步更新虚拟机的网络设备的状态。例如,在虚拟机还在执行时 hypervisor 就可以直接更新接收缓冲区。不过,这种异步更新同样也带来了非确定性。除非能保证在异步更新的时间点在主虚拟机上执行的指令能严格一致的在从虚拟机上重放,否则从虚拟机的状态就会和主虚拟机不一致。

FT 中对网络代码改动最大的一块就是禁止了异步网络更新优化。从原先的异步更新缓冲区修改为强制陷入到陷阱(trap)中,hypervisor 响应后将更新记录到日志中,然后再将其应用到虚拟机上。类似的,异步从缓冲区中拉取数据包也同样被禁止了,同样由 hypervisor 托管执行。

消除了异步的网络设备状态更新以及前面所提到的 Output Rule 带来的延迟输出,为网络性能优化又提出了挑战。VMware 通过两个方面来优化网络性能。第一,实现了集群优化来减少虚拟机的陷阱和中断。当虚拟机在接收网络包时,hypervisor 可以在一个陷阱、中断内处理多组数据包来减少陷阱和中断的次数。

第二个网络优化是降低数据包延迟发送的时间。前面提到,只有当从虚拟机确认收到所有输出操作的日志后,主虚拟机才能执行输出操作。所以减少数据包延迟发送的时间等价于减少日志发送和确认的时间。这里主要的优化点是发送和接受消息回执的过程中确保不会触发线程切换。另外,当主虚拟机将某个数据包加入发送队列时,系统会强制刷新主虚拟机的日志缓冲区到 logging channel 中。

其他设计方案

本节主要描述了 VMware 在实现 VMware FT 时调研和考虑的其他一些方案。

共享磁盘和非共享磁盘

在当前的实现中,主虚拟机和从虚拟机共享一个存储。因此当发生主从切换时,从虚拟机上的数据天然是和主虚拟机上的数据一致的。共享存储相对于主从虚拟机来说是一个外部系统,所以任何对共享存储的写入都被视为和外部的通信。因此,只有主虚拟机被允许写入到共享存储,而且写入必须遵循 Output Rule 规则。

另一种设计是主虚拟机和从虚拟机各自有一套独立的存储。在这个设计中,从虚拟机会将所有的输出写入到自己的存储中,所以从虚拟机的数据也能和主虚拟机保持同步。下图展示了这种设计下的配置:

alt

在非共享磁盘的场景下,每个虚拟机的存储被视为该虚拟机内部状态的一部分。因此,虚拟机的输出没有必要遵循 Output Rule 规则。非共享磁盘的设计在主从虚拟机无法访问共享存储时很有用。这可能时因为共享存储不可用或者过于昂贵,或者主从虚拟机间的物理距离太长。这种方案的一个主要的缺点是主从虚拟机开启容错时必须将双方的磁盘进行初始化同步。另外,双方的磁盘也有可能在异常发生后造成不同步,所以当备份虚拟机重启后,双方磁盘需要再次同步。所以,FT VMotion 不只要同步主从虚拟机的状态,还要同步磁盘的状态。

在非共享磁盘的场景下,系统就不能借助共享存储来解决脑裂问题。在这种场景下,系统可借助其他的外部组件,例如某个主从虚拟机都可以连接的第三方服务器。如果主从服务器属于某个多于两个节点的集群,那么就可以使用某个选举算法来选择谁能进入 go live 模式。在这种场景下,如果某台虚拟机获得了大多数节点的投票,那么它就可以进入 go live 模式。

在从虚拟机上执行磁盘读操作

在当前的实现中,从虚拟机从来不会从虚拟磁盘中读取数据(不论是共享存储还是非共享存储)。因为磁盘读也是一种输入,所以会自然的将磁盘读取后的结果以日志的形式通过 logging channel 发送给从虚拟机。

另一种设计是涉及到磁盘读操作的执行由从虚拟机自行从磁盘中读取,主虚拟机不再向 logging channel 中发送从磁盘读取到的数据。对于磁盘密集型的系统来说,这种设计可以大幅降低 logging channel 的负载。不过,这种设计也存在一些问题。这可能会降低从虚拟机的执行速度,因为从虚拟机在重放时必须先等待所有依赖的磁盘 IO 操作完成。

另外,当磁盘读失败时还需要额外的处理。如果主虚拟机读取磁盘成功了,而从虚拟机读取磁盘失败了,那么从虚拟机就需要进行重试直到成功,因为从虚拟机必须拿到和主虚拟机内存中一样的数据。相反的,如果主虚拟机读取磁盘失败了,则相应主虚拟机中目标内存中的数据就必须通过 logging channel 发送给从虚拟机,因为此时主虚拟机中内存中的数据是不确定的。

最后,在共享存储的模式下如果让从虚拟机执行磁盘读也有个问题。如果主虚拟机在某个磁盘区域执行了读操作,然后马上又对相同的区域执行了写操作,那么这个写操作就必须等到从虚拟机读取完成后再执行。这个依赖也能够被监测和正确处理,不过也给系统实现增加了额外的复杂度。

在实际性能测试中,从虚拟机执行磁盘读操作的情况下应用程序的吞吐会降低 1-4%,但是也能有效降低 logging channel 的负载。因此,在 logging channel 的带宽有限的情况下,可以考虑使用从虚拟机磁盘读操作。

参考