zookeeper快速领导者选举源码分析

261 阅读11分钟

##zookeeper选举的基本原理

zookeeper只有在集群模式下才会进行领导者选举。

Zookeeper选举过程中遇到的四个基本概念;

个人能力:Zookeeper是一个数据库,集群中节点的数据越新就代表此节点能力越强,而在Zookeeper中可以通事务id(zxid)来表示数据的新旧,一个节点最新的zxid越大则该节点的数据越新。所以Zookeeper选举时会根据zxid的大小来作为投票的基本规则。

改票:Zookeeper集群中的某一个节点在开始进行选举时,首先认为自己的数据是最新的,会先投自己一票,并且把这张选票发送给其他服务器,这张选票里包含了两个重要信息:zxid和sid,sid表示这张选票投的服务器id,zxid表示这张选票投的服务器上最大的事务id,同时也会接收到其他服务器的选票,接收到其他服务器的选票后,可以根据选票信息中的zxid来与自己当前所投的服务器上的最大zxid来进行比较,如果其他服务器的选票中的zxid较大,则表示自己当前所投的机器数据没有接收到的选票所投的服务器上的数据新,所以本节点需要改票,改成投给和刚刚接收到的选票一样。

投票箱:Zookeeper集群中会有很多节点,Zookeeper集群并不会单独去维护一个投票箱应用,而是在每个节点内存里利用一个数组来作为投票箱。每个节点里都有一个投票箱,节点会将自己的选票以及从其他服务器接收到的选票放在这个投票箱中。因为集群节点是相互交互的,并且选票的PK规则是一致的,所以每个节点里的这个投票箱所存储的选票都会是一样的,这样也可以达到公用一个投票箱的目的。

领导者:Zookeeper集群中的每个节点,开始进行领导选举后,会不断的接收其他节点的选票,然后进行选票PK,将自己的选票修改为投给数据最新的节点,这样就保证了,每个节点自己的选票代表的都是自己暂时所认为的数据最新的节点,再因为其他服务器的选票都会存储在投票箱内,所以可以根据投票箱里去统计是否有超过一半的选票和自己选择的是同一个节点,都认为这个节点的数据最新,一旦整个集群里超过一半的节点都认为某一个节点上的数据最新,则该节点就是领导者。

下面我结合源码来给大家详细的分析一下Zookeeper的快速领导者选举原理。

QuoruPeer表示集群模式中的一个zkServer,QuorumPeer类定义如下:

public class QuorumPeer extends ZooKeeperThread implements QuorumStats.Provider

定义表明QuorumPeer是一个ZooKeeperThread,表示是一个线程。在节点启动时,会调用start()方法,代码如下:

public synchronized void start() {
   //判断当前服务器是否是集群中成员
    if (!getView().containsKey(myid)) {
       throw new RuntimeException("My id " + myid + " not in the peer list");
    }
    
    //加载数据,从日志文件中恢复数据到内存数据库对象ZkDatabase的dataTree中
    loadDataBase();   (1)
   
   //开启四类(accepted、selector、worker、connection)处理客户端请求线程(开启NIO服务端线程去接收客户端连接请求,处理IO事件等),
   要注意的是,线程开启后就可以接收客户端请求,但是由于zkserver还没有初始化,会把连接请求拒绝掉,直到初始化完成才能正常接收处理请求。
     startServerCnxnFactory();   (2)
     
     //进行领导者选举,确定服务器角色,根据服务器角色进行初始化
     startLeaderElection();   (3)
     startJvmPauseMonitor();
     
     //启动本类的run方法,进行领导者选举,zkserver初始化
     super.start();     (4)
    }

(2):初始化NIO相关线程

Zookeeper作为一个服务器,自然要与客户端进行网络通信,如何高效的与客户端进行通信,让网络IO不成为ZooKeeper 的瓶颈是ZooKeeper急需解决的问题,ZooKeeper中使用ServerCnxnFactory管理与客户端的连接,其有两个实现,一个是 NIOServerCnxnFactory,使用Java原生NIO实现;一个是NettyServerCnxnFactory,使用netty实现;使用ServerCnxn 代表一个客户端与服务端的连接.节点启动时,会调用其start()方法完成NIO相关线程的启动,NIOServerCnxnFactory类定义如下:

public class NIOServerCnxnFactory extends ServerCnxnFactory

@Override
    public void start() {
        stopped = false;
        if (workerPool == null) {
            /**
             * WorkerService主要用来管理worker Thread线程(进行基本的IO读写操作)
             *
             * arg1:指定线程名前缀
             * arg2:worker thread 线程数
             * arg3:线程管理模式:默认是不可指定线程模式,即一个ExecutorService中多个线程,一个任务由任意一个线程执行,任务无序
             *       指定线程模式,多个ExecutorService对应一个线程,处理有序任务
             */
            workerPool = new WorkerService("NIOWorker", numWorkerThreads, false);
        }
        /**
         * 开启所有的selector thread线程(读取IO事件交由worker thread线程处理,注册accepted thread线程新派发的连接到selector,更新连接监听事件)
         */
        for (SelectorThread thread : selectorThreads) {
            if (thread.getState() == Thread.State.NEW) {
                thread.start();
            }
        }
        /**
         * 开启accepted thread线程
         */
        // ensure thread is started once and only once
        if (acceptThread.getState() == Thread.State.NEW) {
            acceptThread.start();
        }
        /**
         * 开启expirer Thread 若连接上的session已过期,则关闭该连接
         */
        if (expirerThread.getState() == Thread.State.NEW) {
            expirerThread.start();
        }
    }

实例化WorkerService时会调用其start()方法:

public void start() {
        if (numWorkerThreads > 0) {
            if (threadsAreAssignable) {
                for (int i = 1; i <= numWorkerThreads; ++i) {
                    workers.add(Executors.newFixedThreadPool(1, new DaemonThreadFactory(threadNamePrefix, i)));
                }
            } else {
                workers.add(Executors.newFixedThreadPool(numWorkerThreads, new DaemonThreadFactory(threadNamePrefix)));
            }
        }
        stopped = false;
    }

SelectThread类定义:

class SelectorThread extends AbstractSelectThread {
   private final int id;
   //存储accept thread线程接收到客户端连接
   private final Queue<SocketChannel> acceptedQueue;
   //存储需要更新监听事件的连接(IO事件)
   private final Queue<SelectionKey> updateQueue;

由定义可知SelectThread是线程类,执行逻辑run()方法如下:

public void run() {
	try{
                while (!stopped) {
                    try {
                        /**
                         *读取IO事件交由worker thread线程处理
                         */
                        select();
                        /**
                         *处理accept线程新分派的连接,
                         *  (1)将新连接注册到selector上;
                         *  (2)包装为NIOServerCnxn后注册到NIOServerCnxnFactory中,使用ipMap来限制每个ip的连接数
                         */
                        processAcceptedConnections();
                        /**
                         * 更新updateQueue中连接的监听事件
                         */
                        processInterestOpsUpdateRequests();
                    } catch (RuntimeException e) {
                        LOG.warn("Ignoring unexpected runtime exception", e);
                    } catch (Exception e) {
                        LOG.warn("Ignoring unexpected exception", e);
                    }
                }

AcceptThread类定义:

private class AcceptThread extends AbstractSelectThread

由定义可知AcceptThread是线程类,执行逻辑run()方法如下:

public void run() {
            try {
                while (!stopped && !acceptSocket.socket().isClosed()) {
                    try {
                        /**
                         * 监听Accepted事件,调用doAccept()方法,将客户端连接轮询添加到selector thread线程的acceptedQueue中
                         */
                        select();
                    } catch (RuntimeException e) {
                        LOG.warn("Ignoring unexpected runtime exception", e);
                    } catch (Exception e) {
                        LOG.warn("Ignoring unexpected exception", e);
                    }
                }
            }

(3):完成领导者选举应用层传输层初始化

领导者选举策略:

 protected Election createElectionAlgorithm(int electionAlgorithm) {
      Election le = null;

      //TODO: use a factory rather than a switch
      switch (electionAlgorithm) {
      case 1:
          throw new UnsupportedOperationException("Election Algorithm 1 is not supported.");
      case 2:
          throw new UnsupportedOperationException("Election Algorithm 2 is not supported.");
      case 3:
          /**
          初始化QuorumCnxManager
          **/
          QuorumCnxManager qcm = createCnxnManager();
          QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);
          if (oldQcm != null) {
              LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");
              oldQcm.halt();
          }
          /**
          初始化QuorumCnxManager.Listener
          **/
          QuorumCnxManager.Listener listener = qcm.listener;
          if (listener != null) {
          	/**
              运行QuorumCnxManager.Listener
              **/
              listener.start();
              FastLeaderElection fle = new FastLeaderElection(this, qcm);
              fle.start();
              le = fle;
          } else {
              LOG.error("Null listener when initializing cnx manager");
          }
          break;
      default:
          assert false;
      }
      return le;
  }

领导者选举在Zookeeper中有三种实现,默认是快速领导者选举FastLeaderElection,其余两种已经废弃,下面着重介绍FastLeaderElection。

Zookeeper快速领导者选举架构图如下:

从架构图我们可以发现,快速领导者选举实现架构分为两层:应用层和传输层。所以初始化就是初始化传输层和应用层。

传输层初始化

初始化步骤:

1.初始化QuorumCnxManager

2.初始化QuorumCnxManager.Listener

3.运行QuorumCnxManager.Listener

4.运行QuorumCnxManager

5.返回FastLeaderElection对象

QuorumCnxManager介绍:

QuorumCnxManager就是传输层实现,QuorumCnxManager中几个重要的属性:

//传输层的每个zkServer需要发送选票信息给其他服务器,这些选票信息来自应用层,在传输层中将会按服务器id分组保存在queueSendMap中。

  • ConcurrentHashMap<Long, ArrayBlockingQueue> queueSendMap

//SendWorker就是封装了Socket的发送器,而senderWorkerMap就是用来记录其他服务器id以及对应的SendWorker的。

  • ConcurrentHashMap<Long, SendWorker> senderWorkerMap

//传输层的每个zkServer将接收其他服务器发送的选票信息,这些选票会保存在recvQueue中,以提供给应用层使用。

  • ArrayBlockingQueue recvQueue

//QuorumCnxManager.Listener负责开启socket监听。

  • QuorumCnxManager.Listener

应用层初始化

FastLeaderElection类是快速领导者选举实现的核心类,这个类有三个重要的属性:

LinkedBlockingQueue sendqueue;

LinkedBlockingQueue recvqueue;

Messenger messenger;

  • Messenger.WorkerSender

  • Messenger.WorkerReceiver

服务器在进行领导者选举时,在发送选票时也会同时接受其他服务器的选票,FastLeaderElection类也提供了和传输层类似的实现,将待发送的选票放在sendqueue中,由Messenger.WorkerSender发送到传输层queueSendMap中。

同样,由Messenger.WorkerReceiver负责从传输层获取数据并放入recvqueue中。

这样在应用层了,只需要将待发送的选票信息添加到sendqueue中即可完成选票信息发送,或者从recvqueue中获取元素即可得到选票信息。

(4):快速领导者选举实现(主循环)

            /*
             * Main loop
             */
            while (running) {
                switch (getPeerState()) {
                //服务器刚启动时都是Looking状态
                case LOOKING:
                  ......
                            /**
                            核心方法
                            
                            makeLEStrategy():返回的是快速领导者选举策略就是前面创建的FastLeaderElection
                            lookForLeader():真正领导者选举的逻辑,返回选举出的领导者
                            setCurrentVote(makeLEStrategy().lookForLeader()):将选举出来的领导者保存在当前服务器
                            **/
                            setCurrentVote(makeLEStrategy().lookForLeader())
                    break;
                case OBSERVING:
                    try {
                    	.....//初始化为观察者
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                    } finally {
                        observer.shutdown();
                        setObserver(null);
                        updateServerState();
                        if (isRunning()) {
                            Observer.waitForObserverElectionDelay();
                        }
                    }
                    break;
                case FOLLOWING:
                    try {
                        .....//初始化为跟随者
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                    } finally {
                        follower.shutdown();
                        setFollower(null);
                        updateServerState();
                    }
                    break;
                case LEADING:
                    LOG.info("LEADING");
                    try {
                       .....//初始化为领导者
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                    } finally {
                        if (leader != null) {
                            leader.shutdown("Forcing shutdown");
                            setLeader(null);
                        }
                        updateServerState();
                    }
                    break;
                }
            }

领导者选举的核心方法lookForLeader():

public Vote lookForLeader() throws InterruptedException {
        	//初始化一个投票箱
            Map<Long, Vote> recvset = new HashMap<Long, Vote>();
   			.....
            synchronized (this) {
                logicalclock.incrementAndGet();
                //更新提议,首先把票投给自己
                updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
            }
	   //发送选票
            sendNotifications();

            while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {
                /*
 		获取其他服务发送过来的选票
                 */
                Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);

                /*
                 * 服务器刚启动时,如果获取到的选票为空,表示还未建立连接
                 */
                if (n == null) {
                    if (manager.haveDelivered()) {
                        sendNotifications();
                    } else {
                    	//与其他所有领导者选举参与者建立连接,前面在进行传输层初始化
                        时,只是开启监听,并未发起连接,由于每个服务器在启动时都会向其
                        他发起连接请求,如果不加以限制就会建立两个socket连接,浪费系统
                        资源,因为socket是双向的,zookeeper限制只有服务器id大的才可以
                        向服务器id小的发起连接请求
                        manager.connectAll();
                    }
                   //n.state:发送该选票的服务器的状态
                    switch (n.state) {
                    case LOOKING:
          		/**
                            electionEpoch:表示第几届选举,如果发送选票服务器届号大,表示当前服务器落后,修改本地届号并清空投票箱的投票
                        **/
                        if (n.electionEpoch > logicalclock.get()) {
                            logicalclock.set(n.electionEpoch);
                            recvset.clear();
                            /**
                               选票PK,Epoch(届)、zxid(事物id)、myid(服务器id)依次比较,大的表示能力强(数据越新)
                            **/
                            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()) {
                         //如果接收的选票届号小于当前服务器的届号,拒绝选票
                        } 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));

                        //统计投票箱中的选票与当前服务器投的选票相同且超过半数即所投的服务器就是leader
                        if (voteSet.hasAllQuorums()) {

                            //上面选举出来的leader可以看成是准leader,如果此时获取到的选票比准leader数据更新,则跳出循环继续进行领导者选举
                            while ((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {
                                if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
                                    recvqueue.put(n);
                                    break;
                                }
                            }

                           //如果获取的选票数据没有准leader新,则不断获取选票,直到没有获取到选票,则选出最终的leader
                            if (n == null) {
                            	//设置服务器的状态
                                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:
                        /*
                         * 如果发送选票的服务器状态是following和leading,则表示领导者已经选举出来,
                            会将leader发送给当前服务器,当前服务器获取到选票后更新服务器状态即可
                         * .
                         */
                        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;
                            }
                        }

                        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:
                        break;
                    }
                } else {
                    
                    }
                }
            }
            return null;
        } finally {
            
        }
    }

领导者选举完成后

ZooKeeper集群在进行领导者选举的过程中不能对外提供服务

根据lookForLeader()我们可以发现,只有当集群中服务器的角色确定了之后,while才会进行下一次循环,当进入下一次循环后,就会根据服务器的角色进入到对应的初始化逻辑,初始化完成之后才能对外提供服务。