Zookeeper选举(fastleaderelection算法)

1、选举相关概念:

选票:(myid,zxid,当前节点选取轮次,被推举服务器选举轮次,状态(looking))。

选举发生情况:启动时选举,运行时选举。

外部投票:其他服务器发送来的投票。

内部投票:服务器自身当前的投票。

选举轮次:epoch--leader选举周期。

pk:比较内部选票和外部选票,确定是否变更内部选票,主要是比较zxid和myid。

 

(1)每个服务器节点先初始化自己的选票,即(myid,zxid,当前节点选取轮次,被推举服务器选举轮次,状态(looking))。

(2)发送初始化选票到所有集群中的节点。

(3)接收外部选票

(4)判断选举轮次,内部选票选举轮次要是大于外部选票,就继续接收外部选票,如果小于等于外部的选举轮次,就进行选票pk,即判断自己是否要变更内部选票。

(5)变更内部选票,将选票发送至集群中。

(6)归档。每个节点将收到的所有外部选票进行归档。

(7)统计。判断是否有过半的服务器认可当前内部选票,如果是,那就选举结束,即超过一半选票同意新leader,那就成功。

2、选举过程:

Zookeeper选举Leader分为启动时选举和服务运行期间的选举。在往下看之前需要先了解服务器的几个状态:

  • LOOKING:观望状态,此时还未选举出Leader
  • LEADING:该服务器为Leader
  • FOLLOWING:该服务器为Follower
  • OBSERVING:该服务器为Observer

在构建Zookeeper集群时,最少服务器数量是2,但是服务器数量最好为奇数(因为需要过半服务器的支持才能完成投票和决策,如果是6台,那么最多允许挂掉2台服务器,存活4台进行投票决策;而如果是5台,那么最多允许挂掉的台数也是2台,但是只有3台进行投票决策。因此,相比较而言,奇数台服务器数量容错率更高的同时还降低了网络通信负担),因此,这里使用3台服务器说明,分别是server1、server2、server3。

启动时选举

启动服务器,当选举完成前,所有服务器的状态都是LOOKING。当启动server1(我们假设其myid为1,myid就是服务器id,需要自己配置,后面实际操作时会讲)时,一台服务器无法完成选举,因为需要过半服务器;然后启动server2(假设myid为2),这时两台服务器开始通信,并进入选举流程竞选Leader。

  • 首先每台服务器都会将自己的myid和ZXID作为投票发送给其它服务器,因为是第一轮投票,所以假设ZXID都会0,那么server1的投票为(1,0),server2的投票为(2, 0)
  • 服务器接收其它服务器的投票并检测投票是否有效,包括是否为同一轮投票,以及是否来自于LOOKING服务器
  • 检验通过后,每个服务器都会将自己的投票和收到的投票进行比较。首先比较ZXID,选出最大的并更新为自己新的一轮投票,这里ZXID都是一样的,所以继续比较myid,同样选出最大的作为自己新的一轮投票。因此,这里server1会更新自己的投票为(2,0),而server2则是将自己之前的投票再重新投一次即可。
  • 每一轮投票结束后,服务器都会统计投票信息,看看是否已经有服务器受到过半服务器的支持,若有,就将其作为Leader服务器,并将状态修改为LEADING,其它服务器状态则变为FOLLOWING。这里就是server2成为了Leader,选举结束。

当server3启动时,发现集群中已经存在Leader,则只需作为Follower服务器将入进去即可,不需要重新选举,除非Leader服务器挂掉。

服务运行期间的选举

服务运行期间Leader的选举其实在崩溃恢复那一节已经提及到,这里再详细说说。

  • 当server2服务器崩溃时,剩余服务器因为找不到Leader,就会将自己的状态更新为LOOKING并进入Leader选举流程,这时候服务是不可用的。
  • 存活服务器都会生成投票信息,因为服务已经运行一段时间,所以ZXID可能是不一样的,我们假设server1的投票为(1, 10),server3的投票为(3,11)。后面的流程就会启动时的选举是一样的了,只不过,server1在收到(3,11)投票后,只要比较ZXID即可。所以最终确定server3为新的LEADER。

Leader选举源码分析

从上文我们了解了Leader选举的核心原理,但代码层面是如何实现的呢?这就要通过分析其源码才能明白。
首先我们需要找到一个入口类,即Zookeeper集群启动的主类QuorumPeerMain:

public static void main(String[] args) {
    QuorumPeerMain main = new QuorumPeerMain();
    main.initializeAndRun(args);
}

protected void initializeAndRun(String[] args)
    throws ConfigException, IOException
{
	// 加载配置的类
    QuorumPeerConfig config = new QuorumPeerConfig();
    if (args.length == 1) {
    	// 从配置文件加载配置到内存中
        config.parse(args[0]);
    }

    if (args.length == 1 && config.servers.size() > 0) {
    	// 配置集群
        runFromConfig(config);
    } else {
        // there is only server in the quorum -- run as standalone
        ZooKeeperServerMain.main(args);
    }
}

public void runFromConfig(QuorumPeerConfig config) throws IOException {
  try {
      ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
      cnxnFactory.configure(config.getClientPortAddress(),
                            config.getMaxClientCnxns());

	  // 将配置信息设置到QuorumPeer类
      quorumPeer = new QuorumPeer();
      quorumPeer.setClientPortAddress(config.getClientPortAddress());
      quorumPeer.setTxnFactory(new FileTxnSnapLog(
                  new File(config.getDataLogDir()),
                  new File(config.getDataDir())));
      quorumPeer.setQuorumPeers(config.getServers());
      quorumPeer.setElectionType(config.getElectionAlg());
      quorumPeer.setMyid(config.getServerId());
      quorumPeer.setTickTime(config.getTickTime());
      quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
      quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
      quorumPeer.setInitLimit(config.getInitLimit());
      quorumPeer.setSyncLimit(config.getSyncLimit());
      quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
      quorumPeer.setCnxnFactory(cnxnFactory);
      quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
      quorumPeer.setLearnerType(config.getPeerType());
      quorumPeer.setSyncEnabled(config.getSyncEnabled());
      quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());

      quorumPeer.start(); // 开启子线程加载db信息以及开始选举
      quorumPeer.join(); // 主线程等待子线程完成
  } catch (InterruptedException e) {
      // warn, but generally this is ok
      LOG.warn("Quorum Peer interrupted", e);
  }
}

主要逻辑都在QuorumPeer类中,该类继承自Thread类:

public synchronized void start() {
    loadDataBase(); // 这里是恢复DB信息,如epoch和ZXID等
    cnxnFactory.start();        
    startLeaderElection(); //开始选举的流程
    super.start(); // 启动线程
}

synchronized public void startLeaderElection() {
	try {
		// 投自己一票
		currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
	} catch(IOException e) {
		RuntimeException re = new RuntimeException(e.getMessage());
		re.setStackTrace(e.getStackTrace());
		throw re;
	}
	
	// 创建选举算法,默认是使用FastLeaderElection选举算法
    this.electionAlg = createElectionAlgorithm(electionType);
}

protected Election createElectionAlgorithm(int electionAlgorithm){
    Election le=null;
            
    //TODO: use a factory rather than a switch
    switch (electionAlgorithm) {
    case 0:
        le = new LeaderElection(this);
        break;
    case 1:
        le = new AuthFastLeaderElection(this);
        break;
    case 2:
        le = new AuthFastLeaderElection(this, true);
        break;
    case 3:
        qcm = new QuorumCnxManager(this);
        QuorumCnxManager.Listener listener = qcm.listener;
        if(listener != null){
            listener.start();
            // 默认会进入到这里,可以在zoo.cfg配置文件中配置
            le = new FastLeaderElection(this, qcm);
        } else {
            LOG.error("Null listener when initializing cnx manager");
        }
        break;
    default:
        assert false;
    }
    return le;
}

从上述代码中我们可以看到QuorumPeer创建选举算法的流程,默认是使用的是FastLeaderElection类(早期的版本中有LeaderElection、UDP版本的FastLeaderElection和TCP版本的FastLeaderElection,自3.4.0版本开始只保留了TCP版本的FastLeaderElection,我们也可以自己实现一个选举算法,然后在zoo.cfg配置文件配置即可)。选定选举算法后,调用了线程的start方法,那我们只需要找到run方法即可:

public void run() {
    try {
        while (running) {
        	// 根据服务器状态进入相应的流程,因为是选举流程,所以都是LOOKING状态,其它的流程可以暂时忽略
            switch (getPeerState()) {
            case LOOKING:
                if (Boolean.getBoolean("readonlymode.enabled")) { // 当前服务器为只读服务器,与我们的流程无关
                    final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(
                            logFactory, this,
                            new ZooKeeperServer.BasicDataTreeBuilder(),
                            this.zkDb);
                    Thread roZkMgr = new Thread() {
                        public void run() {
                            try {
                                // lower-bound grace period to 2 secs
                                sleep(Math.max(2000, tickTime));
                                if (ServerState.LOOKING.equals(getPeerState())) {
                                    roZk.startup();
                                }
                            } catch (InterruptedException e) {
                                LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
                            } catch (Exception e) {
                                LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
                            }
                        }
                    };
                    try {
                        roZkMgr.start();
                        setBCVote(null);
                        setCurrentVote(makeLEStrategy().lookForLeader());
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception",e);
                        setPeerState(ServerState.LOOKING);
                    } finally {
                        // If the thread is in the the grace period, interrupt
                        // to come out of waiting.
                        roZkMgr.interrupt();
                        roZk.shutdown();
                    }
                } else {
                    try {
                        setBCVote(null);
                        // 选出leader
                        setCurrentVote(makeLEStrategy().lookForLeader());
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                        setPeerState(ServerState.LOOKING);
                    }
                }
                break;
            }
        }
    } finally {
        LOG.warn("QuorumPeer main thread exited");
        try {
            MBeanRegistry.getInstance().unregisterAll();
        } catch (Exception e) {
            LOG.warn("Failed to unregister with JMX", e);
        }
        jmxQuorumBean = null;
        jmxLocalPeerBean = null;
    }
}

Leader选举的细节主要就在FastLeaderElection.lookForLeader()(makeLEStrategy().lookForLeader())方法中:

// 发送和接收选票队列
LinkedBlockingQueue<ToSend> sendqueue;
LinkedBlockingQueue<Notification> recvqueue;

public Vote lookForLeader() throws InterruptedException {
   try {
   		// 存放收到的投票信息
       HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
		// 存放当前服务器的投票信息
       HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();

       int notTimeout = finalizeWait;

		// 第一次投票都投自己
       synchronized(this){
           logicalclock++;
           updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
       }
		
		// 发送自己的投票信息
       sendNotifications();

		// 循环直到选出Leader
       while ((self.getPeerState() == ServerState.LOOKING) &&
               (!stop)){
           
           // 从队列中按顺序取出投票
           Notification n = recvqueue.poll(notTimeout,
                   TimeUnit.MILLISECONDS);

			
           if(n == null){ // 未收到任何投票信息
               if(manager.haveDelivered()){ // 发送队列空闲情况下,就继续发送自己的投票信息
                   sendNotifications();
               } else { // 发送队列不为null,可能是其它服务器还未启动,尝试重连
                   manager.connectAll();
               }

           }
           else if(self.getVotingView().containsKey(n.sid)) { // 该消息是否属于当前集群
               switch (n.state) { // 判断收到消息的节点的状态
               case LOOKING:
                   // 判断是否是新一轮的选举
                   if (n.electionEpoch > logicalclock) {
                       logicalclock = n.electionEpoch; // 更新logicalclock 
                       recvset.clear(); // 清空前一轮收到的投票信息
                       // 比较epoch、myid和zxid,并更新自己的投票为胜出的节点
                       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) { // 收到的投票信息已经过期,直接忽略
                       break;
                   } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                           proposedLeader, proposedZxid, proposedEpoch)) {
                       // 同一轮投票直接比较myid和zxid
                       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, proposedEpoch))) {

                       // 等待所有notification都被处理完,直到超时
                       while((n = recvqueue.poll(finalizeWait,
                               TimeUnit.MILLISECONDS)) != null){
                           if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                   proposedLeader, proposedZxid, proposedEpoch)){
                               recvqueue.put(n);
                               break;
                           }
                       }

                       // leader已经确定
                       if (n == null) {
                       		// 修改状态为leader或者follower
                           self.setPeerState((proposedLeader == self.getId()) ?
                                   ServerState.LEADING: learningState());

                           Vote endVote = new Vote(proposedLeader,
                                                   proposedZxid,
                                                   logicalclock,
                                                   proposedEpoch);
                           leaveInstance(endVote);
                           return endVote;
                       }
                   }
                   break;
               case OBSERVING: // observer不参与投票
                   LOG.debug("Notification from observer: " + n.sid);
                   break;
               // follower和leader都要参与投票
               case FOLLOWING:
               case LEADING:
                   /*
                    * Consider all notifications from the same epoch
                    * together.
                    */
                   if(n.electionEpoch == logicalclock){
                       recvset.put(n.sid, new Vote(n.leader,
                                                     n.zxid,
                                                     n.electionEpoch,
                                                     n.peerEpoch));
                      
                       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;
                       }
                   }

                   /*
                    * Before joining an established ensemble, verify
                    * a majority is following the same leader.
                    */
                   outofelection.put(n.sid, new Vote(n.version,
                                                       n.leader,
                                                       n.zxid,
                                                       n.electionEpoch,
                                                       n.peerEpoch,
                                                       n.state));
      
                   if(ooePredicate(outofelection, outofelection, n)) {
                       synchronized(this){
                           logicalclock = 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:
                   LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)",
                           n.state, n.sid);
                   break;
               }
           } else {
               LOG.warn("Ignoring notification from non-cluster member " + n.sid);
           }
       }
       return null;
   } finally {
       try {
           if(self.jmxLeaderElectionBean != null){
               MBeanRegistry.getInstance().unregister(
                       self.jmxLeaderElectionBean);
           }
       } catch (Exception e) {
           LOG.warn("Failed to unregister with JMX", e);
       }
       self.jmxLeaderElectionBean = null;
   }
}

至此,Leader选举流程就结束了,但还有个问题,消息是如何广播的?就是sendNotifications方法:

private void sendNotifications() {
    for (QuorumServer server : self.getVotingView().values()) {
        long sid = server.id;

        ToSend notmsg = new ToSend(ToSend.mType.notification,
                proposedLeader,
                proposedZxid,
                logicalclock,
                QuorumPeer.ServerState.LOOKING,
                sid,
                proposedEpoch);
        if(LOG.isDebugEnabled()){
            LOG.debug("Sending Notification: " + proposedLeader + " (n.leader), 0x"  +
                  Long.toHexString(proposedZxid) + " (n.zxid), 0x" + Long.toHexString(logicalclock)  +
                  " (n.round), " + sid + " (recipient), " + self.getId() +
                  " (myid), 0x" + Long.toHexString(proposedEpoch) + " (n.peerEpoch)");
        }
        sendqueue.offer(notmsg);
    }
}

这个方法主要是将封装一个ToSend对象并加入到发送队列中,那这个队列是被谁消费的呢?浏览FastLeaderElection类结构,我们会看到WorkerSender和WorkerReceiver两个类,不用想,一个是接收,一个发送。我们这里是广播消息,那肯定就是WorkerSender类了(这两个类都是继承自Thread类的)。我们直接看run方法:

public void run() {
    while (!stop) {
        try {
            ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
            if(m == null) continue;

            process(m); // 发送ToSend
        } catch (InterruptedException e) {
            break;
        }
    }
}

最终会调用QuorumCnxManager的toSend方法:

public void toSend(Long sid, ByteBuffer b) {
    if (self.getId() == sid) { // 发送给自己不需要走网络通信,直接放到接收队列中即可
         b.position(0);
         addToRecvQueue(new Message(b.duplicate(), sid));
        
    } else {
    	// 发送给其它节点,需要判断之前是否发送过
         if (!queueSendMap.containsKey(sid)) { 
         	// 未发送过则新建发送队列,SEND_CAPACITY为1,表示每次只发送一个消息
             ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(
                     SEND_CAPACITY);
             queueSendMap.put(sid, bq);
             addToSendQueue(bq, b);

         } else {
         	// 前一个消息还未发送完成,则重新发送
             ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
             if(bq != null){
                 addToSendQueue(bq, b);
             } else {
                 LOG.error("No queue for server " + sid);
             }
         }
         connectOne(sid); // 真正的底层传输逻辑
            
    }
}

至此Leader的选举分析就全部完成,总的也就涉及到以下几个类

  • QuorumPeerMain:启动类
  • QuorumPeer:集群环境辅助初始化类
  • FastLeaderElection:选举算法的实现,其中包含了以下几个内部类:Notification:表示收到的投票信息ToSend:表示发送给其它服务器的投票信息Messager:包含了WorkerSender发送类和WorkerReceiver接受类,通过这两个类去发送和接收投票信息

 

补充:

zxid:

  事务id, 为了保证事务的顺序一致性,zookeeper 采用了递增的事 务 id 号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了 zxid。实现中 zxid 是一个 64 位的 数字,它高32位是epoch(ZAB协议通过epoch编号来 区分 Leader 周期变化的策略)用来标识 leader 关系是否 改变,每次一个 leader 被选出来,它都会有一个新的 epoch=(原来的epoch+1),标识当前属于那个leader的 统治时期。低32位用于递增计数
  epoch的变化大家可以做一个简单的实验
  • 启动一个zookeeper集群。
  • 在/tmp/zookeeper/VERSION-2 路径下会看到一个 currentEpoch文件。文件中显示的是当前的epoch。
  • 把 leader 节点停机,这个时候在看 currentEpoch 会有 变化。 随着每次选举新的leader,epoch都会发生变化。
posted @ 2019-12-05 21:53  guoyu1  阅读(487)  评论(0编辑  收藏  举报