引言
写这篇文章的目的是整理一下看了这篇论文和课程视频以后的一些问题理解与思考,因为是个人的想法,难免出现疏漏,错误之处还请读者斧正。
Replication
首先这节课的主题是复制,所以挑选了《The Design of a Practical System for Fault-Tolerant Virtual Machines》这篇文章从一个容错虚拟机的角度来阐述复制与容错。但其实在我看来其实容错虚拟机确实从“状态”的角度来看是比较特殊的,因为它需要把一个VMM(Virtual-Machine Monitors)中几乎所有的状态(CPU,指令执行,中断等)拷贝至另一个VMM中,而我们平时常见的主从复制模型则是把一些数据进行拷贝。当然从本质上来看都是一样的,只是状态定义不同,这也侧面启发我们要更加注重于事物的本质。
首先想说说容错中的错到底如何定义,即replication到底希望解决怎样的问题,问题的答案就是**对于计算机宕机和网络出现分区(fail-stop)的问题我们可以使用replication来解决,但是对于软件的bug和硬件的设计错误则无法避免。**因为replication在指令相同的情况下无法保证在两台机器上得到相同的答案,这当然也失去了replication的意义。
还有就是一般来说很难处理硬件设计的问题,但有时可以,比如磁盘在放置了一段时间以后可能几bit会出现问题,有时使用纠错码可以修改,有时无法修改但可以找出,此时就可以选择一些解决问题的方法,比如爆出fail-stop。
对于副本之间的可能会发生的故障我们希望彼此之间是独立不相干的,如果出现的问题是一样的,replication也无法帮我们解决问题,比如同时购买的一批服务器都存在同样的设计问题,或者是遇上了地震,那么地震影响的那个城市的数据中心就全毁了,所以类似的错误就成了一种相干的故障了。这也就失去了replication存在的意义
还有就是因为多余的副本意味着两倍或者三倍的计算机资源的花费,那么花费是否值得呢?这就不是一个技术上的的问题,而是一个经济上的问题,它取决于可用服务的价值,取决于你的钱包,取决于故障带来的后果是否可以接收。
课程中提到了两种replication的策略,分别为state transfer
和Replication state Machine
,前者是把整个状态集合全部从primary拷贝至backup,比较简单。后者则是建立在主从之间初始状态一致,只需要少量的交互就可以做到同步,这里的定义为:如果两个确定性状态机以相同的初始状态启动,并以相同的顺序提供完全相同的输入,那么它们将经历相同的状态序列并产生相同的输出。看似简单,但是其中却涉及到很多细节。这两种策略让人联想起redis的主从复制,先拷贝一个RDB文件,使得主从之间初始状态一致,然后就是每次传递一部分指令了,从步骤上来看对应了两种复制策略。
我们上面提到了如果我们想去构建一套Replication state Machine方案,有很多细节需要考虑,首先我们需要回答一些问题:
- 我们需要在哪个层面进行状态复制,即所谓的状态是什么?
- 我们需要考虑主备的同步程度是要有多紧密?因为主在接收到数据以后一定比backup提前一些执行操作,backup必定总会有一定的延迟。所以如果主发生了故障,backup就不一定能完全赶上primary的进度了,如果想让主完全同步与从,这需要很大的代价(强一致性),因为需要大量的通信
- 如何做到在主服务器宕机的时候使得Client和backup进行通信(一个合理的一致性协议或者向redis一样引入哨兵集群(其中也包括一致性协议),分布式锁在一定程度也可以解决此类问题)
- 在没有异常可见的情况下,设计一种切换的系统几乎是不可能的事情。在backup切换到主机的过程中会出现异常,我们必须想一种方法来应对,(异常导致切换?)
- 在创建一个新的replication的时候代价很高,因为在初始阶段我们总要进行一次state transfer。
关于paper问题
首先需要指出这个系统的容错设计希望达到的目标就是在主服务器发生故障时,总是可以使用备份服务器进行接管。 备份服务器的状态必须始终保持与主服务器几乎相同,以便在主服务器发生故障时,备份服务器可以立即接管,且在客户机看来不丢失任何数据。
接下来列出一些paper中值得注意的几点和一些学习过程中的问题与解答:
需要注意的几点
其实可以把本篇文章描述的虚拟机容错步骤看做是与Redis把RDB文件拷贝到从服务器以后的步骤,其实本质都一样,就是在初始状态相同的情况下使得主备服务器状态一致。只不过在虚拟机容错中将指令拷贝到从服务器以后会在VMM丢弃掉本来应该发送的数据包。
其次因为在虚拟机容错中严格的要求全部的执行过程都相同,精确到每一个指令,每一个中断,那么当数据包从网络中到达机器的时候。应该直接把数据包拷贝到内存吗?一般来说一个数据包中除了包含要发送给我们的数据以外,还包含了一个用来通知数据包已经到达的中断信号,那么如果我们直接把数据包拷贝到内存中会在指令流中插入一个中断,但是我前面说过我们需要主备之间需要完全一致,而这样会引入随机性,因为我们没办法控制在哪一个确切的时间将数据拷贝在内存中,这样就可能导致执行可能会出现不同的结果。paper中的做法就是引入bounce buffer
,将数据从网卡中发送到VMM,然后把再一个中断发向VMM,表示有一个数据到到了,并将主服务器挂起并记录在哪一条指令处挂起,然后把指令号和数据发送给back up,使得从服务器也在同一指令号上挂起,从而保证多台机器之间状态几乎完全一致。
我们继续说说output Requirement
。想象一种特殊的情况,假设我们有两台机器构成一个容错的系统,分别为primary和backup,当primary把数据发送给backup后不接收ACK就对client执行回复的话,就可能出现backup实际并未收到log entry,但是primary已经宕机,此时backup晋升,就可能造成数据的不一致。其实这就是在CAP中选择CP还是AP的取舍,因为虚拟机容错要求绝对的一致,所以当然是弃AP而保CP。所以引入了output Requirement
,具体的做法就是在当primary未收到backup的ack时primary不向外界输出值,当backup收到数据的时候才向外界输出,注意这里其实管理程序为主vm和备份vm的日志记录条目维护了一个大型缓冲区,这样的话backup在收到日志条目以后就可以返回ack,然后慢慢执行,以提升效率,primary在执行时也将日志条目放到日志缓冲区中,从缓冲区写入Logging channel
。所以只要primary接收到ACK,就代表backup已经接收到值,此时就算primary宕机也不会丢失数据了。
有意思的是primary并不是阻塞等待ack,VM本身是可以继续执行,这样当然可能会导致一个问题就是主从之间状态差距太大,paper中采用管理日志通道
的来避免。如果备份VM在需要读取下一个日志条目时遇到空日志缓冲区,它将停止执行,直到有新的日志条目可用。 由于备份VM没有与外部通信,因此此暂停不会影响VM的任何客户端。 类似地,如果主VM在需要写入日志条目时遇到一个完整的日志缓冲区,它必须停止执行,直到日志条目被清除为止,可以看出执行中的这个停止是一种自然的流控制机制。显然我们不希望primary与backup之间状态差距太大,即primary中缓冲区已满,这会极大的增加Client的延迟。paper中的做法是在一定情况下降低primary的速度,当值backup落后太多,具体为:
在发送和确认日志条目的协议中,我们发送附加信息来确定主vm和备份vm之间的实时执行延迟。 通常,执行延迟小于100毫秒。 如果备份VM开始出现明显的执行延迟(比如超过1秒),VMware FT就会通知调度器给它分配更少的CPU资源(最初只分配了几个百分点),从而降低主VM的速度。 我们使用一个缓慢的反馈循环,它将尝试逐步确定主VM的适当CPU限制,以便让备份VM能够匹配其执行。 如果备份VM继续滞后,我们将继续逐步降低主VM的CPU限制。 相反,如果备份VM赶上来了,我们会逐渐增加主VM的CPU限制,直到备份VM恢复到有一点延迟。
Q&A
Q:客户端如何在宕机恢复后确定哪一个服务器当前为主?
答:leader宕机以后集群会选择一个从服务器作为leader,这其实在一般情况是一个典型的分布式共识问题,我们需要运行一个一致性协议去选择一个leader,例如Raft,ZAB,Bully等,然后leader去更新dns。这里要重点说明,其实不管是Redis的哨兵还是使用分布式锁去选择leader,其本质都是共识算法,就是让全局都对某一事件达成一致。但为什么我说一般情况呢?paper中选择了一主一从的架构,问题的关键就不是共识了,所以简单的广播就可以了。
Q:有哪些可能发生的不确定的事件?
答:这里所说的不确定事件是同样的输入可能导致不同的输出,一种是随机数相关的,一种是并行写的指令交错问题。前者不必多说,后者是为什么呢?举个例子,假设一段代码是对锁的抢夺,不同的顺序会导致不同线程先拿到锁,这样就会导致不同的运行结果。解决方案通常是检测任何这样的IO争用(这种情况很少见),并强制在主服务器和备份服务器上以相同的方式顺序执行这样的争用磁盘操作。
Q: 猜测 log entry 中有哪些字段
答:猜测至少三个字段{事件发生的指令号
(自机器启动起该命令是第几条);事件类型
(正常的还是由随机事件转换);数据
}
Q: 因为backup的日志缓冲区中存放着从primary中发来的log entry,有可能有这样一种极端的情况,即backup已向primary发送ack,primary收到以后向Client发送回复,此时primary宕机,backup切为primary,开始执行还未执行的log,此时如何确保Client收到数据不重复?
答:很巧妙,因为primary和backup状态几乎完全一致,包括发送数据包的seq!所以TCP协议栈(选择的话)中会帮我们处理重复的数据包。
Q:primary与Client连接,而backup与primary维护一致的状态,在primary宕机以后Client也关闭了套接字,而backup还维护着连接,这样会导致哪些问题吗?
答:不会,因为primary已经宕机,不会导致primary和backup出现分歧,所有primary已经执行过的backup也执行过了,哪怕backup还维护着连接。
Q:关于性能
答:因为replication是为了提高容错,一般来说放在不同机架甚至是不同的城市,而为了保证CP,大量的开销是不可避免的。
Q:是否可以在 output requirement 中让 backup 输出?
答:这是在课堂中一个学生提出的问题,我对这个问题的想法是这样的,因为我们需要保证primary和backup之间指令的强一致性,如果backup执行输出的话,势必需要再发送一条消息给primary表明更新完成,如果不发的话,可能消息在网络中丢失了,backup根本没拿到数据,primary却以为拿到了。所以实际开销是和paper中是一样的,但是这样的做法可能在primary和backup与Client的链路不在一起的时候做到输入流于输出流的分离,做到充分利用带宽。
Q: 关于paper中提到的共享磁盘的问题。
答:paper中为了防止网络分区导致的脑裂问题,引入了一个共享磁盘,但显然paper中所谓的共享磁盘有一个单点故障的问题,实现容错,却依靠于一个单点故障的组件,显然是矛盾的。我认为这其实就相当于实现一个分布式锁,其实现也是一个集群,不过也合理,人家也不能什么都写,毕竟要挣钱的。
这个问题是课程资源后留下的问题:
Q:How does VM FT handle network partitions? That is, is it possible that if the primary and the backup end up in different network partitions that the backup will become a primary too and the system will run with two primaries?
答:这个问题其实就是问我们VM FT如何做到应对网络分区,也就是上一条的答案。
总结
这篇paper其实向我们描述了一个实际应用的主从复制模型的具体细节如何实现。可以看出无外乎就是根据业务决定的C与A的抉择,再就是在前者的基础上实现不同等级的一致性。文中描述的就是一个强一致性的模型,并围绕如何一些可能造成不一致的场合进行讨论。
读这篇文章的收获简单来说就是简要的了解了虚拟化技术,并把以前学到了关于replication的知识点联系到了一个具体的应用场合。因为其中很多设计都是运行于VMM的,所以其实是非常特化的,至少目前看起来很难想到其他可用的地方。