The Design of a Practical System for Fault-Tolerant Virtual Machines论文解读
论文 The Design of a Practical System for Fault-Tolerant Virtual Machines
MIT 6.5840 的第二节课讲到了高可用的问题, 高可用是一些生产系统中的重要保障, 当客户端请求的机器发生故障的时候, 如何避免客户的请求失败, 请求继续正确执行呢? 日常我们浏览网页的时候, 所需要后台的高可用实际上可能并不是很严格, 请求失败, 我再请求一次就可以了. 比如, 页面刷新失败, 我再刷新一次就好了. 但是也有很多系统, 一笔交易由于一致性的问题, 不允许重新发一起一次. 例如银行系统进行一次转账, 或者一个后台计数器. 一些系统需要保障零宕机以及没有数据损失. 这时就需要用到严格的高可用.
这篇文章是提出了在虚机中实现高可用的一种方式, 也就是 VMware FT.
Introduction
在生产系统中, 后台的服务肯定不能仅在一台机器上执行, 如果是这样的话, 那么当这台机器发生故障的时候, 整个系统就会宕机. 如何保障系统的高可用呢? 一种常见的方式就是 primary/Backup 架构. 简单点说就是主从复制, 很多数据库中都存在主从复制机制来进行数据备份与保障数据库的高可用.
主从复制的两种方式
主从复制的基本思想是为生产机器(primary) 创造一个时刻完全一致的备份机器(Backup) 机器, 当 primary 机器发生故障的时候, 可以无缝切换到 Backup 机器, 用户无感知, 外界应用也无法感知. 通常有两种主从复制机器的构建方式:
- 将 primary 机器的所有信息备份到 Backup 机器, 包括 CPU 的使用情况, 实时的内存信息, 寄存器信息等等, 这种方式的最大问题是, 整个主从拷贝的过程需要巨大的网络带宽.
- 另一种使用更小带宽的方式则是将机器看作是状态确定的状态机, 在运行的过程中保证 primary 和 Backup 始终处于相同的确定的状态, 也就是状态同步. 使用状态同步的方式遇到的难点是, Primary 会接收到更多外部请求, 以及会执行一些不确定性的操作, 例如读取时钟, 或者处理中断, 这些不确定的操作(non-deterministic) 会导致状态的不确定性, 也就导致 Backup 机器无法使用确定的状态转换.
虚机上使用主从复制
在理解虚机上主从复制之前, 我们需要先了解一下虚拟化相关的知识. 在文中也会多次提到, 并且会使用相关的原理.
在虚拟化(Virtualization)中, Hypervisor(虚拟机监控器, 也叫 VMM, Virtual Machine Monitor)是虚拟机系统的核心.
它的作用就是:
- 直接管理硬件资源(CPU、内存、磁盘、网络等)
- 将这些资源分配给多个虚拟机
- 隔离不同虚拟机的运行环境, 保证它们互不干扰
- 提供虚拟化接口, 让虚拟机就像在独占一台物理机上运行一样.
在这篇文章中使用的是 Type 1 类型的 Hypervisor, VMware ESXi. 这种虚拟监控器直接运行在物理硬件之上的软件层, 没有宿主操作系统. 性能最好, 常用于数据中心和服务器虚拟化. Hypervisor 的作用就是可以捕捉到在 Primary 上执行的不确定性操作(non-deterministic), 然后将这些操作在 Backup 机器上重放这些操作即可. 那么传递这些不确定性操作信息的就是 Logging Channel. 下面这张图就是这篇文章提出的 VMware FT 的基本框架.
VMware FT 高可用需要满足的条件
我总结了以下 VMware FT 高可用需要满足的必要条件, 也是使用两台虚机保证高可用的必要条件, 但是生产系统往往更加复杂, 因此这些只是必要不充分条件:
- Primary 与 Backup 需要保持状态一致, 至少在某一个确定性的时刻状态一致, 可以在这个确定性的时刻进行主备切换
- 主备的状态切换应该是对客户端无感知的, 在一个请求未完成的时候发生切换, 请求应该在 Backup 上正确执行, 并返回结果. 主备切换不允许跨请求, 请求不允许出现失败.
- 对客户端来说, 只有 Primary 可见, Backup 不可见.
- 在发生一次主备切换后, Backup 机器成为 Primary 机器, 随后选择一台机器作为新的 Primary 机器的 Backup 机器.
- 如果 Primary 和 Backup 存在共享的 IO 资源或者其他资源, 需要避免出现竞态的问题. IO 需要保持一致.
VM FT 的基本设计思想与设计方案
上面的图一就是 VM FT 的基本设计方案, 它的基本设计思想可以总结如下:
- Backup 和 Primary 需要保持同步, 执行相同的命令, 仅存在很小的时间空隙.
- 所有的外部操作, 连接互联网, 接受与发送报文, IO 操作等, 都在 Primary 上进行, Primary 与 Backup 共享一个磁盘.
- Primary 和 Backup 之间的重放机制使用的是 Logging Channel 的方式, 可以保证 Backup 执行和 Primary 完全相同的不确定性操作(non-deterministic operations). Backup 的输出会被 Drop, 不影响系统整体的输出.
- Primary 和 Backup 之间有一个特殊的协议, 使用该协议通信
- 为了保证同一时刻只有 Primary 或者 Backup 运行, 避免出现 split-brain 的问题, VM FT 使用了一个共享磁盘的方式.
Deterministic Replay Implementation
即使 Primary 机器上有很多不确定性的操作, 但是为了保持 Primary 和 Backup 的一致性, 需要将不确定性操作(non-deterministic operations) 转化为确定性的重放. 使用下面的机制:
如果两个确定性的状态机在相同初始状态且接受相同顺序的输入, 那么这两个状态机会产生相同的输出结果. 基于这一点, 副本虚拟机服务器可以被当做是状态机的副本. 一个虚拟机有一系列输入(网路包、磁盘读取、键盘和鼠标输入)、以及一些非确定性事件(例如虚拟机的中断)和非确定性操作(读处理器的时钟周期计数器), 这些不确定性的操作带来的挑战如下:
- 要将所有的输入和必要的非确定性事件转化为确定性事件, 以保证 Backup 的操作和 Primary 一致, 并准确在副本上执行以上操作
- 副本的 Replay 操作不能阻碍 VM 性能
- X86 架构处理器有很多未定义的事件, 存在副作用
- VMware deterministic replay 将主虚拟机的输入和可能的非确定性操作以日志项的形式写到发送到副本虚拟机, 这使得副本虚拟机能够执行 replay. 对于非确定性事件(例如定时器和 I/O 完成中断), 能在这些事件对应的指令上进行实时记录, 这一部分事件的记录, VMware deterministic replay 利用了 AMD 和 Intel 中的性能计数寄存器来实现.
另外, 日志项中包含了以下内容:
- 事件发生时的指令号
- 事件类型, 网络输入或者是其它指令
- 数据, 如果是不确定指令, 则该数据时 Backup 中的执行结果, 这样使得不确定性事件能在 Primary 和 Backup 上结果保持一致
FT Protocol
通过上述的重放与 Logging Channel 的功能, 副本(BakcUP机器) VM 能够从 logging chanel 中读取主(Primary) VM 的各种操作,并实时在 Backup 中执行相同的操作. 本届定义了 FT Protocol, 本质上是用来定义执行与输出的异步执行. 这是因为 Primary 在输出返回客户端的时候, 可能会出现中途宕机的情况, 在论文中定义了下面的需求:
Output Requirement: 如果 Primary 发生故障后 Backup 接管后, Backup 必须接管原来 Primary 已经完成或部分完成的工作, 对外界的输出要保持一致. 即 failover 期间, 上述操作对客户端是透明的, 客户端不会被中断服务或者是察觉到不一致.
异常情况: 当 Backup 接受并执行 Logging Channel 中的日志项的时候, Primary 发生宕机, 此时, Backup 无法直接上线, 无缝替换 Primary, 必须等待 Primary 将操作日志写完到 Logging Channel, 并且 Backup 执行完毕后才可以上线接管, 并且在这个过程中, 还可能存在一些不确定性的事件, 例如计时器的中断等. 导致 Backup 的状态与 Primary 的状态不一致.
因此本文定义了 Output Rule 如下, 用来规范 Primary 的输出.
Output Rule: 如果 Primary 欲产生到客户端的输出, 其必须延迟这项输出操作, 直到 Primary 收到来自于 Backup 的关于该输出的确认日志项. 流程如下图所示:
上图说明了, 必须延迟 Primary 的输出, 知道收到 Backup 的确认信息, 而输出功能恰好是 IO 异步的, 所以采用 ouput rule 的 Primray 不会停止执行并等待 ACK, 其仅仅推迟对外部的输出, 同时又由于 OS 支持的非阻塞磁盘和网络输出, Primary 继续能执行其它程序的情况下异步完成输出操作.
Detecting and Responding to Failure
使用上面的机制与一些原理, 可以执行故障的检测与重启.
故障检测的方式:
- Primary 故障检测的方式是主动发送的, 也就是 Primary 周期性地向 Backup 发送心跳包(UDP), 如果 Backup 在超时时间内没有收到心跳包, 就会怀疑 Primary 发生了故障.
- Backup 的故障检测是通过上面的 logging channel 中的流量和 Backup VM 发送至 Primary 的 ACK. 如果心跳和流量停止时间超过了设定的超时时间, 则会被判断为发生故障.
故障后的操作:
- 如果 Backup 发生故障, 则 Primary 停止向 logging channel 中写日志项, 继续正常执行, 并且需要新找一台 VM 作为 Backup.
- 如果 Primary 发生故障, Backup 执行完 replay 后, 被提升为新的 Primary, 上线并为客户端提供服务. 此外, 在 Backup 提升为 Primary 过程中, 其需要进行一些和设备相关的操作, 例如会在其网络中广播现在的 Primary 的 MAC 地址, 以及重新发起某些磁盘IO.
脑裂问题的原因与解决:
脑裂问题通常出现在网络分区或心跳丢失导致双方误判, 此时 Primary 和 Backup 都认为对方发生故障, 此时 Primary 和 Backup 都上线为客户端提供服务, 造成脑裂问题. 为解决该问题, 必须要确保发生以上故障时, Primary 和 Backup 仅能有一个上线提供服务, 本论文使用了虚拟磁盘的共享存储来解决问题, 两台虚拟机(Primary、Backup) 都能访问一个共享存储区(类似于一个 small metadata block). 当它们检测到故障、要上线时:
- 尝试对共享存储执行一个 atomic test-and-set 操作;
- 如果操作成功, 说明我是第一个上线的, 于是可以成为新的主机;
- 如果失败, 说明另一台已经抢先上线, 那么我就不能提供服务.
如果共享存储无法访问, 由于 Primary 和 Backup 都共享该存储, 则意味着此时 VM 也无法工作. 因此, 使用共享存储解决脑裂问题不会引入额外的不可用.
Practical Implementation of FT
以上介绍的是 FT 协议的理论基础, 本章节介绍了使用 Hypervisor, VMware ESXi 虚拟机实现改协议与机制的实践方法.
Starting and Restating FT VMs
当 Primary 或者 Backup 发生故障时, 需要将系统重新启动, 并且对客户端无感知, 因此实践过程中对 Primary 与 Backup 的故障使用下面不同的操作:
Backup 挂了:
- Primary 继续执行, 客户端无感知.
- Cluster Service 发现 Backup 故障后:选择一个新的宿主机;
- 在该宿主机上 clone 当前 Primary 的状态;
- 重新建立 logging channel;
- Primary --> logging mode, New Backup --> replay mode;
- 冗余恢复完成.
Primary 挂了:
- Backup 执行 replay --> 成为新的 Primary;
- Cluster Service 再选择另一台宿主机;
- 在上面启动新的 Backup;
- 重建 logging channel;
- 再次恢复冗余.
Primary 与 Backup 都是部署在vSphere 集群上, 这是因为集群服务能看到整个物理资源池, 它会基于当前宿主机的 CPU、内存、负载等情况选择一个最合适的宿主机, 避免重建副本对当前正在运行的主机造成负担. 这个迁移的过程就称为 VMotion.
Managing the Logging Channel
Primary 与 Backup 之间的同步通信本质上使用的是 Channel, 由于 Primary 与 Backup 处理 Logging 的速率不同, 因此需要使用下面的方式管理 Channel, 视图如下:
- 如果 Backup 的 log buffer 读空了, 其停止执行, 直至收到收到新的日志项.
- 如果 Primary 的 log buffer 写满了, 则会减慢 Primary 的执行. 当时在此时, 可能会出现客户端无法收到 Primary 的回应(Primary 必须写完 Channel 才会输出).
- 出现 Primary 的 log buffer 写满的情况可能如下: Backup 执行速度过慢, 造成消耗日志慢. —-> 这种情况很有可能是由于物理宿主机上可能还有虚拟机实例, 造成 Backup 所得到的 CPU 和内存资源不足而造成的.
- 如果 Backup 执行速度过慢, 那么 Primary 出现故障后, failover 的时间等于错误检测时间加上 副本replay 所有日志项的时间. 如果 failover 时间过长, 很有可能导致服务不可用.
- 因此, Primary 和 Backup 的执行速度基本要一致. 在本论文中的协议中, 其日志项包含了 Primary 和 Backup 之间的执行延时时间, 如果执行延迟时间过长, 则 VMware FT 分配给 Primary 的 CPU 时间片减少. 这一实现使用了 feedback loop, 依据执行延迟时间来动态分配 CPU 资源.
Operation on FT VMs
FT VMs 中的大部分操作都会通过 Logging Channel 从 Primary 同步到 Backup 机器上, 但是 VMotion 操作不是简单的虚拟机操作, VMotion 操作是在特定的场景下由集群服务主动触发. 它的主要功能是在不中断虚拟机运行的情况下, 将一个 VM 从当前物理宿主机迁移到另一台宿主机上. 通常有以下几种场景会导致 Primary 执行 VMotion 操作:
- 资源调度或负载均衡(最常见), 如果当前宿主机过载或某台主机需要进行维护或集群希望让资源使用更均衡
- Backup 故障后的冗余重建(也可触发 Primary 的 VMotion), Primary 需要建立新的 logging channel在某些情况下(比如网络拓扑优化), Cluster 可能同时决定: 把 Primary 一并迁到与新 Backup 更接近的宿主机上, 以减少同步延迟.
- 宿主机维护或下线
Primary 的 VMotion比普通 VMotion 更复杂主要是因为 IO 与 Logging Channel 的原因, 普通 VMotion 只迁移一个 VM. FT 的 Primary VM 迁移时, 还需要让 Backup 暂停, 切换 logging channel, 确认所有 I/O 一致性, 确保所有 I/O 操作已完成. 因此在 VMware FT 中, 在 Primary 在完成最后一个 I/O 时, 会在日志中写入一个特殊的 log entry, Backup 在 replay 到这一条日志时, 就“确定”所有 I/O 都完成, 然后它才开始安全地执行 VMotion.
Implementation Issues for Disk IOs
为什么在 FT VMs 会存在 IO 冲突呢?
这是因为 Primary 和 Backup 在对宿主机映射时, Primary 和 Backup 的 VM 内部的内存页在映射到宿主机的物理内存页的时候, 可能会出现覆盖的部分. 例如 Primary 上的 IO 的 DMA 内存块与 Backup 上的 IO 的 DMA 内存块在宿主机上使用的是宿主机的相同地址的 DMA 内存块. 另一个潜在的共享是, Primary 和 Backup 在访问宿主机的磁盘 IO 与文件系统时, Primary 和 Backup 访问的虚拟磁盘是实际上同一个文件/块设备.
IO 出现冲突的场景:
Primary 发起异步写, Backup 在回放日志时同时执行写, 由于 Primary 的 I/O 写是异步的, 会导致I/O 顺序不一致, 页缓存状态不同. 另一种可能是 Primary 和 Backup 在写 IO 的时候, Primary 和 Backup 的写线程共享同一页缓存或 DMA buffer, 在 DMA 上造成物理页的冲突.
解决的办法:
在 Hypervisor 中可以探测这种很少发生的 IO 冲突, 强迫冲突磁盘操作在 Primary 和 Backup 上使用顺序执行.
Implementation Issues for Network IOs
对于某些优化操作, hypervisor 异步更新 VM 网络设备的状态, 但异步更新无疑给 VM 的状态增加了不确定性. 因此, 本论文把异步更新 VM 环缓冲区操作修改为 hypervisor 的陷入操作来处理. 在此之上, 本文还做了如下两方面的优化:
- 将数据包分组. 收到一组数据包后再触发陷入操作, 避免了多次陷入操作带来的额外开销.
- 减少数据包传输延迟. 本文通过减少线程上下文切换来达到, 当收到 TCP 数据时, hypervisor 允许函数注册到 TCP stack, 随后在延迟执行上下文中被调用.
Design Alternatives
这一节(4.1 Shared vs. Non-Shared)实际上是 VMware FT 设计的一个关键权衡点, 讨论了 "主备 VM 是否共享同一个虚拟磁盘" 的两种架构方案.这两种方式在一致性保证、性能开销、部署复杂度上有非常不同的取舍. 我把他们总结如下:
| 特性 | 共享磁盘(Shared Disk) | 非共享磁盘(Non-Shared Disk) |
|---|---|---|
| 磁盘访问方式 | 主备 VM 访问同一个共享虚拟磁盘(VMDK) | 主备 VM 各自维护一份虚拟磁盘副本 |
| 磁盘写入 | 仅 Primary 写入;Backup 不写入 | 主、备均可写(内容保持同步) |
| I/O 一致性保证方式 | 依赖 Output Rule 确保主 VM 的写入时机正确 | 因磁盘独占, 不需 Output Rule |
| 脑裂处理方式 | 利用共享磁盘上的 atomic test-and-set 来仲裁谁上线 | 需要 第三方仲裁节点(witness server) |
| 主备间距离限制 | 要求存储共享(距离不能太远) | 可跨机房、跨地区部署(网络容错更高) |
| 磁盘同步 | 无需同步(天然一致) | Backup 故障恢复后需显式磁盘同步 |
| VMotion 难度 | 只需迁移主备内存和日志通道 | 需迁移磁盘状态与执行状态 |
| 实现复杂度 | 较低(依赖底层共享存储) | 较高(需额外同步机制与一致性控制) |
| 性能 | 高:读写延迟低, 日志通道负载可控 | 稍低:磁盘同步开销 + 日志更复杂 |
| 容错性 | 受限于共享存储的可用性 | 即使共享存储不可用仍可运行 |
Summary
学习这篇文章主要还是提升了我对后台系统的认识, 结合目前生产系统, 学习了更多的底层知识, 算是简单的高可用的入门吧, 总结来说, FT VMs 在高可用与容灾方面的优点是, 性能损耗低, 透明.
- FT VMs 将 Deterministic Replay 与 虚拟化平台集成;
- 提出 Output Rule 以保证外部一致性, FT Protocol;
- 设计了轻量级日志通道与动态反馈机制, Logging Channel;
- 解决了 I/O 非确定性与脑裂问题;
- 展示了一个可商用级别的 VM 容错系统.

浙公网安备 33010602011771号