ZAB原子广播协议原理

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)

ZookeeperLeader ZookeeperFollower 发送FollowerInfo 1 发送LeaderInfo 2 发送AckEpoch 3 opt [收到过半Follower节点] SNAP(全量同步)场景 发送全量日志数据 4 等待Leader发送NewLeader消息 5 发送NewLeader消息 6 发送Ack,响应NewLeader 7 发送Uptodata消息,告知Follower可响应客户端 8 DIFF(差异同步)场景 发送Diff,差异区间 9 发送Proposal提案 10 处理提案 11 发送Commit,提交提案 12 提交提案 13 loop [发送提案] 等待Leader发送NewLeader消息 14 发送NewLeader消息 15 发送Ack,响应NewLeader 16 发送Uptodata消息,告知Follower可响应客户端 17 TRUNC(截断)场景 发送Trunc,截断位置 18 发送Proposal提案 19 处理提案 20 发送Commit,提交提案 21 提交提案 22 loop [发送提案] 等待Leader发送NewLeader消息 23 发送NewLeader消息 24 发送Ack,响应NewLeader 25 发送Uptodata消息,告知Follower可响应客户端 26 ZookeeperLeader ZookeeperFollower

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,即是只同步差异的消息日志,条件如下:

  1. peerLastZxid == lastProcessedZxid
  2. proposals.size() > 0 && peerLastZxid <= maxCommitLogId && peerLastZxid >= minCommitLogId && prevProposalZxid >= peerLastZxid
    满足任意一个条件即可,条件1,发送的差异数据集为空

部分同步-TRUNC

TRUNC,意味截断,所谓截断就是Follower上存在的数据,Leader上不存在,在与Leader同步前,先把Follower单方拥有的数据抹掉,然后再与Leader,发生条件:

除上述两个场景之外,就是需要截断的场景

消息广播

在第二阶段的时序图中,数据同步之后,Leader会发送UPTODATE消息给Follower,就是告诉Follower可以对外响应请求了。做一些其他工作之后,就进入了消息广播阶段。

消息广播的如就如同第二阶段给出的时序图所述。简单描述如下

  1. 客户端发起写请求
  2. Leader生成提案发送给Follower
  3. 等待过半的节点收到之后,发送提交给Follower
  4. 等待过半的节点确认,即向客户端响应

上述过程可能没那么严谨,但大致是如此。

事后补充

经再次阅读源码之后,在消息广播阶段,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转发客户端写请求的代码。

结束语

本篇并没有详细说明第一阶段,第一阶段代码里已添加上了注释,对开发者而言应该容易理解的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值