zookeeper QuorumPeer 生命周期

zk peer 生命周期

下面内容是本人挑了一些个人感兴趣的点来研究的,实际的内容远不止这些。😆

本人也是才开始啃 zk 源码,有错误的地方欢迎指正~

另外需要说一下,下文中源码均来自官方 3.4.14 分支。

前情提示

peer 是什么?

开始之前,这里再谈谈 QuorumPeer。在 zk server 集群模式启动时,每个 zk 实例默认都是 QuorumPeer,在恢复完数据,并进行 leader 选举之后,每个 peer 进入新的角色 —— leader 或者 follower。

而 zk 服务启动的过程,其实就是集群 peer 初始化和启动的过程,详细的说,该过程包括:

  1. zk 服务的启动。(这个过程包括配置读取、数据恢复)
  2. 初始化选举。(这个过程包括构建选举算法,构建选举通信连接管理器)
  3. 进入状态机。(这个过程就是 peer 整个生命周期不断循环的过程,这里面包括集群选举、扮演角色)

再谈一下我这里所说的 peer 状态机,其实集群逻辑自洽的服务端应用大多都会用到状态机,zk 也不例外,zk 的状态就漂浮在选举态和 leader、follower 之间:

leader宕机
宕机
选举态
leader
follower

主从同步?数据恢复!

主从同步这个概念,很容易让人想起 master-slave 架构的中间件中的数据同步概念。而本文中的主从同步,并不发生在运行态下主从数据的增量同步;本文中的主从同步,主要描述的是在 leader 选举结束之后,follower 需要和主节点同步数据,来保证 follower 数据和 leader 一致,保证集群数据处于一致性装态,这也是 zab 协议的要求。一言蔽之,就是 follower 数据的恢复!

总结

zk 集群中 QuorumPeer 从启动到死亡包含一下流程:

  • main 启动
  • 读取配置文件
  • 启动 zk 集群服务(quorumPeer 对象)
  • 准备选举(此后,zk 服务进入状态机)
  • 启动 quorumPeer 对象,进入选举状态机
  • 投票选举
  • 进入角色
  • 阻塞在当前状态…
1.main 函数启动

zk 的启动入口 main 函数,进入后,真正服务运行委托了 initializeAndRun 方法:

// org.apache.zookeeper.server.quorum.QuorumPeerMain
public static void main(String[] args) {
    QuorumPeerMain main = new QuorumPeerMain();
    try {
        // 真正运行入口
        main.initializeAndRun(args);
    } catch (IllegalArgumentException e) {
        ...
    }
    ...
}
2.读取配置文件

进入 initializeAndRun 后,第一件事就是解析配置,实际上是以文件方式读取的。

这里有个细节,如果读取到了配置文件,就会按照集群方式启动 zk;如果没有读取到启动配置文件,默认会按 standalone 方式读取配置文件。

另外,在启动 zk 服务之前,会开启一个磁盘清理线程,专门清理 snapshot 历史日志。

protected void initializeAndRun(String[] args)
    throws ConfigException, IOException
{
    QuorumPeerConfig config = new QuorumPeerConfig();
    // 读取文件配置
    if (args.length == 1) {
        config.parse(args[0]);
    }

    // 开启磁盘清理任务(清理多余的 历史snapshot 日志)
    // Start and schedule the the purge task
    DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
            .getDataDir(), config.getDataLogDir(), config
            .getSnapRetainCount(), config.getPurgeInterval());
    purgeMgr.start();

    if (args.length == 1 && config.servers.size() > 0) {
        // 拿刚刚解析的配置,启动 zk
        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
        ZooKeeperServerMain.main(args);
    }
}
3.启动 zk 集群服务

启动 zk 集群服务的流程,核心是创建和启动 quorumPeer 对象。quorumPeer 对象专用来管理集群状态。

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

      // 设置其他 peer 的信息(在配置文件中读取到的)
      quorumPeer.setQuorumPeers(config.getServers());
      quorumPeer.setTxnFactory(new FileTxnSnapLog(
              new File(config.getDataLogDir()),
              new File(config.getDataDir())));
      quorumPeer.setElectionType(config.getElectionAlg());
      quorumPeer.setMyid(config.getServerId());
      quorumPeer.setTickTime(config.getTickTime());
      quorumPeer.setInitLimit(config.getInitLimit());
      quorumPeer.setSyncLimit(config.getSyncLimit());
      ...

     
      quorumPeer.initialize();

      // 启动 zk 服务
      quorumPeer.start();
      
      // 主线程等待 zk 服务线程启动完毕
      quorumPeer.join();
  } catch (InterruptedException e) {
      // warn, but generally this is ok
      LOG.warn("Quorum Peer interrupted", e);
  }
}

zk server 的核心启动流程,就在于 quorumPeer 对象启动流程(start 方法里)。这里做了三件非常重要的事:

  1. 恢复节点数据(恢复 ZkDatabase)(这一步很重要,恢复数据之后,可以选举出更好的 leader)
  2. 启动服务端连接处理器
  3. 准备 leader 选举
  4. 开启集群服务(quorumPeer.start(),进入状态机,其中包括选举流程)
public synchronized void start() {
    loadDataBase();
    cnxnFactory.start();        
    startLeaderElection();
    super.start();
}

其中,在发起 leader 选举时,会创建集群 peer 连接管理器(QuorumCnxManager),这个地方会发起和集群中其他节点的连接,后面也会说到。

4.准备 leader 选举

这一步是为选举做准备,是创建选举算法,一般默认选择 FastLeaderElection

protected Election createElectionAlgorithm(int electionAlgorithm){
    Election le=null;
            
    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        
    case 3:
        // 创建集群连接管理对象
        qcm = createCnxnManager(); 
            
        // 监听 38881 端口
        QuorumCnxManager.Listener listener = qcm.listener;
        if(listener != null){
            listener.start();
            
            // 创建选举算法
            le = new FastLeaderElection(this, qcm);
        } else {
            LOG.error("Null listener when initializing cnx manager");
        }
        break;
    default:
        assert false;
    }
    return le;
}

FastLeaderElection 这个算法在前面 leader 选举章节也说明了,该算法主要负责 leader 选举,并进行选举相关的操作:

  • 选票收集
  • 选票核对
  • 选票广播

这个阶段会创建集群连接管理对象,并创建 listener,其用来监听集群选举端口(38881,用于集群各节点互联)

5.启动 quorumPeer,进入选举状态机

准备工作(加载数据、准备 peer 互联管理器、加载选举算法)做完,就会进入 quorumPeer 状态机,这里面会进行选举、互联、接受客户端服务等,各种各样的操作:

public void run() {
	while (running) {
        switch (getPeerState()) {
        case LOOKING:
            // 选举...
            makeLEStrategy().lookForLeader();
            break;
                
        case OBSERVING:
            // 监听 leader
            observer.observeLeader();
            break;
                
        case FOLLOWING:
            // 监听 leader
            follower.followLeader();
            break;
                
        case LEADING:
            // 开始 lead
            leader.lead();
            break;
        }
    }
}
选举

一开始会处于 LOOKING 状态,这个时候是没有 leader 的,需要去发起 leader 选举。leader 选举的过程,就是不断监听 IO 票箱的过程。

选举时通信互联

别忘了,在选举准备阶段,仅仅是监听集群互联的 38881 选举端口,而没有进行集群互联操作,即,只是自行监听了 38881 选举端口,而没有去连接别人的 38881 选举端口。(毕竟选举通信的前提是要互相建连的)

在选举时,当前节点准备广播自己的投票时,则会对所有集群节点进行一次连接(发起集群的互联操作):

private void sendNotifications() {
    // 将自己投票结果发送给所有的投票者
    for (QuorumServer server : self.getVotingView().values()) {
        long sid = server.id;

        ToSend notmsg = new ToSend(...);
        
        sendqueue.offer(notmsg);
    }
}

这个地方会往 sendQueue 里面塞入自己的选票,然后发给所有人。在给每个节点发送时,会和其进行懒连接:

public void toSend(Long sid, ByteBuffer b) {
    // 发给自己的(zk 会给自己也发一下)
    if (this.mySid == sid) {
         b.position(0);
         addToRecvQueue(new Message(b.duplicate(), sid));
	// 发给别人的
    } else {
         
         ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
         ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
         if (bqExisting != null) {
             addToSendQueue(bqExisting, b);
         } else {
             addToSendQueue(bq, b);
         }
         // 兜底懒链接
         connectOne(sid);

    }
}

这样,zk 就完成了和各个节点的连接。

这个时候就是被动和主动相结合的过程了。被动是在选举的准备阶段,就已经开始监听 38881(使用 listener 监听,上述一.4节中结束部分提到的),等其他人来连自己;主动就是选举时也会尝试和其他节点建立连接。

此外,为了避免互联时造成重复连接,zk 还约定了:只能和比自己 zxid 小的节点连接。否则关闭该链接。

6.投票选举

looking 状态下的投票,就是不断 广播自己选票 --> 汇总检查票 的过程:

广播自己选票
汇总检查票
选出结果
变更状态
结束

这个过程之前已经详细说过,这里不再赘述。最终选举结束后,各个节点变更自己当前的状态,然后开始进入自己的角色。

7.进入角色

选举结束后,各个节点开始扮演自己的角色,做一些初始化操作之后,就可以提供 zk 服务了。

主节点(Leader.lead() 方法)

1.创建 learner 连接接收器,专门用于接受和处理来自 learner 的连接,使用的仍然是 28881 端口:

void lead() throws IOException, InterruptedException {
    // log  当前选举耗时为...

    try {
        //...
        // 监听 28881 上新来的 follower 的连接
        cnxAcceptor = new LearnerCnxAcceptor();
        cnxAcceptor.start();
        //...
    }
}

LearnerCnxAcceptor 监听到 follower 等建连请求后,为其创建 learnerHandler(follower、observer 统称 learner),即 learner 节点处理器,然后启动 learnerHandler:

class LearnerCnxAcceptor extends ZooKeeperThread{
    // ...
    @Override
    public void run() {
        
        while (!stop) {
            try{
                Socket s = ss.accept();
                ...

                // 为 learner 创建 连接管理器
                LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
                // 启动连接处理器
                fh.start();
            } catch (SocketException e) {
                ...
            }
        }
        // ...
    }
}

learnerHandler 中,会监听 follower 的数据包,由于 follower 在初始化完毕后,会向 leader 发送包含自己的 zxid 的签到数据包(我自己起的名字,并非来自源码或者官方文档);这个地方 learnerHandler 就会收到这个数据包;learnerHandler 收到数据包后,会判断 learner 的状态,告知其该 learner 怎么从本 leader 获取数据来恢复。可能是 DIFF、SNAP、TRUNC、TUNC+DIFF。

这里很容易混淆,再详细描述下:

leader 在自己这一端,会为每个 learner(follower、observer)创建一个 learnerHandler,这个 learnerHandler 专门处理来自 learner 的请求。也就是说,这个地方主节点为每个 learner 都分配了一个处理器(单独线程),都是 vip 专坐~(没读源码之前,让我来设计,我可能就是一个 handler 来处理器所有 learner)

在选举结束,集群初始化阶段,learnerHandler 会收到各个 learner 的签到数据包,这个数据包就包含了该 learner 的 zxid

当 learnerHandler 收到这个数据包后,会抽取其中的 learner zxid,然后比对 learder 的 minCommittedLog 和 maxCommittedLog,然后决策出该 learner 应该采取何种数据恢复策略。

learnerHandler 的源码如下:

public void run() {
    try {
        // 1.创建 follower 的读写流
        ...

        // 2.完善当前 handler 信息(handler 和 follower 是一对一的关系)
        ...

        // 3.根据 follower 的信息,决定恢复策略 
        ...
           
        // 4.传输 NEWLEADER 包 (夹在3和5之间,需要注意),这一步算是广播给所有 follower,自己是 new leader
        QuorumPacket newLeaderQP = new QuorumPacket(Leader.NEWLEADER,
                ZxidUtils.makeZxid(newEpoch, 0), null, null);
        ...

        // 5.告诉 follower 应该采取何种数据恢复策略(包括 DIFF、SNAP、TRUNC、TRUNC+DIFF)
        oa.writeRecord(new QuorumPacket(packetToSend, zxidToSend, null, null), "packet");
        bufferedOutput.flush();
        ...

        // 6.不停的接收来自 follower 的包
        while (true) {
            qp = new QuorumPacket();
            ia.readRecord(qp, "packet");
            ...
        }
    }
}

需要额外注意的是,在数据恢复操作之间,有一个发送 NEWLEADER 数据包的操作。follower 收到该数据包后

2.开启 zk 服务:

startZkServer();

3.阻塞在和 follower 之间的心跳,如果每次有半数 follower 响应心跳,则继续下次心跳;如果心跳时发现丢失半数心跳,则退出 leader 角色,重新开始选举。

while (true) {
    // 半个 tick 心跳一次
    Thread.sleep(self.tickTime / 2);
    if (!tickSkip) {
        self.tick.incrementAndGet();
    }
    
    // 每次收到心跳后,将该节点添加到 syncedSet 中
    HashSet<Long> syncedSet = new HashSet<Long>();

    syncedSet.add(self.getId());

    for (LearnerHandler f : getLearners()) {
        // 这是异步的方式,如果 ping 失败后,下次心跳才会发现 synced() == false
        if (f.synced() && f.getLearnerType() == LearnerType.PARTICIPANT) {
            syncedSet.add(f.getSid());
        }
        f.ping();
    }

    ...

	// 和半数以上的节点 ping 失败,则结束循环,退出 leader 角色
    if (!tickSkip && !self.getQuorumVerifier().containsQuorum(syncedSet)) {
         shutdown("Not sufficient followers synced, only synced with sids: [ "
                  + getSidSetString(syncedSet) + " ]");
         return;
    } 
    tickSkip = !tickSkip;
}
从节点(follower.followLeader() 方法)

1.当从节点确定自己是从节点之后,要做的第一件事,向主节点建立连接。此时连接的实际上是 28881 端口(回顾下,之前选举是则是 toSend 方法中发起的 38881,leader peer 使用 qcm.listener 监听的),再建立一个链接。

2.建立连接后,follower 立刻向 leader 汇报自己的 zxid,方便 leader 决断 follower 该采取何种数据恢复策略。

3.向主节点查询数据数据恢复策略,并开始进行数据恢复(这个方法里面会启动 zkServer,zkServer 启动后,就可以对外提供服务了,不过需要等待数据恢复结束,并且主节点发来 UPTODATE 指示)

4.停留在地监听来自主节点的数据(大多是 2PC 的 事务 proposal),扮演好自己的 follower 角色

5.会在 4 中循环,完毕

void followLeader() throws InterruptedException {
    
    try {
        QuorumServer leaderServer = findLeader();            
        try {
            // 1.向 leader 节点 28881 节点建立连接
            connectToLeader(leaderServer.addr, leaderServer.hostname);
            
            // 2.向 leader 报道,同时汇报自己的 zxid
            long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);
            
            // 3.向主节点查询数据数据恢复策略,并开始进行数据恢复
            syncWithLeader(newEpochZxid); 
            
            // 4.不停监听主节点的消息
            while (this.isRunning()) {
                readPacket(qp);
                processPacket(qp);
            }
            
            ...
        }
    }
}
8.阻塞在当前状态…

一旦选举结束,确定完当前 peer 在集群中的角色后,就会阻塞在当前状态。等到集群因为意外而发生状态变更了,就会继续回到选举态,继续选举。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值