深入理解Zookeeper一致性协议:Zab协议
分布式一致性算法
分布式一致性算法可以保证多个数据节点上有一致的数据,目前有Paxos、Raft、Zab和Gossip几种,其中Zookeeper使用的是Zab,而Zab又和Raft比较类似,因此本文主要记录一下Raft和Zab。
ZAB协议用途
ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务zookeeper专门设计的一种支持崩溃恢复的原子广播协议。在zookeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,zookeeper实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB协议介绍
ZAB协议包含两种基本模式,分别是:
1》崩溃恢复之数据恢复
2》消息广播之原子广播
当整个集群正在启动时,或者当leader节点出现网络中断、崩溃等情况时,ZAB协议就会进入恢复模式并选举产生新的leader,当leader服务器选举出来后,并且集群中有过半的机器和该leader节点完成数据同步后(同步指的是数据同步,用来保证集群中过半的机器能够和leader服务器的数据状态保持一致),ZAB协议就会退出恢复模式。
当集群中已经有过半的Follower节点完成了和Leader状态同步以后,那么整个集群就进入了消息广播模式。这个时候,在Leader节点正常工作时,启动一台新的服务器加入到集群,那这个服务器会直接进入数据恢复模式,和leader节点进行数据同步。同步完成后即可正常对外提供非事务请求的处理。
消息广播(原子广播)
消息广播实际上是一个简化版的2PC提交过程。
过程:
1》leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid,通过zxid的大小比较就可以实现因果有序这个特征。
2》leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的 follower。
3》当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
4》当leader接收到合法数量(超过半数节点)的ack后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
5》当follower收到消息的commit命令以后,会提交该消息。
崩溃恢复(数据恢复)
ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下是没有任何问题的,但是一旦Leader节点崩溃,或者由于网络问题导致Leader服务器失去了过半的Follower节点的联系(leader失去与过半follower节点联系,可能是leader节点和 follower节点之间产生了网络分区,那么此时的leader不再是合法的leader了),那么就会进入到崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader。
为了使leader挂了后系统能正常工作,需要解决以下两个问题:
1》已经被处理的消息不能丢失
当leader收到合法数量follower的ack后,就向各个follower广播commit命令,同时也会在本地执行commit并向连接的客户端返回「成功」。但是如果各个follower在收到commit命令前leader就挂了,导致剩下的服务器并没有执行到这条消息。
leader对事务消息发起commit操作,该消息在follower1上执行了,但是follower2还没有收到commit,leader就已经挂了,而实际上客户端已经收到该事务消息处理成功的回执了。所以在zab协议下需要保证所有机器都要执行这个事务消息,必须满足已经被处理的消息不能丢失。
2》被丢弃的消息不能再次出现
当leader接收到消息请求生成proposal后就挂了,其他follower并没有收到此proposal,因此经过恢复模式重新选了leader后,这条消息是被跳过的。 此时,之前挂了的leader重新启动并注册成了follower,他保留了被跳过消息的proposal状态,与整个系统的状态是不一致的,需要将其删除。(leader都换代了,所以以前leader的proposal失效了)
针对崩溃恢复的两种情况分析
ZAB协议需要满足上面两种情况,就必须要设计一个leader选举算法,能够确保已经被leader提交的事务Proposal能够提交、同时丢弃已经被跳过的事务Proposal。
针对这个要求:
如果leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最高编号(ZXID 最大)的事务Proposal,那么就可以保证这个新选举出来的leader一定具有已经提交的提案。因为所有提案被commit之前必须有超过半数的follower ack,即必须有超过半数节点的服务器的事务日志上有该提案的proposal,因此只要有合法数量的节点正常工作,就必然有一个节点保存了所有被commit消息的proposal状态。
另外一个,zxid是64位,高32位是epoch编号,每经过一次Leader选举产生一个新的leader,新的leader会将epoch号+1,低32位是消息计数器,每接收到一条消息这个值+1,新leader选举后这个值重置为0。这样设计的好处在于老的leader挂了以后重启,它不会被选举为leader,因此此时它的zxid肯定小于当前新的leader。当老的leader作为follower接入新的leader后,新的leader会让它将所有的拥有旧的epoch号的未被commit的proposal清除。
关于ZXID
zxid,也就是事务id,为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid,实际中zxid是一个64位的数字,它高32位是epoch(ZAB协议通过epoch编号来区分Leader周期变化的策略)用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch=(原来的epoch+1),标识当前属于那个leader的统治时期。低32位用于递增计数。
epoch:可以理解为当前集群所处的年代或者周期,每个leader就像皇帝,都有自己的年号,所以每次改朝换代,leader变更之后,都会在前一个年代的基础上加1。这样就算旧的leader崩溃恢复之后,也没有人听他的了,因为follower只听从当前年代的leader的命令。
测试epoch的变化
要测试epoch的变化可以做一个简单的实验:
1》启动一个zookeeper集群。
2》在 /tmp/zookeeper/VERSION-2 路径下会看到一个currentEpoch文件,文件中显示的是当前的epoch。
3》把leader节点停机,这个时候在看currentEpoch会发生变化。 随着每次选举新的leader,epoch都会发生变化。
1>先找到leader节点
2>停止leader节点的服务
3>查看当前epoch号的变化(epoch号由原来的6变为7)
总结: