ZAB简介
ZAB是为原子广播协议,是zookeeper实现强一致性的共识算法。ZAB分三个阶段,分别是:Leader选举(发现)、崩溃恢复、消息广播
Leader发现
在具体介绍之前,先解释几个名次。
逻辑时钟(electionEpoch):又名选举轮次,在Leader发现阶段,节点需要发送投票给其他节点,逻辑时钟表达的是大家需要在一个选举轮次上。当集群中某一个节点被过半的节点所支持,则其成为集群Leader
currentEpoch:这个值表达的是周期含义, 也就是说不同的Leader会有不同的epoch值, 值越大,表明其拥有的数据越全。
zxid:这个值表达的是真个集群中的全局事务ID,具有唯一性,此值仅有Leader节点产生,因集群只有一个Leader,故做到唯一性是比较简单的。
zxid是一个long类型的值,数据有两部分含义,高32位表达的就是上面所说的currentEpoch,低32位是序列号, epoch增1,低32位从0开始计数。计算方式如下:
zxid = currentEpoch << 32 | sequenceNumber
sid: 即是server.id, 每一个节点都有一个此值,且集群内不可重复
ZAB的Leader发现阶段,整体的思想就是选举集群中拥有最全数据的节点为集群的Leader,各节点发起的投票需要进行PK,PK的逻辑就是根据上面所说的那几个值,按照以下的优先级进行PK,较大者胜出:
currentEpoch > zxid > sid
Leader发现的具体过程,下面贴出源码,并附加了详细的注释(依赖的方法没有列出),其源码在 FastLeaderElection中
public Vote lookForLeader() throws InterruptedException {
// 省略部分代码...
try {
// 其他非Observer节点发来的投票,key是发送者的server.id,value是发送者的投票信息,包含了发送者提议的leader的server.id,epoch,zxid等
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
// 完成选举的节点发来的投票, 发送者的状态是LEADING或FOLLOWERING
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized(this){
// 逻辑时钟初始化, 逻辑时钟也就是下文的electionEpoch,表达的是选举轮次
logicalclock.incrementAndGet();
// 使用当前的数据来初始化当前节点提议的leader,从这里可以看出,默认节点先投自己的
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
// 发送当前节点的投票信息给其他节点
sendNotifications();
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)){
/*
* 从接受队列中获取其他节点发来的投票数据
* (无需在意里面的数据,无非就是 发送者的server.id,提议的leader的server.id, epoch,electionEpoch,zxid)
*/
Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS);
if(n == null){
if(manager.haveDelivered()){
// 发送队列为空,则会重新给其他节点发送当前节点的投票信息
sendNotifications();
} else {
// 发送队列不为空, 可能是由于连接断开或其他网络问题导致, 这里是尝试和其他节点重建连接
manager.connectAll();
}
/*
* 这里等待的时间加倍,至于用途,不太好描述, 有利于下面的处理
*/
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
}
else if(validVoter(n.sid) && validVoter(n.leader)) {
//
//这里的判断,是检测发送者(n.sid),和leader(n.leader) 是否在投票视图里,投票视图,可简单的认为是非观察者节点
// 根据发送者的节点状态分别处理
switch (n.state) {
case LOOKING:
// 当前节点的选举轮次(逻辑时钟)落后于该发送者
if (n.electionEpoch > logicalclock.get()) {
// 更新当前节点逻辑时钟
logicalclock.set(n.electionEpoch);
// 清空当前节点收到的投票
recvset.clear();
// 此方法是进行当前节点提议的leader,和该发送者提议的leader 进行PK
// 由此可知, (zxid,epoch,server.id) 三元组进行对比
// 具体规则是
// epoch大的胜出,zxid大的胜出,server.id大的胜出
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
// 发送者提议的leader胜出,则更新当前节点的提案为发送者的提案
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {
// 如上面所说的的初始化
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
// 重新发送当前节点提案给其他节点
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
// 发送者选举轮次落后于当前节点,直接忽略
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)) { // 提案PK
updateProposal(n.leader, n.zxid, n.peerEpoch);
sendNotifications();
}
// 添加进已收到投票的集合
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
// 仲裁: 判断当前节点的提案是否已过半
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {
// 这一步的目的,是处理接受队列里剩下的投票信息, 等待时间和前面有一处是2倍此时间的相呼应
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){
// 意味者,刚才仲裁的出的leader 没有胜出,放回队列,重新处理
recvqueue.put(n);
break;
}
}
if (n == null) {
// leader为当前节点则设置为leading,否则为following或observing
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock.get(),
proposedEpoch);
leaveInstance(endVote);
return endVote; // 完成选举
}
}
break;
case OBSERVING:
// 忽略observing的投票
break;
case FOLLOWING:
case LEADING:
// 只处理和当前节点在一个选举轮次的
if(n.electionEpoch == logicalclock.get()){
recvset.put(n.sid, new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch));
// 此方法也是仲裁,从参数中可以看出, 不再以当前节点的提案进行仲裁了,而是已提案n 为仲裁对象
// 此方法仲裁逻辑:
// 1.先看recvset 中支持 n 的是否过半
// 2. 如果1的条件满足过半, 则继续判断
// 3. 如果n.leader为当前节点, 且n.electionEpoch和当前节点相等,则n.leader为集群leader
// 4. 若3不满足,则查看outofelection集合中是否有 n.leader发送的投票,如果有,则n.leader为集群leader
if(ooePredicate(recvset, outofelection, n)) {
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
// n.sid 发送者已经完成选举,实际上再进入新的一轮选举之前,不会再发来投票信息
outofelection.put(n.sid, new Vote(n.version,
n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch,
n.state));
/*
* 这里是查看已完成选举的节点中, 看他们是否都推举n.leader为集群leader
*/
if(ooePredicate(outofelection, outofelection, n)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
}
Vote endVote = new Vote(n.leader,
n.zxid,
n.electionEpoch,
n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
break;
}
} else {
if (!validVoter(n.leader)) {
LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
}
if (!validVoter(n.sid)) {
LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
}
}
}
return null;
} finally {
}
}
看了上面的代码,也许大家会对Leading或Following 分支的逻辑有疑惑,比如什么情况下会走这种分支,主要是上面代码不全。下面的代码是为FastLeaderElection.WorkerReceiver.run()方法里的一段代码
if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){
recvqueue.offer(n);
// 省略...
} else {
/**
* 当前节点状态为非Looking, 则不会进入放入接收队列中,因为当前节点已退出选举过程
*/
Vote current = self.getCurrentVote();
if(ackstate == QuorumPeer.ServerState.LOOKING){
ToSend notmsg; // using current build object
if(n.version > 0x0) {
notmsg = new ToSend(ToSend.mType.notification,
current.getId(),
current.getZxid(),
current.getElectionEpoch(),
self.getPeerState(),
response.sid,
current.getPeerEpoch());
}else {
Vote bcVote = self.getBCVote();
notmsg = new ToSend(ToSend.mType.notification,
bcVote.getId(),
bcVote.getZxid(),
bcVote.getElectionEpoch(),
self.getPeerState(),
response.sid,
bcVote.getPeerEpoch());
}
sendqueue.offer(notmsg);
}
Leader发现阶段完成之后,就进入了第二阶段:崩溃恢复
崩溃恢复
崩溃恢复,就是指新的Leader产生后,非Leader节点要与新的Leader节点同步数据,数据同步之后,才可响应外部请求,即进入消息广播阶段。
崩溃恢复存在三种场景:
- 全量同步[SNAP],Leader把其数据全量同步给Follower或Observer
- 部分同步[DIFF],Leader 把存在差异的,也就是Follower或Observer没有的数据同步过去
- 截断[TRUNC],截断也可以说是部分同步,主要是把Leader上不存在的数据,但Follower或Observer节点上存在,需要Follower或Observer将其截断,也就是删掉
Leader被选举出之后,Leader节点就等待Follower或Observer来与其通信,下面是交互的时许图:
(下文不再区分Follower或Observer)
FollowerInfo:此消息类型是Follower向Leader报告其已接受的最大的acceptedEpoch,Leader用此值来确定最终的epoch
AckEpoch:此消息是Follower向Leader报告其lastLoggerZxid值,也就是下面的peerLastZxid
Proposal:提案消息
Commit:提交提案消息
NewLeader:告诉Follower,本轮此的初始zxid
Uptodate:告诉Follower,可以响应客户端请求
说明:在 0x100000 版本之前 NewLeader的消息发送在同步数据之前
前提假设:
- peerLastZxid:为Follower节点上最新的数据的zxid
- maxCommitLogId: 为Leader节点上最大的已提交日志zxid
- minCommitLogId: 为Leader节点上最小的已提交日志zxid
- lastProcessedZxid: 为Leader节点上最后被处理的zxid
- preZxid: 为Leader节点上 max(小于等于peerLastZxid日志) 的最大zxid
- proposals: 为Leader上已提交的消息日志
全量同步(SNAP)
全量同步也即是快照下载,Follower下载Leader的数据。发生的条件如下:
proposals.isEmpty() || peerLastZxid < minCommitLogId
部分同步-DIFF
DIFF,即是只同步差异的消息日志,条件如下:
- peerLastZxid == lastProcessedZxid
- proposals.size() > 0 && peerLastZxid <= maxCommitLogId && peerLastZxid >= minCommitLogId && prevProposalZxid >= peerLastZxid
满足任意一个条件即可,条件1,发送的差异数据集为空
部分同步-TRUNC
TRUNC,意味截断,所谓截断就是Follower上存在的数据,Leader上不存在,在与Leader同步前,先把Follower单方拥有的数据抹掉,然后再与Leader,发生条件:
除上述两个场景之外,就是需要截断的场景
消息广播
在第二阶段的时序图中,数据同步之后,Leader会发送UPTODATE消息给Follower,就是告诉Follower可以对外响应请求了。做一些其他工作之后,就进入了消息广播阶段。
消息广播的如就如同第二阶段给出的时序图所述。简单描述如下
- 客户端发起写请求
- Leader生成提案发送给Follower
- 等待过半的节点收到之后,发送提交给Follower
- 等待过半的节点确认,即向客户端响应
上述过程可能没那么严谨,但大致是如此。
事后补充
经再次阅读源码之后,在消息广播阶段,Leader 节点和Observer 节点的通讯与Follower不同,Leader 会发送提案给Follower节点,但不发送给Observer,在提交阶段,Leader发送 Commit消息给Follower,发送INFORM(包含提案数据)消息给Observer,且Observer不会Ack Leader的命令,但Follower是需要的(Observer既不参与Leader选举,也不参与过半仲裁)。
这里简单说明一下,zookeeper处理请求使用了责任链(也有称职责链)模式,大家可以参考源码LeaderZookeeperServer,FollowerZookeeperServer,ObserverZookeeperServer 等类的 setRequestProcessor方法,可以看到各个角色所有使用的RequestProcessor的类型,以及顺序的组织和每一个RequestProcessor各自都干些什么事。 在这里你会发现,follower和observer转发客户端写请求的代码。
结束语
本篇并没有详细说明第一阶段,第一阶段代码里已添加上了注释,对开发者而言应该容易理解的。