zookeeper Leader选举源码分析

文章目录

  • zookeeper Leader选举源码分析
    • 一、包含的知识点
    • 二、zookeeper选举的入口
      • 2.1 zookeeper服务启动入口main
      • 2.2 触发选举时机
    • 三、initializeAndRun 初始化并启动服务
    • 四、runFromConfig 按照配置文件启动服务
    • 五、QuorumPeer启动线程
    • 六、startLeaderElection 进行Leader选举参数配置
      • 6.1 QuorumPeerMain服务启动主要流程
      • 6.2 startLeaderElection代码具体分析
      • 6.3 创建Leader选举算法
    • 七、QuorumPeer线程run方法进行选举
    • 八、lookForLeader 进行Leader选举
      • 8.1 LOKKING 状态的节点进行leader选举
      • 8.2 termPredicate 半数校验选举是否成功
    • 九、投票选举通信流程
      • 9.1 ToSend、Notification 比较
      • 9.2 Listener 投票信息的发送、接收
      • 9.3 投票信息发送 Notification
      • 9.4 消息发送WorkerSender
      • 9.5 消息发送QuorumCnxManager#toSend
      • 9.6 开启连接 startConnection
      • 9.7 消息发送SendWorker
      • 9.8 连接请求接收 receiveConnection
      • 9.9 连接请求处理 handleConnection
    • 十、Leader选举后逻辑处理
      • 10.1 leader选举后的逻辑处理
      • 10.2 follower和leader进行连接

zookeeper Leader选举源码分析

一、包含的知识点

二、zookeeper选举的入口

2.1 zookeeper服务启动入口main

​ 要了解zookeeper选举流程, 首先需要了解程序的入口, 那怎么知道程序的入口呢? 使用过zookeeper服务知道启动命令 zkServer.sh、zkServer.cmd, 查看zkServer.sh shell脚本, 对ZOOMAIN处理都包含QuorumPeerMain类的全路径声明, 如:

ZOOMAIN="org.apache.zookeeper.server.quorum.QuorumPeerMain"

​ 显然QuorumPeerMain类是服务启动入口, 通过Idea查询QuorumPeerMain类, main函数包含下面的内容

// 根据指定的配置文件, 启动Server服务
public static void main(String[] args) {
        QuorumPeerMain main = new QuorumPeerMain();
        try {
            main.initializeAndRun(args);
        } catch(Exception e) {
            //省略部分异常处理代码
        }
        LOG.info("Exiting normally");
        System.exit(0);
    }

2.2 触发选举时机

​ Leader在zookeeper集群环境中只存在一个, 那什么时候会触发Leader选举呢 ? 主要包含下面场景

  • 服务集群启动时需要选举Leader
  • Leader服务不可用时需要重新选举

三、initializeAndRun 初始化并启动服务

​ 从2.1节代码可以知道, main方法内部调用了initializeAndRun方法进行服务启动, 下面是具体代码逻辑

//QuoRumPeerMain
protected void initializeAndRun(String[] args)
        throws ConfigException, IOException{
  QuorumPeerConfig config = new QuorumPeerConfig();
  if (args.length == 1) {
    //1. 解析配置文件, 获得配置参数
    config.parse(args[0]);
  }

  // Start and schedule the the purge task
  //2. 创建并启动线程, 用于清理日志文件
  DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
                                                             .getDataDir(), config.getDataLogDir(), config
                                                             .getSnapRetainCount(), config.getPurgeInterval());
  purgeMgr.start();
	
  //3. 启动服务
  if (args.length == 1 && config.servers.size() > 0) {
    //3.1 如果存在配置文件, 按照配置文件启动服务
    runFromConfig(config);
  } else {
    LOG.warn("Either no config or no quorum defined in config, running "
             + " in standalone mode");
    // there is only server in the quorum -- run as standalone
    //3.2 如果没有给定配置文件, 按照单例模式启动, 即args=null
    ZooKeeperServerMain.main(args);
  }
}

​ 服务启动如果存在配置文件, 会对配置文件进行解析, args[0] 其实就是配置文件**zoo.cfg**路径信息, 比如下面zookeeper启动命令

# zkServer.sh ../conf/zoo.cfg

​ initializeAndRun方法的主要逻辑是

  • 创建配置对象QuorumPeerConfig解析配置文件 (配置解析流程比较简单, 这里不分析, 使用到的地方会提一下)

  • 创建对象DatadirCleanupManager用于定期清理日志信息

    • 从config中获取必要的参数: dataDir(数据目录)、dataLogDir(日志目录)、snapRetainCount(快照保留数量)、purgeInterval(清理间隔)

    • 参数对应zoo.cfg配置信息如下

      dataDir=../data
      dataLogDir=../log
      snapRetainCount=5
      purgeInterval=3600
      
  • 服务启动

    • 如果存在zoo.cfg配置参数信息,按照配置文件进行服务启动
    • 如果没有配置zoo.cfg, 按照单例的方式启动服务

四、runFromConfig 按照配置文件启动服务

​ 生产环境肯定使用zookeeper集群方式启动服务, 也就是肯定存在配置文件信息, 因此这里只分析runFromConfig启动服务进行选举流程, ZooKeeperServerMain.main(args)按照单例的方式启动服务暂时不分析。

//QuoRumPeerMain
public void runFromConfig(QuorumPeerConfig config) throws IOException {
      try {
        	//1. 注册log4j JMX mbeans信息, 可以通过设置zookeeper.jmx.log4j.disable=true来禁用
          ManagedUtil.registerLog4jMBeans();
      } catch (JMException e) {
          LOG.warn("Unable to register log4j JMX control", e);
      }
  
      LOG.info("Starting quorum peer");
      try {
        	//2. 创建ServerCnxnFactory用于服务连接
          ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
          cnxnFactory.configure(config.getClientPortAddress(),
                                config.getMaxClientCnxns());
					//3. 创建线程 QuorumPeer, 并根据config进行属性信息设置
          quorumPeer = getQuorumPeer();
        	//... 省略从config获取配置参数, 设置quorumPeer属性的代码
          
          quorumPeer.initialize();
					//4. 启动线程 quorumPeer
          quorumPeer.start();
          //5. join住线程quorumPeer, 让主线程等待子线程执行完
          quorumPeer.join();
      } catch (InterruptedException e) {
          // warn, but generally this is ok
          LOG.warn("Quorum Peer interrupted", e);
      }
    }

​ 从代码逻辑可以看出runFromConfig方法主要是创建QuorumPeer线程, 并进行启动。方法的主要流程如下:

  • 注册log4j JMX mbeans信息, 用于日志输出, 可以通过设置**zookeeper.jmx.log4j.disable=true**来禁用

  • 创建ServerCnxnFactory对象用于服务连接, 这里主要是 NIOServerCnxnFactory

  • 创建线程 QuorumPeer, 并根据config进行属性信息设置

    • config信息解析在第三节中提到
    config.parse(args[0])
    
  • 调用start方法进行服务启动, 并执行join方法保证子线程能够正常执行结束

五、QuorumPeer启动线程

​ QuorumPeer其实是一个线程, 它内部继承了Thread, 下面是类继承关系图, 因此QuorumPeer类启动其实就是线程的启动, 只是包含了部分其它逻辑

zookeeper Leader选举源码分析_第1张图片

​ 下面是QuorumPeer启动入口

//QuorumPeer
public synchronized void start() {
  //1. 加载数据, 进行快照信息回复
  loadDataBase();
  //2. 启动服务连接线程, 其实就是NIOServerCnxnFactory内线程启动
  cnxnFactory.start();
  //3. 进行Leader选举
  startLeaderElection();
  //4. 调用父类进行线程启动
  super.start();
}

​ 从start方法看出在进行服务启动前会进行必要信息处理, 之后会调用父类start方法进行线程启动, 主要流程是

  • 加载数据进行快照信息恢复, 这里通过 FileTxnSnapLog 进行数据加载, 主要加载dataDir、dataLogDir目录中信息, 这两个目录是在下面代码处配置

    //QuoRumPeerMain#runFromConfig方法
    quorumPeer.setTxnFactory(new FileTxnSnapLog(
                      new File(config.getDataLogDir()),
                      new File(config.getDataDir())));
    
  • 通过 NIOServerCnxnFactory 启动服务连接线程

    • 在第四节中创建了 cnxnFactory 对象, 并创建了 ZookeeperThread线程对象

      //QuoRumPeerMain#runFromConfig
      ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
                cnxnFactory.configure(config.getClientPortAddress(),
                                      config.getMaxClientCnxns());
      
      //NioServerCnxnFactory#configure
      public void configure(InetSocketAddress addr, int maxcc) throws IOException {
              configureSaslLogin();
      
              thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr);
              thread.setDaemon(true);
              maxClientCnxns = maxcc;
              this.ss = ServerSocketChannel.open();
              ss.socket().setReuseAddress(true);
              LOG.info("binding to port " + addr);
              ss.socket().bind(addr);
              ss.configureBlocking(false);
              ss.register(selector, SelectionKey.OP_ACCEPT);
      }
      
    • 进行服务启动, 实际就是ZookeeperThread线程的启动

      public void start() {
        // 线程是NEW状态, 还没有启动
        if (thread.getState() == Thread.State.NEW) {
          thread.start();
        }
      }
      
    • 覆写run方法, 采用多路复用技术, 根据SelectKey来分别处理OP_ACCEPT、OP_READ、OP_WRITE

  • 执行节点Leader选举(细节后面分析)

  • 调用父类进行线程启动, 即调用Thread的start方法

六、startLeaderElection 进行Leader选举参数配置

6.1 QuorumPeerMain服务启动主要流程

​ 在进入startLeaderElection方法之前, QuorumPeerMain为服务启动做了相应处理, 简要概括如下:

  • parse(args[0]), 解析配置文件zoo.cfg中相关配置信息
    • 基于解析后的配置config, 创建启动线程类QuorumPeer
    • 创建server服务之间连接工厂 ServerCnxnFactory, 默认是NIOServerCnxnFactory
  • 加载文件(dataDir、dataLogDir)进行数据恢复
  • 启动用于服务连接的线程, 实际是ServerCnxnFactory内部包含的ZookeeperThread
  • startLeaderElection 进行Leader选举
  • 调用super启动QuorumPeer线程

6.2 startLeaderElection代码具体分析

​ 现在我们具体分析 startLeaderElection 方法, 首先看下代码

//QuorumPeer
synchronized public void startLeaderElection() {
  try {
    //1. 创建投票, 票据Vote包含myid, zxid, epoch三个主要信息
    currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
  } catch(IOException e) {
    RuntimeException re = new RuntimeException(e.getMessage());
    re.setStackTrace(e.getStackTrace());
    throw re;
  }
  //2. 从所有view中找到当前节点信息, 设置myQuorumAddr信息
  for (QuorumServer p : getView().values()) {
    if (p.id == myid) {
      myQuorumAddr = p.addr;
      break;
    }
  }
  if (myQuorumAddr == null) {
    throw new RuntimeException("My id " + myid + " not in the peer list");
  }
  //3. 如果选举方式为0, 设置相关信息
  if (electionType == 0) {
    try {
      udpSocket = new DatagramSocket(myQuorumAddr.getPort());
      responder = new ResponderThread();
      responder.start();
    } catch (SocketException e) {
      throw new RuntimeException(e);
    }
  }
  //设置选举类型, 默认是3
  this.electionAlg = createElectionAlgorithm(electionType);
}

​ 从代码执行流程可以看出, 虽然方法名称叫startLeaderElection, 实际上是进行Leader选举前相关信息创建,具体包含下面内容

  • 创建选举使用的投票信息Vote, 主要包含: myid、zxid、epoch 三个主要信息

  • 查找当前服务节点 myQuorumAddr 信息, 通过比较QuorumServer.id 、myid来识别是否属于同一节点

    • getView信息来源于解析zoo.cfg时. 会解析下面内容获取QuorumServer信息

      server.1=IP1:2888:3888 
      server.2=IP2.2888:3888
      server.3=IP3.2888:3888
        
      // 集群节点配置如下
      server.A=B:C:D
      A 数字, 表示服务器编号, 和myid文件内容相对应
      B 服务器节点IP
      C 当前服务器节点和Leader服务器进行信息交换的端口
      D 选举时, 服务器相互通信的端口
      
    • myid信息来源于, data目录下myid文件配置的内容

  • 创建选举算法, 默认情况下electionType=3

    //QuorumPeer
    protected Election createElectionAlgorithm(int electionAlgorithm){
      Election le=null;
      switch (electionAlgorithm) {
        //... 省略部分代码
        case 3:
          //1. 创建QuorumCnxManager, 以TCP的方式为每一对Server维护一个连接connection
          qcm = createCnxnManager();
          QuorumCnxManager.Listener listener = qcm.listener;
          if(listener != null){
            //2. 启动listener, 用于处理连接(connection)请求
            listener.start();
            //3. 创建Leader选举使用的算法, 这里是FastLeaderElection
            le = new FastLeaderElection(this, qcm);
          } else {
            LOG.error("Null listener when initializing cnx manager");
          }
          break;
        default:
          assert false;
      }
      return le;
    }
    

6.3 创建Leader选举算法

​ Leader选举存在多种算法, 主要包括LeaderElectionAuthFastLeaderElectionAuthFastLeaderElectionFastLeaderElection, 默认情况下electionType=3(FastLeaderElection), 其实通过源码也可以看出前三种方式都添加了 @Deprecated 注解, 表示这种选举方式已经废弃。这里只讲解 electionType=3创建选举方法。分析上面的代码其主要逻辑如下

  • 创建连接管理器 createCnxnManager() , 为每对服务器之间维护一个连接, 用于接收消息进行投票
  • 启动listener线程用于监听投票请求, Listener是ZookeeperThread子类, 那listenre也是线程, 启动后会不断监听服务请求
  • 创建投票使用的算法FastLeaderElection

​ 执行createElectionAlgorithm方法后, 选择使用FastLeaderElection算法进行投票选举, 这个算法主要做了什么呢?

//FastLeaderElection
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
  this.stop = false;
  this.manager = manager;
  starter(self, manager);
}

private void starter(QuorumPeer self, QuorumCnxManager manager) {
  			//1. 设置成员属性信息
        this.self = self;
        proposedLeader = -1;
        proposedZxid = -1;
				//2. 实例化 发送、接受 两个阻塞队列, 用于消息的存储
        sendqueue = new LinkedBlockingQueue<ToSend>();
        recvqueue = new LinkedBlockingQueue<Notification>();
			  //3. 创建Message对象, 内部创建了两个线程用于接收、发送消息 
        this.messenger = new Messenger(manager);
}

//FastLeaderElection.Messenger
Messenger(QuorumCnxManager manager) {
  this.ws = new WorkerSender(manager);
  Thread t = new Thread(this.ws,
                        "WorkerSender[myid=" + self.getId() + "]");
  t.setDaemon(true);
  t.start();

  this.wr = new WorkerReceiver(manager);
  t = new Thread(this.wr,
                 "WorkerReceiver[myid=" + self.getId() + "]");
  t.setDaemon(true);
  t.start();
}

​ 创建FastLeaderElection对象时, 主要做了下面的事情

1) 会创建两个阻塞队列, 分别存储ToSend、Notification信息

2) 创建Messager对象, Messager内部维护了两个ZookeeperThread子类, 分别是WorkerSend、WorkerReceiver, 创建Messager对象时, 实际创建的就是WorkerSend、WorkerReceiver两个线程, 分别用于消息的发送和消息的接收

七、QuorumPeer线程run方法进行选举

​ zookeeper集群节点存在四种状态,在进行leader选举时会改变节点状态为 LOOKING, 选举结束后会改变节点状态为 LEADINGFOLLOWING

  • LOOKING
    • 选举状态
  • LEADING
    • 集群leader角色, 选举后如果成为leader角色, 会改变节点状态为LEADING
  • FOLLOWING
    • 集群follower角色, 选举后如果成为follower角色, 会改变节点状态为FOLLOWING
  • OBSERVING
    • 观察者角色, 不参与选举

​ 从上面内容可以知道,选举过程中参与选举的节点都是 LOOKING 状态, 针对线程执行方法run, 为了节省篇幅这里先只分析 LOOKING 对应的内容

//QuorumPeer
@Override
public void run() {
  //... 省略部分代码
  try {
    /*
			 Main loop
     */
    while (running) {
      //1. 获取节点状态, 初始状态为默认值 LOOKING
      switch (getPeerState()) {
        case LOOKING:
          LOG.info("LOOKING");
					//2. 判断节点是否以只读模式启动, 通过 readonlymode.enabled 配置
          if (Boolean.getBoolean("readonlymode.enabled")) {
            LOG.info("Attempting to start ReadOnlyZooKeeperServer");
						//... 省略部分其它代码
          } else {
            try {
              //向后兼容, 设置bcVote 
              //TODO 分析的代码是3.5.x, 这个向后兼容意思不太了解 ??
              setBCVote(null);
              //3. 根据选举算法进行Leader选举
              setCurrentVote(makeLEStrategy().lookForLeader());
            } catch (Exception e) {
              LOG.warn("Unexpected exception", e);
              setPeerState(ServerState.LOOKING);
            }
          }
          break;

          //... 省略部分其它代码
      }
    }
  } finally {
    //4. 主线程退出, 将run方法开始处设置的信息恢复
    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;
  }
}

​ 为了节约篇幅, run方法剔除了部分暂时不分析的代码, 核心的代码在Case条件为 LOOKING中, 具体执行流程如下

  • 首先通过 getPeerState() 获取节点的状态, 初始的时候节点的状态为默认值 LOOKING

    //QuorumPeer
    private ServerState state = ServerState.LOOKING;
    
  • 进入 LOOKING 条件后, 会根据 readonlymode.enabled 判断Server是否以只读模式启动, 什么是只读模式 ?

    • 当服务器和集群中过半节点失去连接后, 这个节点不再提供事务处理请求(follower角色会将请求转发给Leader),但是仍然能够支持读请求
  • 查看代码, 只读启动模式和非只读模式区别是: 只读模式创建了线程 roZkMgr , 用于程序等待若干时间, 选举相关的代码都是一样的

    • 核心代码如下

      setBCVote(null);
      setCurrentVote(makeLEStrategy().lookForLeader());
      
  • 保证向后兼容特性, 设置bcVote = null, 这样做的目的, 目前不知道 ??

  • 根据设置的选举策略, 进行Leader选举 makeLEStrategy().lookForLeader()

    • 在6.2节执行 startLeaderElection 方法时,会根据 electionType 创建对应的选举方法,默认electionType=3, 创建的选举方式为FastLeaderElection
    • lookForLeader()方法的执行,实际是 FastLeaderElection#lookForLeader 方法的执行

八、lookForLeader 进行Leader选举

8.1 LOKKING 状态的节点进行leader选举

​ 无论什么时候QuorumPeer状态变更为 LOOKING , 都会开始新一轮的投票选举, 选举过程中会调用lookForLeader()方法, 它会发送Notification通知到所有其它服务器节点。lookForLeader()具体实现逻辑如下

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){
              //1. AtomicLong 更新逻辑时钟, 用于校验是否在同一轮选举周期
                logicalclock.incrementAndGet();
              //2. 更新票据信息,实际是将QuorumPeer中myid、zxid、epoch更新到FastLeaderElection中
                updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
            }

            LOG.info("New election. My id =  " + self.getId() +
                    ", proposed zxid=0x" + Long.toHexString(proposedZxid));
          //3. 创建ToSend信息, 然后存入阻塞队列 sendqueue 中, 异步发送选举信息
            sendNotifications();

            /*
             * Loop in which we exchange notifications until we find a leader
             */
						//4. 不断循环方式, 服务节点没有停止 且 服务状态为 LOOKING 时 不断处理
            while ((self.getPeerState() == ServerState.LOOKING) &&
                    (!stop)){
                /*
                 * Remove next notification from queue, times out after 2 times
                 * the termination time
                 */
              //5. 从阻塞队列 recvqueue 中获取其它服务节点发送的 Notification 信息
                Notification n = recvqueue.poll(notTimeout,
                        TimeUnit.MILLISECONDS);

                /*
                 * Sends more notifications if haven't received enough.
                 * Otherwise processes new notification.
                 */
              //6. 如果阻塞队列 recvqueue 中没有收到其它节点的 Notification 信息, 执行这部分逻辑
                if(n == null){
                  	//6.1 如果 所有的消息都已经发送, 那么再次发送自己的投票
                    if(manager.haveDelivered()){
                        sendNotifications();
                    //6.2 如果存在消息没有发送, 向所有其它节点发起连接操作
                    } else {
                        manager.connectAll();
                    }

                    /*
                     * Exponential backoff
                     */
                    int tmpTimeOut = notTimeout*2;
                    notTimeout = (tmpTimeOut < maxNotificationInterval?
                            tmpTimeOut : maxNotificationInterval);
                    LOG.info("Notification time out: " + notTimeout);
                }
              //7. 如果接收到其它服务器的投票, 校验服务器节点是否有效, 即sid是否在配置的myid范围内
                else if(validVoter(n.sid) && validVoter(n.leader)) {
                    /*
                     * Only proceed if the vote comes from a replica in the
                     * voting view for a replica in the voting view.
                     */
                   //校验节点的状态
                    switch (n.state) {
                   //7.1 如果节点状态为 LOOKING , 说明其它节点也在寻找Leader 
                    case LOOKING:
                        // If notification > current, replace and send messages out
                        //7.2 如果收到的投票逻辑时钟 > 当前节点的逻辑时钟, 说明需要更新逻辑时钟为最新的逻辑时钟
                        if (n.electionEpoch > logicalclock.get()) {
                            logicalclock.set(n.electionEpoch);
                          	//7.3 说明之前 recvset 中存储的信息失效, 需要清空掉
                            recvset.clear();
                          	//7.4 进行Vote票据比较 epoch、zxid、sid(myid)
                            //7.4.1 如果接收的票据较新, 更新票据为收到的票据
                            if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                    getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
                                updateProposal(n.leader, n.zxid, n.peerEpoch);
                            //7.4.2 如果当前票据较新, 更新票据信息为当前票据
                            } else {
                                updateProposal(getInitId(),
                                        getInitLastLoggedZxid(),
                                        getPeerEpoch());
                            }
                          //7.5 再次创建ToSend信息, 然后存入阻塞队列 sendqueue 中, 异步发送选举信息
                            sendNotifications();
                        //7.6 如果 收到的票据的 逻辑时钟 < 当前票据的逻辑时钟, 输出日志, 跳过处理
                        } else if (n.electionEpoch < logicalclock.get()) {
                            if(LOG.isDebugEnabled()){
                                LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
                                        + Long.toHexString(n.electionEpoch)
                                        + ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
                            }
                            break;
                        //7.7 接收的票据 和 当前节点的票据一样, 按照 epoch、zxid、sid(myid)进行比较
                        } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                proposedLeader, proposedZxid, proposedEpoch)) {
                          	// 7.7.1 如果接受的票据较新, 更新票据信息
                            updateProposal(n.leader, n.zxid, n.peerEpoch);
                            sendNotifications();
                        }
												//7.8 将接收的票据信息存入 recvset 集合中
                        recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
												
                        //7.9 校验是不是最后一次投票, 只要投票结果超过半数即可, 如果超过 投票结束
                        if (termPredicate(recvset,
                                new Vote(proposedLeader, proposedZxid,
                                        logicalclock.get(), proposedEpoch))) {

                            // Verify if there is any change in the proposed leader
                          	//7.9.1 等待 finalizeWait(单位毫秒) 时间用于接收新的票据, 如果收到可能改变Leader的新选票, 需要重新计票
                            while((n = recvqueue.poll(finalizeWait,
                                    TimeUnit.MILLISECONDS)) != null){
                                if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                                        proposedLeader, proposedZxid, proposedEpoch)){
                                    recvqueue.put(n);
                                    break;
                                }
                            }

                            /*
                             * This predicate is true once we don't read any new
                             * relevant message from the reception queue
                             */
                          //7.10 如果 notifaction 为空, 说明在 finalizeWait 时间内没有接收新的票据,Leader节点可以确定了
                            if (n == null) {
                            //7.10.1 比较 proposedLeader 和当前节点的sid是否一致, 来确定当前节点是Leader还是follower角色
                                self.setPeerState((proposedLeader == self.getId()) ?
                                        ServerState.LEADING: learningState());

                                Vote endVote = new Vote(proposedLeader,
                                                        proposedZxid,
                                                        logicalclock.get(),
                                                        proposedEpoch);
                              //7.10.2 清空 recvqueue 队列信息  
                              leaveInstance(endVote);
                                return endVote;
                            }
                        }
                        break;
                    case OBSERVING: // observer 不参与选举, 跳过
                        LOG.debug("Notification from observer: " + n.sid);
                        break;
                    case FOLLOWING:
                    case LEADING:
                        /*
                         * Consider all notifications from the same epoch
                         * together.
                         */
                        //检查是否已选出领导人
                        //8. 考虑来自同一逻辑时钟的所有通知
                        if(n.electionEpoch == logicalclock.get()){
                            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.
                         */
                        //9. 确认大多数节点都跟随相同的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.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:
                        LOG.warn("Notification state unrecognized: {} (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 {
            try {
                if(self.jmxLeaderElectionBean != null){
                    MBeanRegistry.getInstance().unregister(
                            self.jmxLeaderElectionBean);
                }
            } catch (Exception e) {
                LOG.warn("Failed to unregister with JMX", e);
            }
            self.jmxLeaderElectionBean = null;
            LOG.debug("Number of connection processing threads: {}",
                    manager.getConnectionThreadCount());
        }
    }

​ 可以看出选举逻辑还是有点复杂的, 其主要的步骤如下

  • 首先更新当前节点的逻辑时钟 logicalclock, 并更新FastLeaderElection投票信息为当前节点相关信息, 包含epoch、zxid、myid

  • 向其它节点发送 notifaction 通知

    • self.getVotingView().values() 获取zoo.cfg中配置的所有server节点信息, for循环遍历处理

    • 这是一个异步操作, 先创建ToSend信息, 然后存入阻塞队列 sendqueue, 进行异步处理

      private void sendNotifications() {
              for (QuorumServer server : self.getVotingView().values()) {
                  long sid = server.id;
      						//创建 ToSend信息
                  ToSend notmsg = new ToSend(ToSend.mType.notification,
                          proposedLeader,
                          proposedZxid,
                          logicalclock.get(),
                          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.get())  +
                            " (n.round), " + sid + " (recipient), " + self.getId() +
                            " (myid), 0x" + Long.toHexString(proposedEpoch) + " (n.peerEpoch)");
                  }
                  //存入阻塞队列 sendQueue, 进行异步处理
                  sendqueue.offer(notmsg);
              }
          }
      
  • 如果当前节点是 LOOKING 状态 且没有停止服务, while循环持续不断进行处理

  • 从 recvqueue 队列中获取 其它节点的 Notification

    • 如果没有获取到 notification, 根据 QuorumCnxManager 是否已经发送完来确定如何继续处理
      • 如果haveDelivered() = true,说明已经发送完消息,通过sendNotifications()重新发送消息进行异步处理
      • 如果haveDelivered() = false, 说明有消息没有发送完, 连接所有其它服务器, 然后进行消息发送
    • 如果有收到 notification , 校验sid、leaderId是有有效, 即校验是否在getView()包含的服务器节点中
  • 如果接收的节点状态 state=LOOKING , 说明其它节点也在寻在Leader, 然后进行逻辑时钟比较

    • 如果recv节点的逻辑时钟 > 当前节点的逻辑时钟
      • 更新当前节点的逻辑时钟, 然后清空recvset中存储的数据
      • 比较 recv节点 和 当前节点票据哪个比较新, 比较顺序: epoch、zxid、myid
      • 将票据更新为最新票据
      • 再次创建ToSend信息, 然后存入阻塞队列 sendqueue 中, 异步发送投票信息
    • 如果recv节点的逻辑时钟 < 当前节点的逻辑时钟
      • 输出日志信息, 记录当前操作
    • 如果recv节点的逻辑时钟 = 当前节点的逻辑时钟
      • 比较 recv节点 和 当前节点票据哪个比较新, 比较顺序: epoch、zxid、myid
      • 如果 recv节点比较新, 更新投票节点为 recv节点信息, 然后再次发送投票
      • 如果 当前节点比较新, 继续执行后续逻辑处理
    • 校验投票是否可以结束
      • 如果选举的Leader超过半数, 进入投票结束操作
      • 首先等待 finalizeWait(单位毫秒) 时间用于接收新的票据, 如果收到可能改变Leader的新选票, 重新计票
      • 等待 finalizeWait时间后, 如果没有新的投票, 说明在 finalizeWait 时间内没有接收新的票据,Leader节点可以确定了
      • 比较 proposedLeader 和当前节点的sid是否一致, 来确定当前节点是Leader还是follower角色
    • 返回最后的投票信息

8.2 termPredicate 半数校验选举是否成功

​ 从8.1节分析, 在进行 票据 比较后, 会判断最新的票据信息是否已经超过了半数,如果超过半数, 进行选举退出逻辑处理

//FastLeaderElection
protected boolean termPredicate(
            HashMap<Long, Vote> votes, // 收到的所有票据
            Vote vote) { // 当前节点的最新票据

  HashSet<Long> set = new HashSet<Long>();

  /*
         * First make the views consistent. Sometimes peers will have
         * different zxids for a server depending on timing.
         */
  for (Map.Entry<Long,Vote> entry : votes.entrySet()) {
    if (vote.equals(entry.getValue())){
      set.add(entry.getKey()); // 过滤出recvSet中和当前节点具有相同epoch、zxid、logiticLock、leaderId的票据
    }
  }

  return self.getQuorumVerifier().containsQuorum(set); // 校验 票据是否 超过一半
}

​ FastLeaderElection在每次选举后, 会构造出本次最新票据(Vote), 然后从接收的 recvSet集合中查找所有和当前票据(Vote) 相同票据, 最后校验,这些票据数量是否超过一半

  • 是,返回 true

  • 否,返回false

    //QuorumMaj
    public boolean containsQuorum(Set<Long> set){
      return (set.size() > half);
    }
    

九、投票选举通信流程

​ 在讲解Leader选举过程中, 对投票的网络通信有提及过, 比如6.3节, 这里对通信内容再讲解一下

9.1 ToSend、Notification 比较

ToSend Notification
leader 被推荐的服务器sid 被推荐的服务器sid
zxid 被推荐服务器当前最新事物id 被推荐服务器当前最新事物id
peerEpoch 被推荐服务器当前所处epoch 被推荐服务器当前所处epoch
electionEpoch 当前服务器所处epoch 竞选服务器所处epoch
stat 当前服务器当前状态 竞选服务器当前状态
sid 当前接收消息服务器sid(即: myid) 竞选服务器的sid

​ ToSend、Notification是FastLeaderElection的内部类, 它对投票信息进行了包装(主要包含上面表格中的内容), 而进行投票选举主要比较的内容包括: electionEpoch、epoch、zxid、sid , 这些信息都存储在了ToSend、Notification

9.2 Listener 投票信息的发送、接收

​ QuorumPeer类通过方法createElectionAlgorithm创建选举算法时, 会创建 QuorumCnxManager 对象用于管理服务节点之间的连接, 那 QuorumCnxManager 是怎么管理连接的呢? QuorumCnxManager内部有个继承自 ZookeeperThread 线程变量Listener, 既然Listener是线程, 查看run方法代码

public void run() {
  int numRetries = 0;
  InetSocketAddress addr;
  while((!shutdown) && (numRetries < 3)){
    try {
      // 创建客户端连接 ServerSocket
      ss = new ServerSocket();
      ss.setReuseAddress(true);
      // listenOnAllIPs 是否监听端口的所有ip, 默认初始值 = false, 通过构造函数 改变值
      if (listenOnAllIPs) {
        int port = view.get(QuorumCnxManager.this.mySid)
          .electionAddr.getPort();
        addr = new InetSocketAddress(port);
      } else {
        addr = view.get(QuorumCnxManager.this.mySid)
          .electionAddr;
      }
      LOG.info("My election bind port: " + addr.toString());
      // 设置线程名称
      setName(view.get(QuorumCnxManager.this.mySid)
              .electionAddr.toString());
      // 启动socket
      ss.bind(addr);
      // 服务没有停止, 就一直进行监听
      while (!shutdown) {
        Socket client = ss.accept();
        setSockOpts(client);
        // 根据zoo.cfg 配置文件内容 确定是异步连接、还是同步连接
        //QuorumPeerMain -> quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
        if (quorumSaslAuthEnabled) {
          receiveConnectionAsync(client);
        } else {
          receiveConnection(client);
        }

        numRetries = 0; // 重置服务启动重试次数, 否则重试次数递增
      }
    } catch (IOException e) {
      //... 省略部分逻辑
    }
  }
  //... 省略部分逻辑
}

9.3 投票信息发送 Notification

​ 在第八节分析Leader选举代码时, 当前节点会将自己的信息封装成Vote票据, 然后通过 sendNotifications() 方法进行消息发送, 以便参与Leader投票, sendNotifications() 方法主要的逻辑如下。

​ 从代码逻辑可以看出, 会将zoo.cfg配置的服务节点依次遍历, 然后将必要的信息封装到ToSend实例中, 最后存入阻塞队列 sendqueue进行异步处理。

//FaseLeaderElection
private void sendNotifications() {
  /**
  	* 1. 给所有节点发送投票信息
  	* self: QuorumPeer
  	* getVotingView: 获取zoo.cfg 中配置的集群节点地址信息
  	*/
  for (QuorumServer server : self.getVotingView().values()) {
    long sid = server.id;
		
    //1.1 将当前节点的最新投票信息 封装为ToSend
    ToSend notmsg = new ToSend(ToSend.mType.notification,
                               proposedLeader,
                               proposedZxid,
                               logicalclock.get(),
                               QuorumPeer.ServerState.LOOKING,
                               sid,
                               proposedEpoch);
    //1.2 将信息存入同步阻塞队列 sendqueue 中
    sendqueue.offer(notmsg);
  }
}

9.4 消息发送WorkerSender

​ 为什么说消息发送是WorkerSender呢 ? 首先再看下 6.3 节提到的创建 FastLeaderElection 对象构造函数执行流程, 从代码执行流程可以看出, 实例化 FastLeaderElection 时内部会创建处理消息对象 Messenger , 而Messenger 内部创建了两个线程WorkerSender、WorkerReceiver, 它们分别处理消息的发送和消息的接收。

//FastLeaderElection
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
  this.stop = false;
  this.manager = manager;
  starter(self, manager);
}

private void starter(QuorumPeer self, QuorumCnxManager manager) {
  			//1. 设置成员属性信息
        this.self = self;
        proposedLeader = -1;
        proposedZxid = -1;
				//2. 实例化 发送、接受 两个阻塞队列, 用于消息的存储
        sendqueue = new LinkedBlockingQueue<ToSend>();
        recvqueue = new LinkedBlockingQueue<Notification>();
			  //3. 创建Message对象, 内部创建了两个线程用于接收、发送消息 
        this.messenger = new Messenger(manager);
}

//FastLeaderElection.Messenger
Messenger(QuorumCnxManager manager) {
  this.ws = new WorkerSender(manager);
  Thread t = new Thread(this.ws,
                        "WorkerSender[myid=" + self.getId() + "]");
  t.setDaemon(true);
  t.start();

  this.wr = new WorkerReceiver(manager);
  t = new Thread(this.wr,
                 "WorkerReceiver[myid=" + self.getId() + "]");
  t.setDaemon(true);
  t.start();
}

​ 既然WorkerSender是消息发送的处理类, 我们看下它是如何进行消息发送处理的, 下面是WokerSender实现类

//FastLeaderElection.WorkerSender
class WorkerSender extends ZooKeeperThread {
  volatile boolean stop;
  QuorumCnxManager manager;

  WorkerSender(QuorumCnxManager manager){
    super("WorkerSender");
    this.stop = false;
    this.manager = manager;
  }

  public void run() {
    while (!stop) {
      try {
        //从阻塞队列 sendqueue 获取需要发送的ToSend对象, 超时时间是 3000ms
        ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
        if(m == null) continue;
				
        //处理消息发送入口, 有新的message需要发送时,会调用这个方法
        process(m);
      } catch (InterruptedException e) {
        break;
      }
    }
    LOG.info("WorkerSender is down");
  }

  /**
             * Called by run() once there is a new message to send.
             *
             * @param m     message to send
             */
  void process(ToSend m) {
    ByteBuffer requestBuffer = buildMsg(m.state.ordinal(), 
                                        m.leader,
                                        m.zxid, 
                                        m.electionEpoch, 
                                        m.peerEpoch);
    //QuorumCnxManager#toSend进行消息发送 
    manager.toSend(m.sid, requestBuffer);
  }
}

9.5 消息发送QuorumCnxManager#toSend

​ WorkerSender线程进行消息发送时, 实际调用的是QuorumCnxManager#toSend方法, 下面是具体代码

//QuorumCnxManager
public void toSend(Long sid, ByteBuffer b) {
  /*
         * If sending message to myself, then simply enqueue it (loopback).
         */
  if (this.mySid == sid) {
    // 如果消息发送给自己, 直放入自己接收队列中
    b.position(0);
    addToRecvQueue(new Message(b.duplicate(), sid));
    /*
             * Otherwise send to the corresponding thread to send.
             */
  } else {
    /*
              * Start a new connection if doesn't have one already.
              */
    // 如果消息发送给其它节点, 看是否有已经建立 connection
    //1. 如果没有可以使用的connection, 开启一个新的连接
    //2. 如果有可以使用的连接, 使用现有连接发送
    ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
    // 判断当前的sid是否已经存在于发送队列, 如果存在(bqExisting)则直接将已经存在的数据发送出去
    ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
    if (bqExisting != null) {
      addToSendQueue(bqExisting, b);
    } else {
      addToSendQueue(bq, b);
    }
    connectOne(sid);

  }
}

​ 跟踪代码可以发现,创建连接链路为 connectOne -> initiateConnection -> startConnection,后面直接分析 startConnection

9.6 开启连接 startConnection

​ startConnection是创建连接的入口, zookeeper连接按照sid大小, 只能大的连接小的, 下面查看具体代码

//QuorumCnxmanager
private boolean startConnection(Socket sock, Long sid)
            throws IOException {       
  // If lost the challenge, then drop the new connection
  //1. 比较远程服务器节点和当前节点sid的大小
  if (sid > this.mySid) {
    //1.1 如果当前节点的 mySid < 远程节点sid, 说明 小sid的节点在连接大sid的节点, 需要关闭连接
    LOG.info("Have smaller server identifier, so dropping the " +
             "connection: (" + sid + ", " + this.mySid + ")");
    closeSocket(sock);
    // Otherwise proceed with the connection
  } else {
    //1.2 当前节点的mysid > 远程节点的sid , 建立连接
    SendWorker sw = new SendWorker(sock, sid); // SendWorker继承自zookeeperThread的线程
    RecvWorker rw = new RecvWorker(sock, din, sid, sw); // RecvWorker继承自zookeeperThread的线程
    sw.setRecv(rw);
		
    //2. 如果节点编号为sid的服务器之前创建过连接, 现在关闭, 然后更新SendWorker信息
    SendWorker vsw = senderWorkerMap.get(sid);
    if(vsw != null)
      vsw.finish();
    senderWorkerMap.put(sid, sw);
    queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
		
    //3. 启动发送、接收信息线程
    sw.start();
    rw.start();

    return true;    

  }
  return false;
}

9.7 消息发送SendWorker

​ SendWorker启动后, 会监听对应sid的阻塞队列, 如果启动时阻塞队列为空, 会从 lastMessageSent获取数据进行发送, 以确保最后一条消息能被其它节点接收, 防止上次服务器处理时异常退出,造成消息没有正常处理。

​ 如果队列有要发送的内容,不断监听阻塞队列, 有消息时调用send方法进行消息发送。

public void run() {
  threadCnt.incrementAndGet();
  //1. 如果sid对应的阻塞队列为空, 从lastMessageSent获取最后一次发送的数据, 避免服务器处理时异常退出, 消息没有正常处理
  try {
    //1.1 获取sid对应的阻塞队列
    ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
    //1.2 如果阻塞队列为空, 从lastMessageSent获取最后一次发送的数据进行发送
    if (bq == null || isSendQueueEmpty(bq)) {
      ByteBuffer b = lastMessageSent.get(sid);
      if (b != null) {
        LOG.debug("Attempting to send lastMessage to sid=" + sid);
        //1.3 发送数据
        send(b);
      }
    }
  } catch (IOException e) {
    LOG.error("Failed to send last message. Shutting down thread.", e);
    this.finish();
  }

  try {
    //2. sid对应的阻塞队列不为空, 监听阻塞队列, 有消息时就通过send发送消息
    while (running && !shutdown && sock != null) {

      ByteBuffer b = null;
      try {
        //2.1 从queueSendMap获取sid对应的阻塞队列
        ArrayBlockingQueue<ByteBuffer> bq = queueSendMap
          .get(sid);
        if (bq != null) {
          b = pollSendQueue(bq, 1000, TimeUnit.MILLISECONDS);
        } else {
          LOG.error("No queue of incoming messages for " +
                    "server " + sid);
          break;
        }
				//2.2 更新lastMessageSent中sid存储的消息, 然后发送消息
        if(b != null){
          lastMessageSent.put(sid, b);
          send(b);
        }
      } catch (InterruptedException e) {
      }
    }
  } catch (Exception e) {
  }
}
}

9.8 连接请求接收 receiveConnection

​ 根据9.2节 QuorumCnxManager#Listener 代码分析, 消息接收方法是 receiveConnection, 该方法会调用 handleConnection 方法做最后的处理

//QuorumCnxManager
/**
	* 如果服务器收到连接请求, 则放弃新的连接, 它会检测是否有连接已经连接到服务器
	* 如果有服务器连接, 发送最小可能失去连接的节点信息
	*/
public void receiveConnection(final Socket sock) {
  DataInputStream din = null;
  try {
    din = new DataInputStream(
      new BufferedInputStream(sock.getInputStream()));

    handleConnection(sock, din);
  } catch (IOException e) {
    LOG.error("Exception handling connection, addr: {}, closing server connection",
              sock.getRemoteSocketAddress());
    closeSocket(sock);
  }
}

9.9 连接请求处理 handleConnection

​ handleConnection是处理连接请求的入口, 它主要做了什么呢? 首先看下其具体代码

private void handleConnection(Socket sock, DataInputStream din)
            throws IOException {
  //... 省略部分代码
  LOG.debug("Authenticating learner server.id: {}", sid);
  authServer.authenticate(sock, din);
	
  //1. 如果 远程连接请求 的sid < 当前节点的连接请求, 需要将旧的连接请求关闭, 重新创建连接, 由自己发起连接
  // 因为服务集群中, 只能 较大的sid 连接 较小的sid
  //If wins the challenge, then close the new connection.
  if (sid < this.mySid) {
    /*
             * This replica might still believe that the connection to sid is
             * up, so we have to shut down the workers before trying to open a
             * new connection.
             */
    //1.1 远程节点的sid比较小, 停止旧的 SendWorker线程工作
    SendWorker sw = senderWorkerMap.get(sid);
    if (sw != null) {
      sw.finish();
    }

    /*
             * Now we start a new connection
             */
    LOG.debug("Create new connection to server: " + sid);
    //1.2 关闭connection
    closeSocket(sock);
    //1.3 创建新的连接, 由当前节点 连接 远程节点, 因为当前节点的sid 比较大
    connectOne(sid);

    // Otherwise start worker threads to receive data.
  } else {
    //2. 已经建立了连接, 在这个连接基础上创建 发送、接收工作线程
    SendWorker sw = new SendWorker(sock, sid);
    RecvWorker rw = new RecvWorker(sock, din, sid, sw);
    sw.setRecv(rw);
		//2.1 更新 sid 对应 sendWorker 信息
    SendWorker vsw = senderWorkerMap.get(sid);
    if(vsw != null)
      vsw.finish();
    senderWorkerMap.put(sid, sw);
    //2.2 创建sid对应的阻塞队列, 并存入 queueSendMap 中
    queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY));
		
    //2.3 启动 用于消息 接收、发送 的工作线程
    sw.start();
    rw.start();

    return;
  }
}

十、Leader选举后逻辑处理

10.1 leader选举后的逻辑处理

​ 第七节我们分析了 LOOKING 角色节点进行Leader选举的逻辑, 但是leader 选举出来后, 逻辑处理并没有结束, QuorumPeer.run() 方法仍然需要继续执行, 我们再次进入 QuorumPeer.run()方法 分析后续处理逻辑

public void run() {
  //... 省略部分逻辑
  try {
    /*
             * Main loop
             */
    while (running) {
      switch (getPeerState()) {
          //... 省略部分逻辑
        case OBSERVING:
          try {
            LOG.info("OBSERVING");
            setObserver(makeObserver(logFactory));
            observer.observeLeader();
          } catch (Exception e) {
            LOG.warn("Unexpected exception",e );                        
          } finally {
            observer.shutdown();
            setObserver(null);
            setPeerState(ServerState.LOOKING);
          }
          break;
        case FOLLOWING:
          try {
            LOG.info("FOLLOWING");
            setFollower(makeFollower(logFactory));
            follower.followLeader();
          } catch (Exception e) {
            LOG.warn("Unexpected exception",e);
          } finally {
            follower.shutdown();
            setFollower(null);
            setPeerState(ServerState.LOOKING);
          }
          break;
        case LEADING:
          LOG.info("LEADING");
          try {
            setLeader(makeLeader(logFactory));
            leader.lead();
            setLeader(null);
          } catch (Exception e) {
            LOG.warn("Unexpected exception",e);
          } finally {
            if (leader != null) {
              leader.shutdown("Forcing shutdown");
              setLeader(null);
            }
            setPeerState(ServerState.LOOKING);
          }
          break;
      }
    }
  } finally {
    //... 省略部分逻辑
  }
}

​ FastLeaderElection选举之后, 会根据当前节点情况设置节点角色为 OBSERVING、FOLLOWING、LEADING , 线程再次执行run方法时, switch会根据不同的角色走不同的逻辑, 分析上面的代码, 区别主要是 makeObserver()、makeFollower()、makeLeader()

​ 如果需要重新leader选举, 会执行finally代码块, 更新节点的角色为 LOOKING, 重新执行 lookForLeader()方法

// 创建 Follower, 核心是创建 FollowerZooKeeperServer, 表示follower节点处理请求服务
protected Follower makeFollower(FileTxnSnapLog logFactory) throws IOException {
  return new Follower(this, new FollowerZooKeeperServer(logFactory, 
                                                        this,new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb));
}
// 创建 Leader, 核心是创建 LeaderZooKeeperServer, 表示leader节点处理请求服务
protected Leader makeLeader(FileTxnSnapLog logFactory) throws IOException {
  return new Leader(this, new LeaderZooKeeperServer(logFactory,
                                                    this,new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb));
}
// 创建 Observer, 核心是创建 ObserverZooKeeperServer, 表示observer节点处理请求服务
protected Observer makeObserver(FileTxnSnapLog logFactory) throws IOException {
  return new Observer(this, new ObserverZooKeeperServer(logFactory,
                                                        this, new ZooKeeperServer.BasicDataTreeBuilder(), this.zkDb));
}

​ 这里有个重要的点, 每个节点更新自己角色信息后, 是如何和Leader建立连接的呢 ? 分析代码有下面代码 observer.observeLeader()、follower.followLeader()、leader.lead() , 它们的作用分别是

  • observer.observeLeader() , observer角色节点根据sid和leader进行连接
  • follower.followLeader(), follower角色节点根据sid和leader进行连接
  • leader.lead(), leader角色节点和其它节点follower、observer进行连接

10.2 follower和leader进行连接

​ 首先看下下面代码

void followLeader() throws InterruptedException {
    //... 省略部分代码
    try {
      //1. 根据最新投票的sid 和 所有服务节点的sid进行比较, 找到leader角色节点
        QuorumServer leaderServer = findLeader();            
        try {
          //2. 将follower连接到leader节点
            connectToLeader(leaderServer.addr, leaderServer.hostname);
          //3. 将follower的zxid、myid信息封装后去leader进行 epoch 信息同步
            long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);

            //check to see if the leader zxid is lower than ours
            //this should never happen but is just a safety check
            long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid);
          //4. 如果leader节点的 epoch信息比当前节点的epoch信息要小, 抛出异常
            if (newEpoch < self.getAcceptedEpoch()) {
                LOG.error("Proposed leader epoch " + ZxidUtils.zxidToString(newEpochZxid)
                        + " is less than our accepted epoch " + ZxidUtils.zxidToString(self.getAcceptedEpoch()));
                throw new IOException("Error: Epoch of leader is lower");
            }
          //5. 和leader进行数据同步
            syncWithLeader(newEpochZxid);                
            QuorumPacket qp = new QuorumPacket();
            while (this.isRunning()) {
                readPacket(qp);
                processPacket(qp);
            }
        } catch (Exception e) {
            //... 
        }
    } finally {
        zk.unregisterJMX((Learner)this);
    }
}
分析上面的代码, 其主要逻辑是
  • 通过 findLeader() 方法进行leader信息查询, 主要是通过最新票据的sid和所有服务节点(zoo.cfg配置)的sid进行比较 来找到leader节点
  • follower和leader进行通信连接, 然后follower将自己的zxid、myid进行封装去leader进行epoch同步
  • 如果leader的epoch比当前follower的epoch还要小抛出异常
  • 如果leader的epoch比当前follower的epoch要大,进行数据信息同步

你可能感兴趣的:(zookeeper,分布式)