Zookeeper使用ZAB(原子广播协议)来保证分布式数据的一致性,通过ZAB协议,Zookeeper实现了主备模式的系统架构。关于ZAB的两篇论文:A simple totally ordered broadcast protocol、ZooKeeper’s atomic broadcast protocol:
Theory and practice
在Zookeeper的架构中,Leader负责处理写请求,并把写请求封装成一个事务广播到各个Follower节点,只要超过半数节点写入成功,事务就会被提交。
ZAB算法是Paxos算法的变种,之所以Zookeeper中不直接采用Paxos,而是重新开发ZAB算法的原因:
Zookeeper要求事务有序,为了实现有序和主从数据一致性:
为了保证正确性,ZooKeeper额外地需要如下前置属性(Prefix property):
前置属性:如果m是Leader L交付的最后一条消息,那在m之前L提出的消息都必须已交付。
通过以上限制,就能够推断出Zookeeper数据库副本的正确性:
1、可靠性和全序性保证所有副本有一致的状态。
2、因果有序保证了从使用Zab的应用的角度来看状态是正确的。
3、Leader以收到请求的前提下向数据库提出更新。
Zookeeper的节点中存在三种成员角色:
1、Leader:集群中主节点,负责处理用户写请求,同一时刻集群中只有一个主节点
2、Follower:可以处理用户读请求,对于用户写请求,会转发到Leader。同时响应Leader的心跳,如果Leader失联,参与Leader选举
3、Observer:集群中备份节点,不能处理用户读写请求,不能参与选举
这三种成员角色对应4中状态:
1、LEADING:Leader的LEADING状态
2、FOLLOWING:Follower的FOLLOWING状态
3、OBSERVING:Observer的OBSERVING状态
4、LOOKING:当集群中没有Leader时,Follower的状态
整个ZAB协议过程中分为四个阶段:
1、ELECTION:选举阶段,标明集群正在进行Leader选举
2、DISCOVERY:成员发现阶段,Follower之间进行沟通,协商Leader的合法性
3、SYNCHRONIZATION:数据同步阶段,Leader以自己为准,同步自己的数据给各个Follower
4、BROADCAST:原子广播阶段,Leader将自己的事务广播到各个Follower上
集群中各个节点启动开始,分别经历选举阶段、成员阶段、数据同步阶段最终到原子广播状态,只有处于原子广播阶段才能对外提供服务。
节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 Leader。只有到达数据同步阶段之后,准Leader 才会成为真正的Leader。这一阶段的目的是就是为了选出一个准Leader,然后进入下一个阶段。
协议并没有规定详细的选举算法,后面我们会提到实现中使用的 Fast Leader Election。
发现阶段准Leader与各个Follower通信,同步Followers最近接受到的事务提议,这一阶段的目的是发现多数派中最新提议,同时准Leader会生成新的epoch传播给Follower,防止前Leader继续提交提案。
发现阶段具体过程如下:
1、Follower发送FILLOWINFO给准Leader,Leader收到多数派的FILLOWINFO之后,从中提取出最大的epoch并递增作为新的epoch,然后把新的epoch作为MEWEPOCH信息发送给各个Follower
2、Follower对于新的epoch,如果比自己大,则接受新的epoch,发送ACKEPOCH给准Leader,否则退回到选举节点
3、准Leader收到ACKEPOCH之后,从中选取出最新的日志:先比较currentEpoch,如果currentEpoch一致,那么比较ZXID,如果能找到最新的日志,那么准Leader就想这个Follower同步数据
数据同步阶段的目的是把准Leader最新的日志历史同步给所有的Follower,之后准Leader就变成正式Leader,步骤如下:
1、准Leader发送NEWLEADER信息给所有的Follower,其中包含了自己的历史日志
2、Follower接受到NEWLEADER信息之后,判断acceptedEpoch是否与新的epoch相等,如果相等,那么更新自己的currentEpoch为新的epoch,同时按照ZXID逐一比较自己的历史日志(这些事务还没有commit),然后更新日志历史,返回一个ACKNEWLEADER 消息表示已经同步完数据;否则退回到选举阶段
3、Leader接受到ACKNEWLEADER消息之后,向Follower发送commit消息,把这些事务commit掉
4、Follower接受到commit信息之后,对还没有commit的事务按照ZXID顺序进行commit
原子广播阶段对外提供服务,Leader将自己的事务提交给各个Follower是个二阶段提交的过程
1、首先Leader受到用户写请求时,会将写请求封装为一个proposal,每个proposal都有唯一的id:zxid,是一个64位的整数,前32位表示当前Leader的纪元epoch,类似于逻辑时钟,后32位表示该proposal是这个Leader任期内的请求序号,递增。
2、Leader将proposal以事务日志的方式写入磁盘,然后广播给Follower
3、Follower受到请求之后,也会将proposal写入磁盘,成功之后,给Leader回复ACK
4、Leader接收到多数派的ACK之后,就commit proposal,同时广播commit消息给Follower,此时就可以给用户返回写请求成功
5、Follower受到commit消息之后,就将proposal提交
Leader给每个Follower维护一个FIFO队列来保证Follower消息的有序性。
当Leader失联之后,Follower对于Leader的心跳没有响应,集群会进入奔溃恢复状态,此时要保证:
1、在Leader上被commit的消息,一定会被commit,因为commit掉的消息,Leader会给用户返回成功的响应,用户认为此次写请求成功,那么在新选举出来的Leader上也必须commit这条消息,否则用户认为数据不一致。
2、被跳过的消息就一定要被跳过,比如Leader生成一个proposal之后,还没广播这条proposal,就失联,Leader被重新选举之后,如果之前的Leader重启来提交这个proposal,集群要丢弃掉这个proposal。
新增 Follower 流程:
1)新加入的节点会给 Leader 发一个 FOLLOWERINFO 请求
2)Leader 收到 FOLLOWERINFO 请求后会回复 NEWEPOCH 和 NEWLEADER,即告诉 Follower 当前的 epoch 和 history
3)新节点收到 NEWLEADER 后,如果正常逻辑处理后,回一个 ACKNEWLEADER 给 Leader
4)Leader 收到 ACKNEWLEADER 后给该新节点一个 COMMIT 请求,让新节点提交 history
5)Leader 最后把新加入的 Follower 节点放入自己的 Quorum 列表中。
值得注意的是:
1、心跳由Leader向Follower发送,如果多数派的心跳在规定时间内没有收到,Leader就会进入选举阶段,同时如果Follower在规定时候内没有收到Leader的心跳,也会进行选举阶段
2、整个 Propose 过程是并行的,对于 Leader 来说,一个 Proposal 不会等上一个 commit 才会发起新 Proposal 的 propose 请求
3、每个 Peer 进行本地 commit Proposal 的时候是有序的,即 zxid 小的需要先 commit。这也是为了保障全局顺序性
在实际实现过程中,出于性能的考虑,ZAB算法把选举阶段实现为快速Leader选举过程,把发现阶段和数据同步阶段实现为恢复过程:
1、Leader将自己的epoch递增,Followers发送FOLLOWINFO给Leader,Leader根据FOLLOWINFO决定给Follower发送的消息类型:SNAP、DIFF、TRUNC等
2、Follower收到NEWLEADER信息之后,如果epoch比自己小,则退回到选举阶段,否则根据消息类型做相关操作
FLE算法是Recovery Phase的重要保证,它尝试保证leader节点拥有所有历史提交的事务,基于如果一个peer节点有最新的proposal(lastZxid),那么它同样拥有最新的commit这样一个假设。
思想很简单,即尽量采取ZXID最大的节点当做Leader,这样做的好处是,当新的Leader当选以后,只需要Leader向Follower的单向同步就能保证集群数据的一致性,Leader不需要删除事务日志,同时Leader上ZXID最大,那么事务日志就最全。
具体步骤如下:
1、每个节点只能有一张选票,在投票过程中,节点之间不断交换信息,然后更新自己的投票结果,更新的优先级:先比较此次投票信息的epoch,节点epoch大,就投给节点,epoch相同时,投给ZXID大的节点
2、只要一个节点获取到多数派的投票,这个节点就能成为准Leader
那么这个交换信息的过程到什么时间结束呢
1、在选举开始前,每个节点都会选择一个递增的round number作为本轮投票的标识,然后,每个节点都有一个状态:election和leading、following,在选举开始前,每个节点都会投票给自己,然后广播notification消息,包括vote,id,state,round信息。
2、其他节点收到notification消息之后,如果收到的节点是election状态:如果round比自己小,则忽略,如果比自己大,则清空自己的本次选票讯息,增大自己的选票轮次,再比较选票大小。
选票的大小按照ZXID的大小规则来比较,如果收到节点的选票比自己大,则更新自己的选票为收到节点的选票,并再次广播notification消息。
每次接受到notification消息之后,会整理选举结果,如果能够得出Leader,那么更新自己的状态,并再次广播notification消息,此时该节点的选举结束。
如果收到的节点是leading或者following状态,说明当前集群中已经确定出Leader了,如果是相同轮次的选票,则将当前轮次的选票放在一起,看是否能选出一个quorum,确定出leader,如果可以再确定自己的state状态并结束选举。如果是不同轮次的选票,将选票放在外部的选票集合中(因为此时可能处于当前节点崩溃,但集群仍正常可用的状态),并收集所有外部选票,如果能选出一个quorum,确定出Leader,并确定自己的state状态结束选举。