ZAB协议简介
Zookeeper主要依赖ZAB协议来实现分布式数据一致性,基于该协议,Zookeeper实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性(满足上文中提到的五个一致性要求)。
ZAB的核心事务请求的处理方式是:
所有事务写请求都必须由一个全局唯一的Leader服务器来处理。若是写请求且当前节点不是leader,该节点会将写请求转发给leader。
对于任一节点,若客户端提交读请求,当前节点会根据自己保存的数据进行响应。
当leader服务器状态发生变化后,leader采用事务提案(proposal)的方式广播该写操作到所有的Follower服务器。ZAB为保证全局的序列变更,为每一个事务分配一个全局的递增编号xid。
Leader服务器需要等待所有Follower服务器的反馈。一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader服务器将向所有的Follower服务器发送commit消息,要求将前一个事务提议刷新到磁盘。
当Leader由于网络、崩溃退出或重启等异常情况与过半的Follower失去联系时,就会进入崩溃恢复模式。整个集群无法处理写请求。需要通过Leader选举算法选举出新的Leader。
节点的三种状态
在ZAB协议中,每一个zk节点都有可能处于以下三个状态:
LOOKING:Leader选举阶段
FOLLOWING:Follower服务器和Leader保持同步状态
LEADING:Leader服务器作为Leader领导Follower状态
且每一个节点,都会循环执行: 选举(Leader Election)、发现(Discovery)、同步(Synchronization)和广播(Broadcast)四个状态。
Leader和Follower之间保持一个tcp长连接 ,如果在指定的超时时间内Leader无法从过半Follower获取到心跳包,那么Leader就结束对当前”epoch”的领导 其他的Follower会转换到LOOKING状态开始新的一轮选举
ZAB协议的一些设计
ZXID
ZXID是一个64位的数字,由xid和epoch组成。
xid: 低32位是一个简单的单调递增的计数器,Leader服务器产生一个新的事务提议的时候,都会对该计数器+1。
epoch: 高32位,用来区分不同的Leader服务器。具体做法是,每选举产生一个新的Leader服务器,就会从Leader服务器的本地日志中取出一个最大的ZXID,生成对应的epoch值,然后再进行加1操作,之后就会以该值作为新的epoch。并将低32位从0开始生成ZXID。
ZAB协议中通过epoch编号来区分Leader周期变化,能够有效避免不同Leader服务器使用相同的ZXID。
崩溃恢复
当Leader由于网络、崩溃退出或重启等异常情况与过半的Follower失去联系时,就会进入崩溃恢复模式。通过Leader选举算法选举出新的Leader。
由于一致性需求:
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。
如果让Leader选举算法保证新选举出来的Leader服务器拥有集群中所有机器最高编号(即ZXID最大)的事务Proposal,那么就可以保证这个新选举出来的Leader一定具有所有已经提交的提案。
由于选举 leader 的时候,整个集群无法处理写请求的,需要快速进行 leader 选举。最快的方法是直接让具有最高编号事务Proposal的机器成为leader,这种选举算法称为Fast Leader Election。
ZAB协议的四个阶段
ZAB协议主要包括消息广播和消息恢复两个过程,进一步可以细分为四个阶段:选举(Leader Election)、发现(Discovery)、同步(Synchronization)和广播(Broadcast)
Phase 0: 选举
该阶段的目是为了选出一个准Leader
投票箱:
每个服务器存储了一个投票箱(recvset),记录了它所收到的选票。
例如:Server_2 投票给 Server_3,Server_3 投票给 Server_1,则Server_1的投票箱为(2,3)、(3,1)、(1,1)。(每个服务器都会默认给自己投票)
前一个数字表示投票者,后一个数字表示被选举者。票箱中只会记录每一个投票者的最后一次投票记录,如果投票者更新自己的选票,则其他服务器收到该新选票后会在自己的票箱中更新该服务器的选票。
Zookeeper 规定所有有效的投票都必须在同一个轮次中,每个服务器在开始新一轮投票时,都会对自己维护的 logicalClock 进行自增操作,并将自己的投票箱清空
- 基于Fast Leader Election的准leader的选举流程:
候选节点A初始化自身的zxid和epoch,logicalclock + 1。节点在选举开始时,都先投票给自己
向其他所有节点发送选主通知
等待其他节点的选票。当收到其它节点的选票,则根据消息中对方的状态进行相应的处理:
1)LOOKING状态:
1>
a. 如果对方也处于本轮投票,根据以下的条件判断对方的投票是否优于当前的投票,是的话更新当前的投票并发送 。
选epoch最大的节点
若epoch相等,选具有最大ZXID的节点
若ZXID相等,则选择server_id最大的节点
b. 如果发送过来的逻辑时钟(logicalclock)大于目前的logicalclock,那么说明这是更新的一次选举投票,此时更新本机的logicalclock,清空投票箱(因为已经过期没有用了),判断对方的投票是否优于自己(根据上述规则),是的话用对方推荐的leader更新下一次的投票,否则使用初始的投票(投自己),通知所有服务器自己的选择
c. 如果对方处于上轮投票,不予理睬,回到3。
2> 将收到的投票放入自己的投票箱中。
3> 判断所推荐的leader是否得到集群多数人的同意,如果得到多数人同意,那么还需等待一段时间,看是否有比当前更优的提议,如果没有,则认为投票结束。根据投票结果修改自己的状态。以上任何一条不满足,则继续循环。
2)FOLLOWING或LEADING状态:
以下原因可能导致出现这种情况:
- 节点A(Follower)与Leader出现网络问题而触发一次选主,但是其他Follower与Leader正常;
- 新节点加入集群。
流程:
a. 如果logicalclock相同,将该数据保存到投票箱,根据当前投票箱的投票判断对方推荐的leader是否得到多数人的同意,如果是则设置状态退出选举过程,否则到b。
b.这是一条与当前logicalclock不符合的消息,或者对方推荐的leader没有得到多数人的同意(有可能是收集到的投票数不够)。那么说明可能在另一个选举过程中已经有了选举结果,于是将该选举结果加入到OutOfElection集合中(OutOfElection用于保存那些状态为FOLLOWING或者LEADING的ZooKeeper节点发送的选票,由于对方的状态为FOLLOWING或者LEADING,所以它们当前不参与选举过程),再根OutOfElectio来判断推荐的leader是否有多数人同意,且存活。如果设置当前logicalclock,设置状态,退出选举过程。否则继续循环。
Phase 1: 发现
这个阶段的主要目的是发现当前大多数节点接收的最新 Proposal,然后准Leader生成新的epoch ,让 Followers接收,更新它们的acceptedEpoch。最后准leader选取一个Follower,使用其作为初始化事务集合。
如果是采用FastLeaderElection算法的话,选出来的Leader其实就拥有了最大的epoch
流程:
Follower将自己最后接收的Proposal的epoch值发送给准Leader
接收到来自过半Follower的epoch后,会从这些epoch中选择最大的epoch并+1,将它再发送给过半的Follower
Follower收到新的epoch,检测最后处理过的proposal的epoch,如果小于等于新的epoch,那么更新为新的epoch,同时向准Leader发送Ack消息(包含当前的epoch以及历史处理过得Proposal的集合 ),如果大于,将重新进入Phase 0。
准Leader收到过半Ack后,会从这过半的Ack中选择epoch最大的Follower(如果相等选zxid最大的),将其历史Proposal的集合置为当前的初始化事务集合
Phase 2: 同步
该阶段主要是利用 Leader 前一阶段获得的最新 Proposal 历史,同步集群中所有的副本。
流程:
准Leader会发送消息给Quorum (一个过半的节点集合)中的Follower(消息内容=新的epoch+准Leader初始化的历史Proposal集合)
Follower接收到消息后,如果发现自己的epoch与准Leader的不相等,那么无法同步;反之,Follower会接受该事务提议,将其以事务日志的形式写入到本地磁盘中,写入成功后,反馈给Leader服务器ACK。
当准Leader收到过半Follower的反馈后,会向Follower发送Commit消息
Follower收到Commit后会按zxid顺序依次提交准Leader发来的且未处理过的事务完成该步骤后,准Leader才真正称为Leader
Phase 3: 广播
到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 Leader 可以进行消息广播。同时,如果有新的节点加入,还需要对新节点进行同步。
流程:
客户端提交事务,leader生成与之对应的事务Proposal,并将该Proposal(提议)发给所有的Follower机器
Follower收到Proposal后,将其以事务日志的形式写入到本地磁盘中。写入成功后反馈给Leader一个ACK
Leader收到Quorum的ACK后,自身先完成对事务的提交之后广播Commit消息给所有Follower服务器
Follower按ZXID顺序执行事务的提交
在消息广播过程中,Leader服务器会为每一个Follower服务器分配一个队列,然后将事务提议依次放入到这些队列中去,并且根据FIFO的策略进行消息发送。并基于具有FIFO特性的TCP协议来进行网络通信。因此能够保证消息广播过程中发送与接受的顺序性。