zookeeper源码阅读之七(leader选举)

本文围绕Zookeeper的FastLeaderElection选举机制展开。介绍了重要术语如zxid、electionEpoch等,阐述服务器状态切换,重点讲解FastLeaderElection策略,包括选举的网络IO、消息数据结构,详细分析lookForLeader方法,该方法通过选票比较决定是否变更选票,选出包含所有已提交事务的leader。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

zookeeper的所有的事务请求都是leader来完成的,而当leader宕机后会进行leader的选举操作,leader选举是zookeeper最重要的技术之一,也是zookeeper实现的zab协议的最开始的阶段。zab协议在Zookeeper's atomic broadcast protocol:Therory and practice这篇论文中有详细介绍。

重要术语

  • zxid:这个值是表示的是zookeeper中的事务日志Id,不过在下面的选举中都是指的事务日志的最后一条日志的zxid
  • electionEpoch:这表示的是选举的周期
  • epoch:这表示的是leader的周期,这个值以前是直接从zxid中获取,之前是zxid的前32位。

代码实现

zookeeper的服务器主要是在looking,observing,following,leading这四个状态进行切换,当状态在looking状态时则表示在进行leader选举,其他状态在异常时也会进入这个状态,下面代码

FastLeaderElection

zookeeper实现了多种选举策略,不过现在基本上只使用FastLeaderElection这一种策略,而这个FastLeaderElection的主要选举逻辑在其lookForLeader方法中,在具体介绍这个方法之前,先看一下其相关的其他的模块的逻辑。

选举的网络IO

下面图展示了FastLeaderElection的选举网路IO,FastLeaderElection获取其他节点的选票信息和发送选票信息则是通过sendqueuerecvqueue这两个队列来完成的,然后对应的WorkerSender会将消息分发到各个节点对应的发送线程中去,而WorkerReciever则是对各个节点发回的消息进行解析。对于当前节点到其他的所有节点的tcp连接都会分配两个线程,一个sendWorker和一个recvWorker,分别处理将消息写入IO流中以及从IO流中读取消息的操作。对于集群中的任意的两个节点都会有一条连接,这条连接是sid的连向sid小的节点。

消息数据结构

下面图展示了选举中各个模块主要维护的数据

首先是FastLeaderElection维护的:

  • proposedLeader:当前的节点投票的leder
  • proposedZxid: 当前节点投票的leader对应的zxid
  • proposedEpoch:当前节点投票的leader对应的epoch
  • logicalclock:表示的是当前节点的选举周期

ToSend和Notification中的数据差不多,都是节点的信息交换的数据:

  • version:这表示的是协议的版本号,主要是对于滚动升级时和低版本的协议的兼容
  • leader:这个节点投票的leader
  • zxid:这个节点投票的leader对应的zxid
  • electionEpoch:这个节点投票的leader对应的选举周期
  • state:这个节点所处的状态
  • peerEpoch: 这个节点投票的leader所对应的epoch
  • qv:这个时当前节点所知道的集群的信息(主要是动态配置加入的,后续分享再分析)

Vote的数据主要也是这些数据

  • id: 投票对应的leader的sid
  • zxid:这个投票的leader对应的zxid
  • electionEpoch:这个投票的leader对应的选举周期
  • state:这个节点所处的状态
  • peerEpoch: 这个节点投票的leader所对应的epoch

 lookForLeader

下面主要来分析一下lookForLeader这个方法,其主要逻辑是从recvQueue中获取其他节点的选票信息,让后来和其他的节点的选票进行比较,从而来决定是否变更自己的选票,而再变更自己选票时则也会将自己变更后的选票广播给其它的节点,当当前的节点收到了对同一个节点的过半选票后,则会将对应的节点设置为候选leader,并退出选举阶段,进行后续的恢复阶段。

对于选票的比较方式则主要时通过epoch,zxid和sid来进行比较的,这种比较方式保证了最终选出的leader包含了所有的commit了的事务。

//这里的选举主要是服务器之间交换数据,这其中可能会有节点最终选出的leader不是最终选出的leader
//但是其选出的候选leader最终会由于没有过半的follower连接而退出leader,重新开始选举
//但是这会等待initLimit*tickTime时间
public Vote lookForLeader() throws InterruptedException {
     //jmx相关
     self.start_fle = Time.currentElapsedTime();
     try {
         Map<Long, Vote> recvset = new HashMap<Long, Vote>();//当前logicalClock下的其他的节点的投票
         //此处存储的是对于没有logicalClock验证的消息,因为对于在leading和following状态的节点
         //其logicalClock是不会变的,但是对于断线的新的节点,其logicalClock会加1,从而比稳定的leader和follower节点的
         //electionEpoch要大,故而如果加上了logicalClock的判断,则其可能一直都无法选举成功
         Map<Long, Vote> outofelection = new HashMap<Long, Vote>();

         int notTimeout = minNotificationInterval;
         //这里的logicalclock从最开始的版本就存在了,那时选举时
         synchronized (this) {
             logicalclock.incrementAndGet();//增加electionEpoch的周期数量
             updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());//最开始的投票时投给自己
         }
         sendNotifications();//广播消息给其他的节点
         SyncedLearnerTracker voteSet;
         while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {//交换notification知道发现leader
             Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);//从recvqueue中获取notification
             if (n == null) {
                 if (manager.haveDelivered()) {//至少有一个节点已经被发送消息了
                     sendNotifications();//发送消息给配置中的集群的所有的节点
                 } else {
                     manager.connectAll();//一条消息都没发送出去,尝试重新连接所有的连接
                 }
                 //这里等待时间指数上升 这段代码是在https://issues.apache.org/jira/browse/ZOOKEEPER-230加入的
                 int tmpTimeOut = notTimeout * 2;
                 notTimeout = Math.min(tmpTimeOut, maxNotificationInterval);
             } else if (validVoter(n.sid) && validVoter(n.leader)) {
                 switch (n.state) {
                 case LOOKING:
                 	 //这里-1表示的是当前的节点可能丢失了持久化的数据,不应该让其参与选举
                 	 //这里的代码时https://issues.apache.org/jira/browse/ZOOKEEPER-261中加入的
                     if (getInitLastLoggedZxid() == -1) {
                         break;
                     }
                     if (n.zxid == -1) {
                         break;
                     }
                     if (n.electionEpoch > logicalclock.get()) {//其他节点的选举周期比当前节点的周期大
                         logicalclock.set(n.electionEpoch);//将当前周期赋值为其他节点的周期,主要是来进行同步
                         recvset.clear();//把当前节点收到的之前的选票都清理掉,不在考虑之前的选举数据
                         //这里比较的是当前节点的初始选票而不是当前的选票,因为当前的选票已经无效
                         if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
                             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)) {//选举周期相同则看其他节点的选票是否比当前节点的选票要更符合,从而更新当前节点的选票
                         updateProposal(n.leader, n.zxid, n.peerEpoch);
                         sendNotifications();
                     }
                     recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));//更新对应节点的选票信息
                     voteSet = getVoteTracker(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch));
                     if (voteSet.hasAllQuorums()) {//检查这个选票是否过半
                         while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {
                             //等待当前收到的其他信息的节点的队列finalizeWait时间来保证其他节点的选票不会影响当前结果   
                             //个人认为主要是避免这样的情况,A,B,C,B发送节点给A,然后A将自己的选票改为B,此时A节点的选票中B节点已
                             //经有两张选票,此时A中认为的leader是B,但是对于B,它在收到A的选票改变之前收到了C的选票,因此其将自己
                             //的选票改位C,并广播出去,然后C收到B的选票为C,此时B和C都认为C为leader。
                             //如果不处理后续的选票,则A认为的leader为B,B和C认为的leader则为C.                    
                             if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
                                 recvqueue.put(n);
                                 break;
                             }
                         }
                         if (n == null) {//此处表示等待了finalizeWait时间没有其他节点来影响选举结果,则退出选举并返回选举结果
                             setPeerState(proposedLeader, voteSet);
                             Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
                             leaveInstance(endVote);
                             return endVote;
                         }
                     }
                     break;
                 case OBSERVING:
                     LOG.debug("Notification from observer: {}", n.sid);
                     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.state));
                         voteSet = getVoteTracker(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
                         if (voteSet.hasAllQuorums() && checkLeader(recvset, n.leader, n.electionEpoch)) {
                             setPeerState(n.leader, voteSet);
                             Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
                             leaveInstance(endVote);
                             return endVote;
                         }
                     }
                     //这里没有考虑logicalClock,因为对于leading和following状态的节点其对应的logicalClock是不会改变
                     //所以此处是直接检查也没有过半的节点完成选举
                     outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
                     voteSet = getVoteTracker(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
                     if (voteSet.hasAllQuorums() && checkLeader(outofelection, n.leader, n.electionEpoch)) {
                         synchronized (this) {
                             logicalclock.set(n.electionEpoch);
                             setPeerState(n.leader, voteSet);
                         }
                         Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
                         leaveInstance(endVote);
                         return endVote;
                     }
                     break;
                 default:
                     LOG.warn("Notification state unrecoginized: {} (n.state), {}(n.sid)", n.state, n.sid);
                     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 {
        //jmx
     }
 }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值