VMware-FT 论文下载:下载链接
VMware-VMotion 论文下载:下载链接
前言
自 4.0 版本开始,VMware vSphere 平台提供虚拟机的容错(VMware vSphere Fault Tolerance)功能,该功能参考了复制状态机(RSM)模型,实现了两台单核虚拟机的状态同步。同步的结果是当作为 primary 的虚拟机宕机时,曾经的 backup 虚拟机可以快速接替它成为新的 primary,而这种切换对上层应用是透明的。对于后人来说,这是一个学习复制状态机模型的绝佳例子,本文将记录我在阅读相关论文时的一些思考与总结。
复制状态机概述
在前面讨论 GFS 时我们提到,为了避免 Master 造成单点故障,GFS 以同步+异步的方式将 primary Master 的操作日志发送到其他服务器上,这些服务器通过回放(replay)接收到的操作日志,就可以达成和 primary 一致的状态。
这种通过传递操作在多个节点间达成一致的方式就被称为复制状态机,与之相对的还有一种同步方式叫做“状态复制”,这种方式通过传递状态来达成一致,在 GFS 的场景下 checkpoint 的传递就可以理解为是一种“状态复制”。所以相对而言,复制状态机每次传递的数据量是比较小的。
复制状态机的总体思路是这样的,如果两个状态机从同一个状态开始接收一系列相同的确定性输入,那么它们最终会达成相同的状态。什么叫做确定性输入呢,比如我们有函数 Now() 用于获取调用这个函数时系统的时间,那么“调用 Now(),并将它的返回值赋给 X”这个操作就不是确定性的,因为两个状态机可能会在不同的时间收到这个指令,而指令执行时间的不同会导致 Now 的返回值不同,进而导致 X 的值不一致。
所以利用复制状态机来做状态同步的系统需要特别处理那些不是确定性的输入,比如在 primary 上执行“调用 Now(),并将它的返回值赋给 X”这个指令,但是记录下 Now 的返回值,比如是 123456,然后在 backup 上则执行“将 123456 赋值给 X”的指令,从而达成两个状态机的同步。
VM FT 原理
论文第 4 节给出了两种非默认的实现,为了方便,这里仅讨论 VM FT 的默认实现,也就是 Shared Disk 以及不在 backup 上执行实际的读盘操作。
首先要明确的是,VMware vSphere 是一个全虚拟化平台,这意味着虚拟机看到的 cpu、内存、外设等都是由 Hypervisor 模拟出来的,因此虚拟机的方方面面对 Hypervisor 而言都是可见且可控的。在这样的虚拟化方案下,如果将 VM FT 的功能实现在 Hypervisor 上,就可以达到“任何操作系统不经过任何改动就可以使用 VM FT 的功能”的效果。
总体而言,vSphere 会被部署在集群上,集群中的每个物理节点运行 Hypervisor,Hypervisor 上运行各个虚拟机。Hypervisor 本质上只是服务器上的进程,所以对于一个开启了 VM FT 的虚拟机而言,它的 primary 和 backup 不应该被运行在同一台物理机上,因为一旦这个物理机宕机,primary 和 backup 就同时失效了。
primary 虚拟机所在的 Hypervisor 会与 backup 所在的 Hypervisor 通信,这种通信是通过在 Logging Channel 上传输 Log Entry 来实现的。所有的外部输入都会被发送到 primary,而 primary 将输入封装成确定性的 Log Entry 发送给 backup,所以 backup 的状态变化是由 primary 发来的 Log Entry 驱动的,它本身不直接接受外部输入。而对外的输出同样都由 primary 产生,backup 的输出会被 Hypervisor 直接屏蔽掉。
在默认实现上,集群中的物理节点使用 Shared Disk,这可能是 NFS 或 iSCSI 协议背后的存储集群。通过共享存储,primary 虚拟机不需要将磁盘变化同步给 backup,这一方面减少了状态同步的开销,一方面又可以为存储集群配置单独的容错策略,从而和应用解耦。
那么 backup 如何知道 primary 发生错误导致退出了呢?首先,primary 执行过程中遇到的中断也同样会被发送给 backup,而在所有的中断中,时钟中断是会定期发生的,所以如果 primary 能够正常执行,由于时钟中断的存在,backup 不会很长时间收不到 Log Entry。反过来说,如果 backup 很久(理论上指大于“两次时钟中断之间的间隔时间加网络传输时间”的时间,但论文中说通常设为几秒)没收到 Log Entry,那么就可以认为 primary 出了问题,此时 backup 会接替它成为 primary。除了这个机制,vSphere 还让 primary 和 backup 的 Hypervisor 保持心跳检测,以进一步检测 primary 的问题。
这一切都显得非常美好且合理,但是正如前文所述,除了让两个复制状态机保持接收相同的确定性输入外,它们还需要从一个相同的状态开始接收才可以保证它们进入到相同的新状态。但是当 primary 异常,backup 成为新的 primary 时,系统需要创建一个新的虚拟机并让它成为新的 backup。这个新的虚拟机要如何才能达成与当时的 primary 一致的状态呢?这就是 VM VMotion 的工作了。
VMotion 原理
VM VMotion 需要做到的是将某个虚拟机从其所在的源物理机上迁移到另一台物理机上,在迁移过程中不需要完全停机,虚拟机的使用者也很难感知到这个迁移动作的发生。
那么迁移具体指什么呢?本质上讲,这个过程是在另一台物理机上启动一个新的虚拟机,但是这个虚拟机的 cpu、外设、网络、硬盘、内存状态都与源虚拟机完全一致。如果将源虚拟机删除,那么这被称为迁移,如果保留源虚拟机,那么这被称为克隆。VM FT 使用的是克隆,但是其原理和迁移几乎是一样的,所以后面的讨论中以迁移举例。
大体而言,VMotion 会经历如下步骤:
- 选定需要被迁移的虚拟机以及目标物理机;
- 在保持虚拟机运行的状态下,预复制(Pre-copy)虚拟机的内存到目标物理机上;
- 暂停虚拟机的运行,然后复制除内存外的其他状态,这通常只需要很短的时间;
- 复制剩余的内存,然后在目标物理机上让虚拟机运行。
可以发现在整个过程中,有两个阶段都是针对内存的复制。之所以会有这样的情况,是因为内存其实是最难被复制的状态,因为首先内存的容量通常都比较大,这使得先暂停虚拟机再复制内存的方式变得不可行,因为复制所需的时间会很长,导致虚拟机停机的时间也会很长,这对虚拟机中的应用而言是不可接受的。另一方面,内存又是一个会随着虚拟机的运行而不断变化的组件,所以虚拟机是一定要被暂停的,否则传输变化的速度很难超过产生变化的速度,这样永远都不能完成迁移。
那么为了尽可能减少对虚拟机内应用的影响,就需要让虚拟机暂停的时间尽可能少,如何做到这一点呢?答案是局部性原理。具体而言,操作系统把内存按页划分,在很短的时间中内存的变化会聚集在几个页面里。基于这个原理,我们就可以在不停机的状态下先将完整的内存空间复制到目标物理机上,由于完整的内存很大,这通常是一个比较耗时的操作。而每个页面在传输后一旦发生变化就会被记录下来,这对一个全虚拟化平台而言可以很容易地做到。在上面的第 3 步时,为了复制除内存外的其他状态(比如虚拟 cpu 中各个寄存器的值),就需要将虚拟机暂停,到此为止我们已经记录了一些变化过的页面,而由于虚拟机已经暂停,从此之后就不会再有变化的页面。此时系统就可以将这些变化的页面传输到目标物理机上,如前所述,由于局部性原理,这些页面的数量通常是比较少的,所以传输它们并不会花太多时间。
相较于内存,网络和存储就没有那么困难了。尤其是存储,因为我们使用 Shared Disk,所以只需要让被复制的虚拟机连接存储集群就可以了。而对于网络而言,由于虚拟机的网卡是被 Hypervisor 模拟出来的,多个虚拟网卡可能共享同一个物理网卡,所以不管迁不迁移,物理网卡都照常首发网络包,只是它到虚拟网卡的映射被 Hypervisor 修改,使得网络包被发送到了新的虚拟机上。
由于网络流量可以平滑地被迁移到另一台虚拟机上,而这台虚拟机又有着与源虚拟机完全相同的内存状态,这意味着它们打开的 TCP 连接等状态也是一致的,所以应用并不会因为迁移操作而受到什么影响。
所以总结来说,VMotion 可以创建一个与源虚拟机完全一致的复制虚拟机,这使得复制状态机“一致的初始状态”的条件就可以通过它来达成了。
VM FT 的一些细节
Hypervisor 为开启 VM FT 的虚拟机维护了一个 Buffer 用于发送和接收 Log Entries。Primary 将状态变化(具体内容见下文)封装成确定性的 Log Entry,然后写入到 Buffer,通常情况下它完成这个写入就可以继续执行。Buffer 有点类似于 TCP 的滑动窗口,里面的 Log Entry 会被异步发送到 backup 的 Buffer 中。每当 backup 处理一个 Log Entry,它就会返回一个 ACK 给 primary,论文中重点强调了这个 ACK 对 Output Rule(见下文)的作用,但我猜 primary 虚拟机的 Buffer 中 Log Entry 应该仅在收到 ACK 时才会被删除,从而留出空间放新的 Log Entry。因为 TCP 只能保证网络包被送达,但是不能保证里面的内容被放入 Hypervisor 为 backup 提供的 Buffer 中。
所以,由于发送速度和处理速度的不均等,primary 的 Buffer 可能会满,backup 的 Buffer 也可能会空。当 backup 的 Buffer 为空时,它需要被暂停执行,与之类似的,当 primary 的 Buffer 为满时,它也需要被暂停执行。在这个过程中,backup 的暂停不会对上面的服务产生影响,因为用户仅与 primary 打交道,他甚至意识不到 backup 的存在,但 primary 的暂停却实打实地会影响到用户的体验。为了避免 primary 比 backup 快出太多,系统会检测它们之间的距离,并在超过一定值时降低 primary 的执行速度,等 backup 追赶上 primary 时再将速度恢复。
说了这么多,那么 Log Entry 到底包含什么内容呢?论文对此并没有给出说明,但 VM FT 是基于复制状态机模型的,所以它不会传递诸如寄存器的值、内存状态等,这些状态的同步通过让两台虚拟机接受相同的确定性输入与事件来达成。 如 2.1 小节所言,输入主要指网络包、读盘、键盘鼠标输入等,事件则主要指中断。其实由于默认配置下 backup 并不会真的读盘,所以它会“读到什么内容”也是通过 Log Entry 来同步的,即如果 primary 读盘获取到了内容 ABCD,那么这个内容会被写入 Log Entry,backup 的 Hypervisor 会通过模拟来让 backup 以为自己读盘并获取到了 ABCD 的内容。与之类似的,由于 backup 也不会收到其他的外设发送的内容,所以这些信息也是通过 Log Entry 来显式传递并被模拟的。
对于中断,要求要更严格一些。首先,操作系统本身其实就是一个被各种中断所驱动的大循环体,所以要想保证两个操作系统的状态一致,那么“在什么指令处发生了什么中断”必须是严格一致的。怎么保证这一点呢,我推测每个 Log Entry 都记录了它被发送时 primary 执行到了什么阶段,而 backup 在重放这个 Log Entry 时,最多只能执行到同样的阶段,也就是说,Log Entry 对 backup 而言就像是调试代码时的断点,backup 的执行并不是连续的。即便 primary 不接受任何的外部输入,由于时钟中断的存在,backup 也能以此来同步 primary 的执行。
此外,虚拟机在读盘时可能会使用 DMA 等异步传输技术。这意味着在虚拟 cpu 收到中断前,有一块内存区域是被协处理器写入的。如果此时我们主动去读取这部分内存,那么由于并发读写的原因获得的结果就会是不确定的。为了避免这样的情况,VM FT 使用 bounce buffer 来处理。具体来说,它使用另一块内存空间用于供协处理器使用 DMA 来读写内容,这块空间对虚拟机而言是不可访问的,在读盘结束时协处理器会触发中断,此时由 Hypervisor 主动将这块内存中的内容拷贝到虚拟机内存中供应用访问,这份内容同样会以 Log Entry 的形式同步给 backup。这其实有点像 MVCC,在整个过程中,虚拟 cpu 与协处理器接触的区域是不一样的,所以它们互不影响。
从上面的描述中可以发现,primary 几乎以异步的方式使用 Logging Channel 来向 backup 同步状态,在之前讨论 GFS 时我们提到,异步同步的缺点在于不能确定操作什么时候被同步以及是否同步成功。那么这种方式会不会造成什么问题呢?考虑这样一个场景,primary 从硬盘读取一些内容,再在相同的地方写入新的内容。这时由于 primary 和 backup 的执行存在时间差,可能 backup 会在 primary 执行写入之后才进行读取,那么此时 backup 读到的内容是否会与 primary 读到的不一致呢?在我们当前的所有讨论中,都以 VM FT 的默认实现方式为准,这种实现方式中 backup 并不会真的去读盘,它读取到的内容实际是被 primary 显式传输再被自己的 Hypervisor 模拟的。也就是说,primary 读取到内容 123,它会发送“让 backup 在 X 这个执行阶段从硬盘中读到 123”这种语义的 Log Entry,backup 的 Hypervisor 在收到它时,会欺骗 backup,让它以为自己真的读取了硬盘并从中获取了 123 这个内容。因为 backup 并不会读盘,所以即便此时硬盘上的内容被更新为 456,也不会对 backup 在 X 这个执行阶段产生任何导致不一致的影响。
另一方面,primary 也不是完全异步地在使用 Logging Channel 的,除了在 Buffer 满时要停下来等待,VM FT 还设置了 Output Rule 的限制。这个限制要求 primary 的所有输出都要在收到 backup 的 ACK 时才能被发送,所以如果用户与 primary 交互并获得了它的反馈时,这个反馈前的所有 Log Entry 都被 backup 重放过了。而这其实就足够了,因为在此之后即便 primary 崩溃了,backup 与 primary 的不一致也不会被用户感知到,因为这种不一致是从上一次的输出开始的,而下一次的输出取决的是当时的 primary,谁又能知道它是谁呢?