Lec 4 链式复制(Chain Replication)
课件:Chain Replication (l-cr.txt);论文:CR (2004);FAQ
本讲两条线:① 链式复制(本讲正题,强一致 + 负载分摊的主备变体);② VMware FT(一个机器指令级的主备复制案例,见文末「附」)。
课件精讲:链式复制
要做强一致(线性一致)+ 容错的复制存储,有两条路:① 把所有操作过共识(Raft/Paxos,自洽但复杂);② 分离「配置服务」+「数据复制协议」——配置服务(用 Paxos/Raft)只存成员关系(谁是 head/tail、分片在哪),数据复制协议可以做得简单,不必自己处理脑裂。链式复制走第二条路。
- head 挂:配置服务把第二个服务器升为新 head(链上其余已有全部状态,重同步简单);只到过旧 head 的请求由客户端超时重发。
- tail 挂:把倒数第二个升为新 tail(它必有旧 tail 的全部更新);读立即切到新 tail。
- 中间挂:配置服务把它的前驱直接接到后继;前驱补发未转发完的更新。
FAQ(链式复制 答疑整理)
来自
cr-faq.txt。
- 实践中用链式复制还是 Raft/Paxos? 通常分层并用:Raft/Paxos 管配置元数据(防脑裂强),链式/主备管实际数据复制(对大数据更简单高效)。
- CR 如何应对分区/防脑裂? 故障时暂停、等配置服务重配置;论文承认分区下误判 head/tail 失败会有脑裂风险,建议用租约式 tail,但细节未完全解决。
- CR vs Raft/Paxos 的取舍? CR 性能好(head 只发一个副本、读走 tail),但任一副本故障就要暂停等重配置(~10 秒);Raft/Paxos 能容忍慢副本、多数可用就继续。
- CR 比 GFS 那种主备快很多还是慢? 两副本时差别小(CR 因 tail 直接回复可能写更快);三副本及以上,CR 把网络负载摊到各服务器而非压在 primary,可能减瓶颈但写延迟略增。
- §5.2 的"多条链"是什么意思? 把不同数据分片分布到各服务器,每台在不同链里轮流当 head/中间/tail,当三条链活动量大致相等时负载被均衡。
- §3 假设 fail-stop,现实如此吗? 现实有非 fail-stop(拜占庭)故障(硬件错、数据损坏、软件 bug);论文结论只在"全是 fail-stop"时成立,拜占庭要 PBFT 这类设计。
- 还有哪些系统用 CR? Amazon EBS、Ceph Rados、Google Parameter Server、COPS、FAWN。
- CR 对延迟不友好吧? 部分对:写延迟随链长增加;但读延迟很低(只 tail,无 head 瓶颈)。
- rndpar 客观上比 ring 好吗? 服务器少时 ring 略好(随机布局下 N 台同时坏构成整条链的概率更高);服务器多时 rndpar 靠广泛分摊修复工作而占优。
- 倒数第二台等 tail 的 ACK 丢了会怎样? 只有倒数第二台等 tail 的 ACK 并重传;因所有更新按 head 定的顺序执行,重发重复更新无害(顺序保证不需幂等)。
- 强一致在现实中多重要? 弱一致会让应用编程复杂;证据表明大家更看重强一致(如 S3 一致性增强让 Dropbox 大幅简化代码,Google 渐进提供 Spanner 这类更强一致)。
- 为何不用并行的中间服务器降延迟? 并行中间大幅增加复杂度(head 挂时选谁当新 head 需新逻辑),串行链的"下一个就是接替者"清晰无歧义。
- "可靠 FIFO 链路"是什么? 即"普通网络上的 TCP",保证有序投递;每个更新带 head 的序号以强制处理顺序,链路本身仍可能丢包失败。
附:主备复制案例研究 — VMware FT(容错虚拟机)
阅读论文
The Design of a Practical System for Fault-Tolenrant Virtual Machines,
这篇论文的价值在于它展示了一个很清晰的主备架构,并引出了将遇到的几个关键主题,例如状态机复制、输出规则、故障切换以及主节点选举。令人印象深刻的是,这种容错能力是在机器指令层面完成的,意味着可以对应用进行VM级别的复制,而不需要应用程序设计者参与,这是之后的所有系统设计都难以做到的。
本节的目标是实现系统的高可用,也就是说,即使集群内部一台机器发生故障,仍然能够提供服务。采用的方法就是复制,这是一种经典的容错技术,基本思想就是维护多份数据的副本。
复制技术
能/不能解决的问题
复制技术能解决,单一副本停止工作( fail-stop),比如风扇坏了导致 CPU 过热自动关机、有人拔了电源线、软件发现磁盘满了自动停止等等。
但复制通常不能很好地应对程序漏洞或人为误操作(因为这些往往不是 fail-stop,也可能在所有副本中表现一致),也不能单独应对自然灾害或大范围断电,除非副本部署在物理上分散的位置。
复制状态机
一种常见的复制方式是复制状态机(RSM):客户端将操作发送给主节点,主节点对操作排序并同步给备份节点,所有副本以相同初始状态、相同顺序、执行相同操作,在系统是确定性的前提下,最终会得到相同的结果。VMware FT 采用的正是这种机制。副本越多越安全,但也越昂贵。大多数系统通常运行 3 到 5 个副本来兼顾成本和容错。而本论文中的 VMware FT 仅使用两个副本,可以容忍任意时刻最多 一个故障。
几个关键问题
- 如何定义系统状态和操作?
- 主节点是否必须等待备份节点?
- 备份节点如何决定接管?
- 切换期间是否会出现异常?
- 如何让一个新的备份节点快速赶上当前状态并恢复复制?
- 我们希望什么层级上实现副本的一致性?
- 一种方式是在应用层级保持一致,比如数据库的表结构和内容。这种方式效率较高,主节点只需将高层操作发送给备份节点,例如 GFS 就是这样做的。但缺点是需要应用程序本身理解并支持容错机制。
- 另一种方式是在机器层级保持一致,即保持副本的寄存器、内存等状态完全一致。这样可以在不修改现有应用程序的前提下进行复制,支持任意操作系统和服务器软件。但这种方式要求能够捕获并转发所有机器事件(如中断、网络数据包等),并对底层“机器”做出修改以实现事件流的发送与接收。
VMware FT基本原理
系统架构中,应用程序和操作系统运行在虚拟机中,虚拟机由底层的虚拟机监控器(Hypervisor/VMM)支持。在 FT 架构中,有两台物理机器,分别运行主虚拟机和备份虚拟机,初始时两者的内存、寄存器等状态完全相同,包括相同的操作系统和应用软件。绝大多数机器指令(例如加法)在主备上会产生相同的效果,因此在大多数时间内,主备之间无需额外工作即可保持同步。 但一旦发生可能导致主备执行路径出现差异的事件,主机就必须将相关信息发给备机,以避免状态发散。这些事件通常是不确定性的,不能仅由程序状态决定,比如:读取当前时间(非确定性指令);来自外部世界的输入(网络数据包、磁盘读数据),这些通常表现为 DMA 的数据加上中断;中断的具体触发时机;多核并发带来的竞态条件并不是 VMware FT 要考虑的问题,因为其复制的是单核执行模型。
为什么状态发散是灾难性的?
Solution: 因为如果主机发生故障而备机接管,备机的状态如果不同,会导致系统行为异常或数据错误。例如,一个课程作业提交服务器使用了有缺陷的 FT 方案。主机在午夜定时器触发前收到作业,因此记录为“按时提交”;但备机在定时器之后收到同样的数据包,因此记录为“迟交”。主机崩溃后,备机接管,老师看到的是“迟交”的状态,学生因此受到不公正的处理。为了避免这种问题,备机必须在指令流中的相同位置、以相同顺序接收到与主机完全一致的事件。这也是 VMware FT 的核心保障机制之一
日志通道
主机通过网络将所有可能导致执行发散的事件发送给备机,这些事件以日志项的形式通过日志通道传输,包括中断、网络数据包、磁盘读取的数据等。备机并不直接从硬件接受这些事件,而是由FT从日志中“喂给”它,并且备机不会对外发送
论文阅读:VMware FT (2010)
摘要
我们实现一个企业级的商业系统,系统通过在另外一个服务器上备份虚拟机的执行,实现了虚拟机的容错。特点是性能损失小(小于10%)、同步数据带宽需求低(20Mbs)、易于使用。
1. 介绍
实现容错服务器集群的一般方法就是主从复制(primary/backup)技术,其中备服务器总是能够在主服务器失效后能顺利接管。备用服务器的状态几乎需要在任何时候保持与主服务器一致,如此才能够立即接管,通过这样的方式故障对外部客户端来说是被隐藏的。一种方式主服务器的所有状态变更几乎持续地发送到备份服务器上。然而这种方式所需的带宽会非常大。
本文提出了能大幅减少带宽消耗的服务器复制方法,称之为状态机方法。其核心思想就是将服务器建模为确定性状态机,并通过让他们从相同的初始状态开始,并按照相同的顺序接受相同的输入请求来同步服务器。然而,由于大多数服务器或服务都包含一些非确定性的操作,因此需要额外的协调机制来确保主服务器和备份服务器保持同步。
要确保物理服务器的确定性执行,需要进行协调,在处理器频率不断提高的情况下很难实现。相比之下,运行在管理程序(hypervisor)之上的虚拟机是实现状态机方法的理想平台。可以将虚拟机视为一个定义良好的状态机,由于管理程序对虚拟机的执行拥有完全控制权,包括所有输入的传递,因此它能够捕获主虚拟机上所有非确定性操作的必要信息,并在备份虚拟机上正确重放这些操作。确定性重放和 VMware 容错功能仅支持单处理器(uni-processor)虚拟机。
2. 基本FT设计

图1展示容错VM的基本设置。主VM和副VM运行在不同的物理服务器上,副VM与主VM保持同步和与主VM执行情况相同,基本有一点点落后。虚拟机的虚拟磁盘存储在共享存储上,因此主/备VM都能访问这些存储设备的输入和输出。只有主VM在网络上公开其存在,因此所有网络输入到发送到主VM。从主VM收到的所有输入都会通过日志通道(logging channel)发送到备VM。对于服务器负载,主要的输入流量是网络和磁盘。在2.1节我们会细节的讨论确定性响应技术,该技术确保主备VMs能够在日志通道保持同步;在2.2节我们会描述基本的FT协议的规则;2.3节我们描述我们用于探测和响应一个失败的方法。
2.1 确定性回复实现
非确定性时间(比如虚拟中断)和非确定性操作(读取处理器时钟频率总数)等影响VM的状态。这会出现3种挑战:
- 正确捕捉所有的输入和非确定性因素,以确保备份服务器能按照正确的方式执行
- 确定性地将输入和非确定性因素应用到备份服务器。
- 不降低性能的前提下完成上述工作。
x86 微处理器中的许多复杂操作具有未定义的(即非确定性)副作用。捕获这些未定义的副作用并在重放时使其产生相同的状态,构成了额外的挑战。
VMware 确定性重放(deterministic replay) 在 VMware vSphere 平台上为 x86 虚拟机提供了这一功能。确定性重放会记录虚拟机的输入以及所有可能的非确定性因素,并将其作为日志条目流写入日志文件。稍后,可以通过读取日志条目精确重放虚拟机的执行。对于非确定性操作,日志中记录了足够的信息,以便在重放时复现相同的状态变化和输出。对于非确定性事件(如定时器或I/O 完成中断),日志还会记录事件发生时的确切指令,以确保在重放过程中该事件在相同的指令位置被触发。VMware 确定性重放采用了一种高效的事件记录和交付机制,其中包括与 AMD [2] 和 Intel [8] 合作开发的硬件性能计数器等技术。
Bressoud 和 Schneider [3] 提出了一种方法,将虚拟机的执行划分为多个“epoch”(时间片),并且仅在时间片结束时传递非确定性事件(如中断)。这种epoch 机制实际上是一种批处理策略,因为逐条传递中断的开销过高。然而,我们的事件传递机制足够高效,因此 VMware 确定性重放 无需使用 epoch 机制。相反,每个中断都会在发生时被记录,并在重放过程中准确地传递到相应的指令位置。
2.2 FT协议
我们使用确定性重放来生成必要的日志条目,已记录主VM的执行过程。与传统方法不同,我们不是将日志条目写入磁盘,而是通过日志通道将其发送到备份VM。备份VM实时重放这些日志条目,为了确保真正的容错能力,我们必须在日志通道上加入严格的FT协议。其基本要求就是
IMPORTANT
输出要求: 如果主VM发生故障, 并且备份VM接管,那么备份VM的执行必须与主VM已经对外发送的所有输出完全一致。
一旦发生故障转移(failover),备份VM的执行路径可能会与主VM本应该执行的路径发生很大不同,因为在运行过程中会遇到许多非确定性事件,然而只要备份虚拟机满足输出要求,那么在转移过程中不会丢失任何外部可见的状态或数据,外部客户端也就不会察觉或不一致。
为了保证输出的一致性,我们必须延迟任何外部输出(通常是网络数据包),直到备份虚拟机已经接受到所有必要的信息,确保它可以至少重放到执行该输出操作的时刻。备份虚拟机必须已经接收到所有在该输出操作之前生成的日志条目。这些日志条目允许它执行到最后一个日志条目的位置。假设主虚拟机在执行输出操作后立即发生故障,备份虚拟机必须知道自己需要继续重放,直到执行完该输出操作,然后才能正式接管(“go live”),即停止重放,变为新的主虚拟机。如果备份虚拟机在输出操作之前的最后一个日志条目处直接“go live”,那么某些非确定性事件(如定时器中断)可能会改变其执行路径,导致它在执行输出操作之前发生状态变化,从而破坏输出的一致性。
给定上述限制,最简单的方法方法就是为每个输出操作创建一个特殊的日志条目,然后通过以下规则强制执行输出要求:
IMPORTANT
输出规则: 主VM不得向外部世界发送任何输出,直到备份虚拟机接收到并确认该输出操作相关的日志条目。
若备用虚拟机已接收全部日志条目(含触发输出操作的日志条目),则其能精确复现主虚拟机在该输出点的状态。此时若主虚拟机崩溃,备用虚拟机将正确达到与该输出一致的状态。反之,若备用虚拟机在未接收全部必要日志条目的情况下接管运行,其状态可能快速偏离,导致与主虚拟机输出不一致。
注意的是,输出规则并没有要求暂停主VM的执行。我们所做的只是延迟输出的发送,而主VM本身可以继续运行。由于现代OS的网络和磁盘采用非阻塞方式,通过异步中断(asynchronous interrupts)来指示完成情况,因此 VM 完全可以继续执行,并不会因为输出延迟而立即受到影响。所有与异步事件、输入操作和输出操作有关的信息都必须作为日志条目(log entries)发送给备份 VM,并由后者进行确认。只要遵守这个“输出规则”,当主 VM 故障时,备份 VM 就能在与主 VM 最后一次输出保持一致的状态下接管运行。

图2展示了FT协议的执行要求,主虚拟机 → 备份虚拟机:箭头表示日志条目的传输;备份虚拟机 → 主虚拟机:箭头表示日志条目的确认(acknowledgment)。主虚拟机的外部输出 必须被延迟,直到主虚拟机收到来自备份虚拟机的确认,表明备份已经收到了对应的日志条目。只要严格遵循“输出规则”,备份虚拟机在故障转移后就能保持与主虚拟机最后一次输出一致的状态。
我们无法保证在故障切换(failover)场景下所有输出都能被精确执行一次。由于主虚拟机在发送输出时未采用两阶段提交(2PC)事务机制,备用虚拟机无法确定主虚拟机崩溃是发生在发送最后一条输出之前还是之后。值得庆幸的是,现有网络基础设施(包括普遍采用的TCP协议)本身就能处理丢包和重复包问题。需注意的是:主VM故障期间,所有接受的数据包可能会丢失,无法传递至备VM,但数据包丢失可能由多种与服务器故障无关的原因导致。网络基础设施、操作系统及应用程序均内置了补偿机制,确保能妥善处理丢包情况。
2.3 探测和响应Failure
如上述所言,主虚拟机和备用虚拟机必须对另一方故障作出快速响应。若备用虚拟机发生故障,主虚拟机将接管运行——即退出记录模式(停止向日志通道发送条目)并开始正常执行。若主虚拟机发生故障,备用虚拟机同样需接管运行。由于执行存在滞后,备用虚拟机可能已接收并确认了若干日志条目,但因尚未执行到对应位置而未能消费这些条目。此时备用虚拟机必须持续从日志条目重放执行,直至消费完最后一条日志。届时,备用虚拟机将停止重放模式,开始作为正常虚拟机运行——实质上它已被提升为主虚拟机。在切换至正常模式过程中,可能需要执行某些设备特定操作以确保输出正常进行。VMware FT会自动在网络中宣告新主虚拟机的MAC地址,使物理网络交换机可定位其所在服务器。此外,新晋升的主VM可能需要重新发送I/O请求(如3.4所述)
有很多可能的方法来探测主副VM是否故障。VMware FT使用了UDP心跳。此外,VMware FT监控从主到副的日志流,和副到主的确认消息。当心跳信号或日志流量中断超过特定阈值时长(通常为数秒量级),系统即宣告故障发生。
然而,任何此类故障检测机制都可能面临脑裂(split-brain)问题。若备用服务器停止接收来自主服务器的心跳信号,可能意味着主服务器已故障,也可能仅表明正常运行中的服务器之间网络连接完全中断。倘若此时备用虚拟机接管运行,而主虚拟机实际上仍在运作,则可能导致数据损坏,并与虚拟机通信的客户端产生严重问题。因此,必须确保检测到故障时,仅有一个虚拟机(主或备)上线(go live)。为了解决脑裂问题,我们使用共享存储(shared storage),该存储保存了虚拟机的虚拟磁盘。无论是主 VM 还是备份 VM,在想要上线时,都会在共享存储上执行一个 原子性的 test-and-set (CAS)操作。
- 如果这个操作成功,那么该 VM 被允许上线。
- 如果操作失败,则说明另一个 VM 已经上线了,因此当前这个 VM 必须终止自身运行(也就是“自杀”)。
- 如果 VM 无法访问共享存储来执行这个原子操作,它就必须等待,直到能够访问为止。
注意,如果由于存储网络故障导致无法访问共享存储,那 VM 无论如何也无法执行正常业务,因为虚拟磁盘本身就是存在这个共享存储上的。因此,使用共享存储来解决脑裂问题,并不会引入额外的不可用性。设计的最后一个关键点是:一旦故障发生,并且有一台 VM 成功上线,VMware FT(Fault Tolerance)会自动在另一台主机上启动一个新的备份 VM,以恢复冗余性。
3. FT的实现
第二节我们描述了基本设计和FT协议,然而,为了实现可用、鲁棒性和自动化系统,有很多组件必须设计和实现
3.1 启动和重启FT VMs
在 VMware FT 的设计中,一个最重要的额外组件就是如何启动一个与主VM处于相同状态的备 VM。这个机制不仅要支持初始创建备份 VM 的场景,也要能在发生故障后重新启动备份 VM。因此,该机制必须能够适用于一个正在运行且处于任意状态的主 VM(也就是说,不仅仅是在启动时的状态)。此外,我们希望这个机制不会对主 VM 的运行造成明显干扰,因为这会影响当前正在使用该 VM 的客户端。
在 VMware FT 中,我们基于 VMware vSphere 中已有的 VMotion 功能进行了改造。VMotion本身支持在两台服务器之间迁移一个正在运行的 VM,而且中断时间非常短——通常小于一秒。
我们实现了一种经过修改的 VMotion,它不是将 VM 从本地迁移到远程服务器,而是在远程服务器上创建一个完全一致的运行副本,同时保留本地 VM 不变。也就是说,这个 FT VMotion 的作用是克隆(clone)一个 VM 到远程主机,而不是迁移。它还会,建立一条日志传输通道(logging channel);让源 VM 进入日志记录模式,作为“主 VM”;让目标 VM 进入回放模式,作为“备份 VM”。和普通 VMotion 一样,FT VMotion 对主 VM 的运行中断也非常短,通常小于一秒。因此,即使是在一个正在运行的 VM 上启用 FT,也是一项简单、几乎无感知的操作。
启动备份 VM 的另一个方面是:选择在哪台服务器上运行它。 FT VM 运行在一个服务器集群中,这些服务器都能访问共享存储,因此集群中的任何一台服务器都可以运行备份 VM。这种灵活性使得即使某些服务器发生故障,VMware vSphere 也可以快速恢复 FT 冗余性。
VMware vSphere 实现了一个集群服务(clustering service),用于维护管理信息和资源分布情况。当某个主 VM 失去了它的备份,需要重新建立冗余时,它会通知集群服务。集群服务根据资源使用情况和其他约束条件,选择一个最合适的服务器来运行新的备份 VM,并通过 FT VMotion 创建它。
最终的效果是:VMware FT 能够在服务器发生故障后的几分钟内重新建立 VM 的冗余性,而且整个过程中,容错 VM 的执行不会有任何可感知的中断。
3.2 管理日志通道
3.3 操作FT VMs
3.4 磁盘I/O的实现问题
3.5 网络I/O的实现问题
虚拟机网络提供了性能优化技术,有些会依赖于Hypervisor对网络设备状态的异步更新机制,这样会引入非确定性,为了确保主备VMs不产生偏差,需要禁用异步网络优化。客户机在 ring buffer 中写入发送请求或读取接收数据,通常是异步的,比如:数据准备好了通知host,host后台线程异步从ring buffer拿数据发出去,现在客户机要通过触发 trap,让 Hypervisor 来代理网络收发。具体来说,发生trap发生后, Hypervisor首先记录下这次网络队列的更新(即日志),然后才真正取处理这次网络事件,如此一来,网络操作变得同步和可重放
| 原异步设计 | 改为同步设计(禁用异步优化) |
|---|---|
| 客户机直接写 ring buffer | 客户机必须 Trap 进入 Hypervisor |
| Host 异步处理网络收发 | Hypervisor 同步处理,每次都记录日志 |
| 高性能但可能缺少可重放性 | 可审计、可回放,更适合容错、调试 |
FT中的第二个性能优化手段:减少发送数据包的延迟,要想降低延迟,就得加快日志发送 + 等待 ack 的过程。这一部分的核心优化是:确保日志的发送和接收(包括 ack)都不需要线程上下文切换。VMware Sphere Hypervisor 允许开发者将函数注册到 TCP 栈中,这些函数会在某种“延迟执行的上下文”中被调用 —— 类似于 Linux 里的 tasklet,具体意思是,当网络中有 TCP 数据到达(比如日志 ack),Hypervisor 不需要创建或唤醒线程,而是直接使用一个轻量的机制(deferred execution)来执行这些处理函数。我们能快速处理,备份 VM 收到的日志条目以及主 VM 收到的 ack。此外,当主 VM 想要发送一个数据包时,我们会立即刷新(flush)与该包相关的日志,确保日志尽快发给 backup,从而可以早一点收到 ack,让包尽快发出去。