zookeeper 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类启动其实就是线程的启动, 只是包含了部分其它逻辑

在这里插入图片描述

​ 下面是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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值