ZooKeeper学习第七期--ZooKeeper一致性原理
概念
ZooKeeper为高可用的一致性协调框架,自然的ZooKeeper也有着一致性算法的实现,ZooKeeper使用的是ZAB协议作为数据一致性的算法, ZAB(ZooKeeper Atomic Broadcast ) 全称为:原子消息广播协议;
ZAB可以说是在Paxos算法基础上进行了扩展改造而来的,ZAB协议设计了支持崩溃恢复,ZooKeeper使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器数状态以事务形式广播到所有Follower上;
由于事务间可能存在着依赖关系,ZAB协议保证Leader广播的变更序列被顺序的处理,:一个状态被处理那么它所依赖的状态也已经提前被处理;
ZAB协议支持的崩溃恢复可以保证在Leader进程崩溃的时候可以重新选出Leader并且保证数据的完整性;
在ZooKeeper中所有的事务请求都由一个主服务器也就是Leader来处理,其他服务器为Follower,Leader将客户端的事务请求转换为事务Proposal,并且将Proposal分发给集群中其他所有的Follower,然后Leader等待Follwer反馈,当有 过半数(>=N/2+1) 的Follower反馈信息后,Leader将再次向集群内Follower广播Commit信息,Commit为将之前的Proposal提交;
ZooKeeper从以下几点保证了数据的一致性
我们知道ZooKeeper也是一种分布式系统,它在一致性上有人认为它提供的是一种强一致性的服务(通过sync操作),也有人认为是单调一致性(更新时的大多说概念),还有人为是最终一致性(顺序一致性),反正各有各的道理这里就不在争辩了。然后它在分区容错性和可用性上做了一定折中,这和CAP理论是吻合的。
ZAB协议中多次用到“过半”设计策略 ,该策略是zk在A(可用性)与C(一致性)间做的取舍,也是zk具有高容错特性的本质。相较分布式事务中的2PC(二阶段提交协议)的“全量通过”,ZAB协议可用性更高(牺牲了部分一致性),能在集群半数以下服务宕机时正常对外提供服务。
① 顺序一致性
来自任意特定客户端的更新都会按其发送顺序被提交。也就是说,如果一个客户端将Znode z的值更新为a,在之后的操作中,它又将z的值更新为b,则没有客户端能够在看到z的值是b之后再看到值a(如果没有其他对z的更新)。
② 原子性
每个更新要么成功,要么失败。这意味着如果一个更新失败,则不会有客户端会看到这个更新的结果。
③ 单一系统映像
一个客户端无论连接到哪一台服务器,它看到的都是同样的系统视图。这意味着,如果一个客户端在同一个会话中连接到一台新的服务器,它所看到的系统状态不会比在之前服务器上所看到的更老。当一台服务器出现故障,导致它的一个客户端需要尝试连接集合体中其他的服务器时,所有滞后于故障服务器的服务器都不会接受该连接请求,除非这些服务器赶上故障服务器。
④ 持久性
一个更新一旦成功,其结果就会持久存在并且不会被撤销。这表明更新不会受到服务器故障的影响。
==========================================================================
ZAB协议的两个基本模式:恢复模式和广播模式
恢复模式:(选举)
概述
当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和server具有相同的系统状态。
具体选举看下面文章
http://www.jasongj.com/zookeeper/fastleaderelection/
崩溃恢复过程中,为了保证数据一致性需要处理特殊情况:
1、已经被leader提交的proposal确保最终被所有的服务器follower提交
2、确保那些只在leader被提出的proposal被丢弃
针对这个要求,如果让leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高的ZXID事务proposal,就可以保证这个新选举出来的Leader一定具有所有已经提交的提案,也可以省去Leader服务器检查proposal的提交与丢弃的工作。
恢复阶段的保证
① 我们绝不能遗忘已经被deliver的消息,若一条消息在一台机器上被deliver,那么该消息必须将在每台机器上deliver。
② 我们必须丢弃已经被skip的消息。
保证示例
第一条:
若一条消息在一台机器上被deliver,那么该消息必须将在每台机器上deliver,即使那台机器故障了。
(待此机器恢复,也能看到此消息的deliver,所以为了一致性,必须将此消息deliver到所有机器)
例如,出现了这样一种情况:Leader发送了commit消息,但在该commit消息到达其他任何机器之前,Leader发生了故障。也就是说,只有Leader自己收到了commit消息。如图3.4中的C2。
图3.4是"第一条保证"(deliver消息不能忘记)的一个示例。在该图中Server1是一个Leader,我们用L1表示,Server2和Server3为Follower。
首先Leader发起了两个Proposal,P1和P2,并将P1、P2发送给了Server1和Server2。然后Leader对P1发起了Commit即C1,之后又发起了一个
Proposal即P3,再后来又对P2发起了commit即C2,就在此时我们的Leader挂了。那么这时候,P3和C2这两个消息只有Leader自己收到了。
因为Leader已经deliver了该C2消息,client能够在消息中看到该事务的结果。所以该事务必须能够在其他所有的Server中deliver,最终使得client看到了一个一致性的服务视图。
第二条:
一个被skip的消息,必须仍然需要被skip。
例如,发生了这样一种情况:Leader发送了propose消息,但在该propose消息到达其他任何机器之前,Leader发生了故障。也就是说,只有Leader自己收到了propose消息。如图3.4中的P3所示。
在图3.4中没有任何一个server能够看到3号提议,所以在图3.5中当server 1恢复时他需要在系统恢复时丢弃三号提议P3。
在图3.5是"第二条保证"(skip消息必须被丢弃)的一个示例。Server1挂掉以后,Server3被选举为Leader,我们用L2表示。L2中还有未被deliver的消息P1、P2,
所以,L2在发出新提议P10000001、P10000002之前,L2先将P1、P2两个消息deliver。因此,L2先发出了两个commit消息C1、C2,之后L2才发出了新的提议P10000001和P10000002。
如果Server1 恢复之后再次成为了Leader,此时再次将P3在P10000001和P10000002之后deliver,那么将违背顺序性的保障。
保证的实现
如果Leader选举协议保证了新Leader在Quorum Server中具有最高的提议编号,即Zxid最高。那么新选举出来的leader将具有所有已deliver的消息。新选举出来的Leader,在提出一个新消息之前,首先要保证事务日志中的所有消息都由Quorum Follower已Propose并deliver。需要注意的是,我们可以让新Leader成为一个用最高zxid来处理事务的server,来作为一个优化。这样,作为新被选举出来的Leader,就不必去从一组Followers中找出包含最高zxid的Followers和获取丢失的事务。
① 第一条
所有的正确启动的Servers,将会成为Leader或者跟随一个Leader。Leader能够确保它的Followers看到所有的提议,并deliver所有已经deliver的消息。通过将新连接上的Follower所没有见过的所有PROPOSAL进行排队,并之后对该Proposals的COMMIT消息进行排队,直到最后一个COMMIT消息。在所有这样的消息已经排好队之后,Leader将会把Follower加入到广播列表,以便今后的提议和确认。这一条是为了保证一致性,因为如果一条消息P已经在旧Leader-Server1中deliver了,即使它刚刚将消息P deliver之后就挂了,但是当旧Leader-Server1重启恢复之后,我们的Client就可以从该Server中看到该消息P deliver的事务,所以为了保证每一个client都能看到一个一致性的视图,我们需要将该消息在每个Server上deliver。
② 第二条
skip已经Propose,但不能deliver的消息,处理起来也比较简单。在我们的实现中,Zxid是由64位数字组成的,低32位用作简单计数器。高32位是一个epoch。每当新Leader接管它时,将获取日志中Zxid最大的epoch,新Leader Zxid的epoch位设置为epoch+1,counter位设置0。用epoch来标记领导关系的改变,并要求Quorum Servers 通过epoch来识别该leader,避免了多个Leader用同一个Zxid发布不同的提议。
这 个方案的一个优点就是,我们可以skip一个失败的领导者的实例,从而加速并简化了恢复过程。如果一台宕机的Server重启,并带有未发布的 Proposal,那么先前的未发布的所有提议将永不会被deliver。并且它不能够成为一个新leader,因为任何一种可能的 Quorum Servers ,都会有一个Server其Proposal 来自与一个新epoch因此它具有一个较高的zxid。当Server以Follower的身份连接,领导者检查自身最后提交的提议,该提议的epoch 为Follower的最新提议的epoch(也就是图3.5中新Leader-Server2中deliver的C2提议),并告诉Follower截断 事务日志直到该epoch在新Leader中deliver的最后的Proposal即C2。在图3.5中,当旧Leader-Server1连接到了新leader-Server2,leader将告诉他从事务日志中清除3号提议P3,具体点就是清除P2之后的所有提议,因为P2之后的所有提议只有旧Leader-Server1知道,其他Server不知道。
Paxos与Zab ****
① Paxos一致性
Paxos的一致性不能达到ZooKeeper的要求,我们可以下面一个例子。我们假设ZK集群由三台机器组成,Server1、Server2、Server3。Server1为Leader,他生成了 三条Proposal,P1、P2、P3。但是在发送完P1之后,Server1就挂了。如下图3.6所示。
Server1挂掉之后,Server3被选举成为Leader,因为在Server3里只有一条Proposal—P1。所以,Server3在P1的基础之上又发出了一条新Proposal—P2',P2'的Zxid为02。如下图3.7所示。
Server2发送完P2'之后,它也挂了。此时Server1已经重启恢复,并再次成为了Leader。那么,Server1将发送还没有被deliver的Proposal—P2和P3。由于Follower-Server2中P2'的Zxid为02和Leader-Server1中P2的Zxid相等,所以P2会被拒绝。而P3,将会被Server2接受。如图3.8所示。
我们分析一下Follower-Server2中的Proposal,由于P2'将P2的内容覆盖了。所以导致,Server2中的Proposal-P3无法生效,因为他的父节点并不存在。
② Zab一致性
首先来分析一下,上面的示例中为什么不满足ZooKeeper需求。ZooKeeper是一个树形结构,很多操作都要先检查才能确定能不能执行,比如,在图3.8中Server2有三条Proposal。P1的事务是创建节点"/zk",P2'是创建节点"/c",而P3是创建节点"/a/b",由于"/a"还没建,创建"a/b"就搞不定了。那么,我们就能从此看出Paxos的一致性达不到ZooKeeper一致性的要求。
为了达到ZooKeeper所需要的一致性,ZooKeeper采用了Zab协议。Zab做了如下几条保证,来达到ZooKeeper要求的一致性。
(a) Zab要保证同一个leader的发起的事务要按顺序被apply,同时还要保证只有先前的leader的所有事务都被apply之后,新选的leader才能在发起事务。
(b) 一些已经Skip的消息,需要仍然被Skip。
我想对于第一条保证大家都能理解,它主要是为了保证每 个Server的数据视图的一致性。我重点解释一下第二条,它是如何实现。为了能够实现,Skip已经被skip的消息。我们在Zxid中引入了 epoch,如下图所示。每当Leader发生变换时,epoch位就加1,counter位置0。
我们继续使用上面的例子,看一下他是如何实现Zab的 第二条保证的。我们假设ZK集群由三台机器组成,Server1、Server2、Server3。Server1为Leader,他生成了三条 Proposal,P1、P2、P3。但是在发送完P1之后,Server1就挂了。如下图3.10所示。
Server1挂掉之后,Server3被选举成为 Leader,因为在Server3里只有一条Proposal—P1。所以,Server3在P1的基础之上又发出了一条新Proposal—P2', 由于Leader发生了变换,epoch要加1,所以epoch由原来的0变成了1,而counter要置0。那么,P2'的Zxid为10。如下图3.11所示。
Server2发送完P2'之后,它也挂了。此时Server1已经重启恢复,并再次成为了Leader。那么,Server1将发送还没有被deliver的Proposal—P2和P3。由于Server2中P2'的Zxid为10,而Leader-Server1中P2和P3的Zxid分别为02和03,P2'的epoch位高于P2和P3。所以此时Leader-Server1的P2和P3都会被拒绝,那么我们Zab的第二条保证也就实现了。如图3.12所示。
广播模式:(数据同步)
广播模式类似一个简单的两阶段提交:Leader发起一个请求,收集选票,并且最终提交,图3.3演示了我们协议的消息流程。我们可以简化该两阶段提交协议,因为我们并没有"aborts"的情况。followers要么确认Leader的Propose,要么丢弃该Leader的Propose。没有"aborts"意味着,只要有指定数量的机器确认了该Propose,而不是等待所有机器的回应。
广播协议在所有的通讯过程中使用TCP的FIFO信道,通过使用该信道,使保持有序性变得非常的容易。通过FIFO信道,消息被有序的deliver。只要收到的消息一被处理,其顺序就会被保存下来。
一旦Leader已经和多数的Follower进行了状态同步后,他就可以开始广播消息了,即进入广播状态。
这时候当一个Server加入ZooKeeper服务中,它会在恢复模式下启动,发现Leader,并和Leader进行状态同步。待到同步结束,它也参与消息广播。
ZooKeeper服务一直维持在广播状态,直到Leader崩溃了或者Leader失去了大部分的Followers支持。
广播模式极其类似于分布式事务中的2pc(two-phrase commit 两阶段提交):即Leader提起一个决议,由Followers进行投票,Leader对投票结果进行计算决定是否通过该决议,如果通过执行该决议(事务),否则什么也不做。
广播协议在所有的通讯过程中使用TCP的FIFO信道,通过使用该信道,使保持有序性变得非常的容易。通过FIFO信道,消息被有序的deliver。只要收到的消息一被处理,其顺序就会被保存下来。
Leader会广播已经被deliver的Proposal消息。在发出一个Proposal消息前,Leader会分配给Proposal一个单调递增的唯一id,称之为zxid。
广播是把Proposal封装到消息当中,并添加到指向Follower的输出队列中,通过FIFO信道发送到Follower。
当Follower收到一个Proposal时,会将其写入到磁盘,可以的话进行批量写入。一旦被写入到磁盘媒介当中,Follower就会发送一个ACK给Leader。
当Leader收到了指定数量的ACK时,Leader将广播commit消息并在本地递交该消息。当收到Leader发来commit消息时,Follower也会递交该消息。
需要注意的是, 该简化的两阶段提交自身并不能解决Leader故障,所以我们 添加恢复模式来解决Leader故障。
ZAB协议简化了2PC事务提交:
1、去除中断逻辑移除,follower要么ack,要么抛弃Leader;
2、leader不需要所有的Follower都响应成功,只要一个多数派ACK即可。
丢弃的事务proposal处理过程:
ZAB协议中使用ZXID作为事务编号,ZXID为64位数字,低32位为一个递增的计数器,每一个客户端的一个事务请求时Leader产生新的事务后该计数器都会加1,
高32位为Leader周期epoch编号,当新选举出一个Leader节点时Leader会取出本地日志中最大事务Proposal的ZXID解析出对应的epoch把该值加1作为新的epoch,将低32位从0开始生成新的ZXID;
ZAB使用epoch来区分不同的Leader周期,能有效避免了不同的leader服务器错误的使用相同的ZXID编号提出不同的事务proposal的异常情况,大大简化了提升了数据恢复流程;
所以这个崩溃的机器启动时,也无法成为新一轮的Leader,因为当前集群中的机器一定包含了更高的epoch的事务proposal。