提到ZAB,恐怕大家第一时间就会想到Zookeeper,然后由Zookeeper又会联想到Paxos。这之间的联系是不是因为有本畅销书叫《从Paxos到Zookeeper分布式一致性原理与实践》,使得大家常常把Zookeeper和Paxos关联起来,毕竟“买了就是读了”,开个玩笑,ZAB和Paxos的确也是有很多相似之处的,理解了Paxos也的确对学习其它分布式一致性或共识算法非常有帮助。不过Paxos的内容我们以后再说,在这里只讨论ZAB。
ZAB的全称为Zookeeper Atomic Broadcast,其实大家看到“Atomic”和“Broadcast”两个词,应该就能大概明白ZAB的主要工作方式了。他是一个为Zookeeper量身定制的支持崩溃恢复的原子广播协议,用于保障Zookeeper各副本之间在正常或异常情况下的数据一致性。
既然是支持崩溃恢复的原子广播协议,那我们介绍ZAB就可以从他的这两个名词说起,分别是“原子广播“和“崩溃恢复“。
原子广播
在ZAB协议中,存在两种角色,Leader和Follower(在Zookeeper中实际上还有一个角色叫Observer,但他和ZAB协议没有直接关系,所以在这里不做讨论。),Leader负责数据的读和写请求,Follower只负责读请求,外部应用可以给任意的Zookeeper端发送请求,所以如果是写请求,就会转给Leader处理,读的话则就地响应。这样做同时也是为了达到所有写请求都能有序处理的效果。
“原子”可以理解为事务,在Zookeeper收到一个数据写请求后,对其分配一个全局唯一且递增的Zxid,然后将该请求转化为事务Proposal,严格按照请求的接收次序放到针对每个Follower的FIFO队列中,即向集群中所有Follower广播该数据,Follower成功收到数据后,会发送Ack给Leader,当Leader收到的Ack超过半数,则向Follower发送commit命令,完成事务的提交。
可以看出在这个分布式事务的提交过程中是遵循了2PC协议的,即事务的预处理请求和事务提交是分成两阶段进行的,不同之处在于2PC要求所有副本应答,而ZAB只要求超过半数的副本应答即可,这样也避免了2PC单点超时造成阻塞的问题。
崩溃恢复
Zookeeper作为一个典型的CP(一致性/分区容错性)系统,在设计上必须考虑节点异常的情况,所以ZAB针对崩溃恢复的设计是必不可少的,这也是Zookeeper抛弃可用性的证明,在崩溃恢复过程中,Zookeeper服务对外是不可用的。
崩溃恢复的过程可以分成两个阶段来说:一是Leader选举,二是数据同步。
1、Leader选举
如果Leader节点崩溃,则Follower节点的状态会从FOLLOWING变为LOOKING,这里节点的状态是用一个枚举标识的(码 1),即进入选举状态,选举的方式简单来讲就是看谁能得到超过半数的选票。
// 码 1 QuorumPeer.java:节点状态枚举
public enum ServerStatepublic enum ServerState {
LOOKING,
FOLLOWING,
LEADING,
OBSERVING
}
选票的信息见class Vote(码 2),还记得刚才提到的Leader为每个事务分配的Zxid吧,该字段为一个64位长整形,其中高32位称作Epoch,低32位是一个递增的计数器(码 3)。Epoch代表了Leader的编号,每次选举出了新的Leader,该数值就被+1,并将Counter清0,之后该Leader每收到一个请求,都会将Counter+1。
// 码 2 Vote.java:选票结构
public class Vote {
private final int version;
private final long id; //服务器ID
private final long zxid; //Epoch + Counter
private final long electionEpoch; //选举轮次
private final long peerEpoch; //被推举的Leader所在的选举轮次
private final ServerState state; //当前服务器状态
}
// 码 3 ZxidUtils.java:Zxid结构
public class ZxidUtils {
public static long getEpochFromZxid(long zxid) {
return zxid >> 32L;
}
public static long getCounterFromZxid(long zxid) {
return zxid & 0xffffffffL;
}
public static long makeZxid(long epoch, long counter) {
return (epoch << 32L) | (counter & 0xffffffffL);
}
public static String zxidToString(long zxid) {
return Long.toHexString(zxid);
}
}
在选举初期,每个节点都会初始化自身选票(Vote实例化),节点默认都是推举自己做Leader的,之后将自己的信息填到选票后放到队列中发送给其它节点,也包括他自己,当其他节点收到了选票之后,先对比electionEpoch是否和自身一样,如果比自身大,则清空自身的Vote和已收到的选票,更新electionEpoch后重新对比。如果比自身小,则直接丢弃该选票。如果和自身一样,则会进行下一阶段的对比,这个对比次序依次为peerEpoch、zxid和id,规则为比自身大,则更新自身选票,比自身小,则丢弃(码 4)。所有对比更新完成后,发出自身选票。最后统计所有选票,当某个节点的票数超过一半(Quorum规则),则该节点被推举为新的Leader。
// 码 4 FastLeaderElection.java:选票对比
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
LOG.debug(
"id: {}, proposed id: {}, zxid: 0x{}, proposed zxid: 0x{}",
newId,
curId,
Long.toHexString(newZxid),
Long.toHexString(curZxid));
if (self.getQuorumVerifier().getWeight(newId) == 0) {
return false;
}
/*
* We return true if one of the following three cases hold:
* 1- New epoch is higher
* 2- New epoch is the same as current epoch, but new zxid is higher
* 3- New epoch is the same as current epoch, new zxid is the same
* as current zxid, but server id is higher.
*/
return ((newEpoch > curEpoch)
|| ((newEpoch == curEpoch)
&& ((newZxid > curZxid)
|| ((newZxid == curZxid)
&& (newId > curId)))));
}
在新Leader被选举出后,则将自己的状态从LOOKING更新为LEADING,其它节点变为FOLLOWING,然后进入数据同步阶段。
2、数据同步
在新Leader正式开始工作之前,每个Follower会主动和Leader建立连接,然后将自己的zxid发送给Leader,Leader从中选出最大的Epoch并将其+1,作为新的Epoch同步给每个Follower,在Leader收到超过半数的Follower返回Epoch同步成功的信息之后,进入数据对齐的阶段。对齐的过程也比较直接,如果Follower上的事务比Leader多,则删除,比Leader少,则补充,重复这个过程直到过半的Follower上的数据和Leader保持一致了,数据对齐的过程结束,Leader正式开始对外提供服务。
还有一种情况是,在新Leader选举出来之后,原来挂掉的Leader又重新连接上了,那么此时因为原Leader所持有的Epoch已经比新Leader的Epoch小了,故原Leader将会变为Follower,并和新的Leader完成数据对齐。
在这个数据对齐过程中还是有很多细节的,感兴趣的人可以从源码再进行更深入的研究,我这里就不做介绍了。
* 所用源码均引自 apache-zookeeper-3.6.0
* https://zookeeper.apache.org/releases.html