对一个分布式系统的设计中,往往在系统的可用性和一致性之间反复权衡,于是产生了一系列的一致性协议。为了解决分布式一致性问题,涌现出了一大批经典的一致性协议和算法,其中最著名的就是二阶段提交协议、三阶段提交协议、Paxos算法以及ZooKeeper使用的ZAB协议
二阶段提交协议
2PC即二阶段提交,协议说明如下:
阶段一:提交事务请求
1.事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者响应
2.执行事务
各参与节点执行事务操作,并将Undo和Redo信息记入事务日志中
3.各参与者向协调者反馈事务询问的响应
如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行
阶段二:执行事务提交
在阶段二中,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:
执行事务提交
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。
1.发送提交请求
协调者向所有参与者节点发出Commit请求
2.事务提交
参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交后释放在整个事务执行期间占用的事务资源
3.反馈事务提交结果
参与者在完成事务提交之后,向协调者发送Ack消息
4.完成事务
协调者接收到所有参与者反馈的Ack消息后,完成事务
中断事务
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务
1.发送回滚请求
协调者向所有参与者节点发出Rollback请求
2.事务回滚
参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源
3.反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送Ack消息
4.中断事务
协调者接受到所有参与者反馈的Ack消息后,完成事务中断
简而言之,二阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式,因为可以将其看作一个强一致性的算法
优点:原理简单,实现方便
缺点:同步阻塞(所有参与该事务操作都处于阻塞状态,极大影响性能)
单点问题(协调者出问题,则流程无法运转,若在阶段二出现问题,则是参与者一直处于事务资源锁定状态)
数据不一致(阶段二,由于网络等因素某些参与者收到Commit请求,某些未收到,则会导致数据不一致)
太过保守(若某参与者出现故障无法获得相应信息,只能自身超时判断是否需要中断,任何一个节点失败都导致整个事务回退)
三阶段提交协议
2PC即三阶段提交,其将二阶段提交协议的“提交事务请求”过程一分为二,协议说明如下:
阶段一:CanCommit
1.事务询问
协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2.各参与者向协调者反馈事务询问的响应
参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,则反馈Yes响应,并进入预备状态,否则反馈No响应。
阶段二:PreCommit
阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事物的PreCommit操作,包含两种可能。
执行事务预提交
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交
1.发送预提交请求
协调者向所有参与者节点发出preCommit的请求,并进入Prepared阶段
2.事务预提交
协调者向所有参与者节点发出preCommit的请求,并进入Prepared阶段
3.各参与者向协调者反馈事务执行的响应
如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)
中断事务
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交
1.发送中断请求
协调者向所有参与者节点发出abort请求
2.中断事务
无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务
阶段三:doCommit
该阶段将进行真正的事务提交,会存在以下两种可能的情况:
执行提交
1.发送提交请求
进入这一阶段,假设协调者处于正常工作状态,并且它收到了来自所有参与者的Ack响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit请求
2.事务提交
参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源
3.反馈事务提交结果
参与者在完成事务提交之后,向协调者发送Ack消息
4.完成事务
协调者接收到所有参与者反馈的Ack消息后,完成事务
中断事务
进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应
1.发送中断请求
协调者向所有的参与者节点发送abort请求
2.事务回滚
参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个执行期间占用的资源
3.反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送Ack消息
4.中断事务
协调者接收到所有参与者反馈的Ack消息后,中断事务
需求注意的是,一旦进入阶段三,可能会存在以下两种故障
协调者出现问题
协调者和参与者之前的网络故障
无论出现哪种情况,最终都会导致参与者无法及时接受到来自协调者的doCommit或是abort请求,针对这种异常情况,参与者都会在等待超时之后,继续进行事务提交
优点:相较于二阶段提交协议,三阶段提交协议最大的优点就是降低参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。
缺点:在参与者接收到PreCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,这种情况下,参与者依然会进行事务的提交,这必然会出现数据的不一致性
ZAB协议
ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复的原子广播协议。ZooKeeper使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB协议将服务器数据状态变更以事务Proposal的形式广播到所有的副本进程上去。数据的依赖关系对ZAB协议提出了一个要求:ZAB协议必须保证一个全局的变更序列被顺序应用。
集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,非Leader服务器首先这个事务请求转发给Leader服务器。
当Leader服务器出现崩溃退出或机器重启,亦或是集群中已经不存在过半服务器与该Leader服务器保持正常通信时,那么在重新开始新一轮的原子广播事务操作之前,所有进程首先会使用崩溃恢复协议来使彼此达到一个一致的状态,于是整个ZAB流程就会从消息广播模式进入崩溃恢复模式。一个机器要成为新的Leade,必须获得过半进程的支持,同时由于每个进程都有可能会崩溃,因此,在ZAB协议运行过程中,前后出现多个Leader,并且每个进程也有可能会多次成为Leader。进入崩溃恢复模式后,只要集群中存在过半的事服务器能够彼此进行正常通信,那么就可以产生一个新的Leader并再次进入消息广播模式。
消息广播
ZAB协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程。针对客户端的事务请求,Leader服务器会为期生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务的提交。ZAB协议的二阶段提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么就抛弃Leader服务器。简化的二阶段提交协议模型下,无法处理Leader服务器崩溃退出带来的数据不一致的问题,因此在ZAB协议中添加了另一个模式,即采用崩溃恢复模式来解决这个问题。另外,整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,一次你能够很容易地保证消息广播过程中的消息接收与发送的顺序性。整个消息广播过程中,Leader服务器会为每个事务请求生成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会首先为这个事务Proposal分配一个全局单调递增的唯一ID,我们称之为事务ID(即ZXID)。由于ZAB协议需要保证每一个消息严格的因果关系,因此必须将每一个事务Proposal按照其ZXID的先后顺序来进行排序和处理。
具体的,在消息广播过程中,Leader服务器必须会为每一个Follower服务器各自分配一个单独的队列,然后将需要广播的事务Proposal一次放入这些队列中,并且根据FIFO策略进行消息发送。每一个Follower服务器在接受到这个事务Proposal之后,都会首先将其以事务日志的形式写入本地磁盘中,并且在成功写入后反馈给Leader服务器一个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会广播一个Commit消息给所有的Follower服务器以通知其进行事务提交。
崩溃恢复
ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下运行非常良好,但是一旦Leader服务器出现崩溃,或者由于网络原因导致Leader服务器失去了与公办Follower的联系,那么就会进入崩溃恢复模式。
基本特性
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交。
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。
针对这两个,选举出来的Leader服务器拥有集群中机器最高编号(即ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有所有已经提交的提案。更为重要的,如果让具有最高编号事务Proposal的机器来成为Leader,就可以省去Leader服务器检查Proposal的提交和丢弃工作的这一步操作了。
数据同步
完成Leader选举之后,Leader服务器会首先确认事务日志中的所有Proposal是否都已经被集群中过半的机器提交了。
所有正常运行的服务器,要么成为Leader,要么成为Follower并和Leader保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每一条事务Proposal,并且能够正确的将所有已经提交了的事务Proposal应用到内存数据库中。具体的,Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被个Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每一个Proposal消息后面紧接着再发一个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务Proposal都从Leader服务器上同步过来并成功应用到本地数据库中,Leader服务器就会将该Follower服务器假如到真正的可用Follower列表中,并开始之后的其他流程。
在ZAB协议的事务编号ZXID设计中,ZXID是一个64位的数字,其中低32位可以看作是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader服务器在产生一个新的事务Proposal的时候,都会对该计数器进行加1操作;而高32位则代表了Leader周期epoch的编号,每当选举产生一个新的Leader服务器,就会从这个Leader服务器上去除其本地日志中最大事务Proposal的ZXID,并从改ZXID中解析出对应的epoch值,然后再对其进行加1操作,之后就会以此编号作为新的epoch,并将低32位置0来开始生成新的ZXID。ZAB协议中的这一通过epoch编号来区分Leader周期变化策略,能够有效避免不同的Leader服务器错误地使用相同的ZXID编号提出不一样的事务Proposal的异常情况,这对于识别在Leader崩溃恢复前后生成的Proposal非常有帮助,大大简化和提升了数据恢复流程。
基于这样的策略·,当一个包含了上一个Leader周期中尚未提交过的事务Proposal的服务器启动时,其肯定无法成为Leader,原因很简单,因为当前集群中一定包含一个Quorum集合,该集合中的机器一定包含了更高的epoch的事务Proposal,因此这台机器的事务Proposal肯定不是最高,也就无法成为Leader了。当这台机器加入到集群中,以Follower角色连接上Leader服务器后,Leader服务器会根据自己服务器上最后被提交的Proposal和Follower服务器的Proposal进行对比,对比的结果当然是Leader要求Follower进行一个回退操作——回退到一个确实已经被集群中过半机器提交的最新的事务Proposal。
参考:
《从Paxos到ZooKeeper分布式一致性原理与实践》