Fault-Tolerant Virtual Machines 论文解读

364 阅读10分钟
原文链接: icell.io

如果想要实现一套容错方案,通常做法是提供一套 primary/backup 服务器,两者保持一致,从而让 backup 能够在 primary 挂了的时候能够迅速进行替代,这样对于客户端或者外界来说并没有看到很大的变化。

但是要做到这一点,一个很重要的前提是 primary 和 backup 能够保持高度一致,一种做法就是将服务器看成是一个 DSM(Deterministic State Machine),在给定初始状态和一致的输入,是能够确定达到下一个状态的,这样的话只要保证 primary 和 backup 的初始一致性,然后将 primary 中的操作行为(包括发生此行为时系统所处的状态)都作为输入传递给 backup,这样的话就能确保两者能够得到一致的状态。

但是,在真实的物理机上是很难将上面的操作看成是一个确定性(deterministic)的行为(比如中断事件、读取 CPU 计数器的值),所以 VMware 的解决方案是将所有操作虚拟化,并在 VM 和物理机中间增加 hypervisor(虚拟机管理程序),通过 hypervisor 让 VM 能够模拟成一个 DSM 在运行,primary 所有操作都是确定的,再通过 logging channel 将这些操作传递到 backup 中,让 backup 进行 replay, 从而实现两者的高度一致。

设计要点

如下图所示,就是 VMWare FT 的一个基本的结构图:

如图,想要对一个服务(primary)做容错处理,在另一个机器上运行一台 backup,两者通过 logging channel 保持同步,对外界来说这个系统只有一个 primary 在工作,所有的输入输出都是由 primary 进行处理的。另外,在此设计中,primary 和 backup 是共享的同一个存储,这样做可以有效处理 split-brain 问题(这点下面会有解释)。

保证 backup 能够执行确定性的 replay 操作

上面说过,对于一个真实物理机而言,看成是 DSM 来处理是不现实的,主要有三点挑战:

  1. 需要正确捕获所有的 input 以及不确定操作(non-determinism)来确保能够在 backup 上进行确定性(determinism)地执行;
  2. 在 backup 上正确、不出错地执行所有的 input 以及不确定操作(non-determinism);
  3. 还要在保证性能的前提下做到以上两点;

但是呢,VMware 做到了(是的,我也想知道到底怎么做的,大概就是通过 hypervisor 来将所有的不确定事件虚拟化为确定事件)。实现的最终结果是,primary 会将它所有的 input 以及所有可能会发生不确定性结果的相关状态全部会记录到日志中,这个日志会交给 backup 进行 replay 操作,能够确保 backup 进行 replay 后同 primary 的状态是一模一样的。

举个例子,一个操作让 primary 生成一个随机数,那么 primary 会在日志中记录当前生成这个操作的所有状态,比如它是根据当前时间或者是当前某个时钟周期当作 seed,这些随机性全部由 hypervisor 来处理,backup 进行日志 replay 时,碰到这种随机性事件,hypervisor 让它执行的时候跟 primary 得出的结果一摸一样,让两者在状态上没有差别,很了不起。

通过 FT Protocol 确保主备的信息一致

primary 会时刻写日志交给 backup 来进行 replay,这些日志通过 logging channel 传递,传递过程中满足 FT Protocol,FT Protocol 定义了一个最基本的要满足的需求,叫做 Output Requirement,它要求在系统外界看来(比如客户端),哪怕是发生了 primary 宕机然后 backup 顶上这种事情,外界还是认为一切正常,backup 顶上的行为要求和 primary 要一致。

要满足这一点,VMware 定义了一个叫做 Output Rule 的准则,当收到要对外界做 output 类别的操作时,primary 要等到 backup 能够 replay 到 primary 能够执行将要进行 output 操作那一点,才能开始对外界进行 output 操作。

上面说得有点绕,换个说法,就是 primary 收到了一条信息,要对这个信息做 output(比如对这条信息进行回复),然后 primary 先写日志并传给 backup,并且确保 backup 收到了这条日志,也就是收到来自 backup 关于这条日志的 ACK 之后,才能够开始执行真正的 output 操作。论文中有张图片能便于理解:

上面这张图展示了 primary 和 backup 进行通信的时序图,primary 收到异步事件、input 和 output 时,都要对 backup 发送 log,但是收到 output 操作后,直到收到 backup 的确认信息后才发出 output,这样的话,哪怕是之后 primary 挂了,backup 顶上变成 primary 后,外界对此也不会有任何感知。

这里有一点我还不是很确定,从上图中来看只有 output 类别的操作需要 backup 发送 ack,但是从整篇论文来看,感觉意思是不管什么类别的信息都需要 backup 发 ack,所以不太确定。

但是,如果 primary 在收到 backup 的 ACK 后,刚准备发出 output 或者刚刚发出 output,这时候很不幸挂了,然后 backup 就顶上了,这时就比较尴尬。backup 顶上之后不管之前的 output 发出去了还是没发出去,都还是会再发一遍 output,也就是说这个系统是**不能保证 exactly once **的。发生这种情况的话,假如说这个 output 是一个 TCP 通信,那么就依赖 TCP 来解决这个问题,如果是磁盘 I/O 操作的话,也只能依靠 I/O 操作的幂等性来解决。

primary 和 backup 各自在生产、消费日志时,是不会将日志写入到磁盘中的,它们各自在 VM 中都会有一个缓存,primary 产生日志写入到自己的缓存中,然后通过 logging channel 传到 backup 的缓存中,backup 从自己的缓存中获取日志进行 replay 操作。如果缓存满了,或者是空了,则生产者或者是消费者就会等待,等到可以进行操作。

当然,如果是类似于生产者-消费者这样的模型,就会遇到生产过快或者消费过慢的问题。大多数情况下,backup 都是能够很快消费来自 primary 的日志的,但是一旦出现 primary 产生日志速度过快,或者是 backup 执行 replay 操作比较慢,就导致 primary 和 backup 之间的差距越来越大。整个系统是不希望出现这样的情况的,因为如果 primary 挂了,backup 是需要完全 replay 才能顶上,差距大了,backup 就需要耗费比较长的时间追赶上,这样的话意味着系统的响应是不及时的。

所以在这套系统中,就有一种机制,如果出现上述情况,比如 primary 和 backup 之间的延迟超过了某一个范围,就降低分配给 primary 的 CPU 资源,让它慢一点生产,待 backup 赶上之后再让 primary 的 CPU 资源提高一些。通过这种均衡分配策略,让 primary 和 backup 始终维持一个较低的延迟。

即时监测和响应错误

Primary 和 backup 同时存在,目的就是要 primary 挂了之后,backup 能够立即顶上。当然,backup 也是会挂掉的,当它挂掉的时候,primary 就会停止向它发送日志,但是自己照样保持正常运行,同时通知 cluster service 赶紧给自己换一个新的 backup 来代替之前的。

这里提一句,VMware 提出了一个叫做 FT VMotion 的技术,它是负责 backup 的创建,且在创建过程中不会对 primary 造成很大的影响。它能够做到直接对一个 VM 进行克隆,得到一个新的 copy,并且整个过程对 VM 的暂停不会超过 1s,另外,在完成克隆操作之后会建立好两者的 logging channel,被克隆的一方就是 primary,另一个就是 backup。

Primary 和 backup 的服务之间会有 UDP 的 heartbeat 来进行监测对方是否是挂了,但是这里就有可能遇到这种问题。比如说,backup 长时间没有收到来自 primary 的 heartbeat,此时要不就是 primary 挂了,要不就代表 primary 其实没挂但是整个系统的网络挂了,这时就不能单纯的让 backup 顶上去了,因为顶上去之后就可能会有两个自称为 primary 的服务一直在跑。这种问题被称为 split-brain 问题。

VMware FT 解决这个问题的方法也很奇妙,最开始说过,primary 和 backup 之间会共享存储,当 primary 和 backup 两者之间任何一方认为对方挂了,想要做进一步行动之前,需要在共享存储中执行一个叫做 test-and-set 的原子操作,如果能成功则继续进行下一步,但是如果失败了,那就得自杀(对,是自杀)。当然,还有一种情况,就是共享存储也无法访问了,那遇到这种情况就没办法只能等,因为如果共享存储挂了,无论是 primary 还是 backup 谁顶上也没有办法,所以这里的共享存储其实是一个单点风险。

对于 test-and-set 操作,类似于下面这样的伪代码,对一个 flag 进行原子操作:


test-and-set() {
	acquire lock()
    if flag == true:
    	release lock()
        return false
    else:
    	flag = true
        release lock()
        return true
}

另外要说明的一点是,primary 和 backup 并不会都对共享存储进行操作,对于这套容错系统来说,同共享存储交互会被视为同外界在进行交互,也就是说如果对存储进行写操作,结合之前所说这属于一种 output 操作,根据 Output Rule,对共享存储的写操作只有 primary 会进行操作。

额外的选择

针对于 VMware 给出的已有设计之外,论文中还提出了一些别的设计方案。比如将原本的共享存储更改为 primary 和 backup 各自有自己的存储。当在实际部署时,发现 primary 和 backup 的服务不得不架设在两个不同的区域,此时使用共享存储的方案就会比较低效。但是这样选择的话,就要保证 primary 和 backup 之间的存储也要高度一致才行,除此之外,还要再解决之前提到的 split-brain 问题。

另外还有一种设计方式,是让 backup 来处理读数据的请求,这样的方式能够有效减少 logging channel 在处理很多读操作时带来的压力,但是这样就会带来新的问题:

  1. 有可能造成 backup 的消费压力,这样导致 primary 和 backup 之间的差距越来越大,从而拖慢整个系统;
  2. 可能会发生这种情况,backup 处理一个读的请求,但是此时 primary 对同样的一个位置进行了写操作,这时就要让 backup 等待 primary 写入完成之后才可以进行读操作。

总结

VMware 提出的这套 Fault-Tolerant 方案可以用在许多系统中。在 VMware 给出的性能报告中,开启 FT 和未开启 FT 并没有造成很大的性能所示。但是,它依然不适合进行高吞吐量的服务,毕竟它要对整个系统内的内存、磁盘、中断等都进行 replay,这是一个巨大的负担。但是它仍然是分布式容错方法中一个非常重要的部分。

参考资料

  1. Primary-Backup Replication FAQ, MIT 6.824
  2. The Design of a Practical System for Practical System for Fault-Tolerant Virtual Machines