四、Zookeeper集群及启动流程

1.1 Zookeeper集群

ZooKeeper集群是由多个ZooKeeper服务器节点组成的分布式系统,用于提供协调服务。一个ZooKeeper集群通常由三个或更多个服务器节点组成。

集群角色如下:

  • Leader(领导者):在ZooKeeper集群中,只有一个节点可以担任Leader角色。Leader负责处理所有的写请求(包括创建、更新和删除操作),并协调集群中的其他节点。Leader通过与Follower节点保持心跳连接,并将更新操作广播给它们。如果Leader节点发生故障,集群会重新选举新的Leader
  • Follower(跟随者):Follower节点负责接收客户端的读请求,并将写请求转发给Leader节点。Follower节点通过与Leader节点保持心跳连接来确保自身状态与Leader保持同步。Follower节点无权执行写操作,只能复制Leader节点的状态。

1.2 启动流程

假设现在有zk节点1、节点2、节点3,集群启动流程如下(节点3和节点2一致):
image.png

大致步骤如下:

  1. 通过快照文件 + 日志文件,将数据加载到内存中
  2. 开启监听端口,默认2181,用于接收客户端的请求
  3. 开始故障恢复阶段,选举leader,节点会先把票投给自己,然后交换选票并比较,当有节点获得超过半数的投票则成为leader,其余节点为follower。接下来进行主从之间的数据同步,leader会将数据同步给follower节点
  4. 开始原子广播阶段,启动zk服务,leader处理读写请求,并会将请求同步给follower,这里采用了两阶段提交机制,当有超过半数的follower确认该请求后,leader才会将该请求commit,将数据更新到内存

源码分析

入口是QuorumPeerMain类的main方法,当前节点会先加载配置文件,然后开始启动,源码如下

org.apache.zookeeper.server.quorum.QuorumPeer

@Override
public synchronized void start() {
    // 1、加载磁盘文件数据到内存中
    loadDataBase();
    // 2、开启监听端口,默认2181
    cnxnFactory.start();
    // 3、准备选举leader
    startLeaderElection();
    // 4、开始启动过程
    super.start();
}

可以看到,当前zk节点QuorumPeer会先加载磁盘文件数据到内存中,开启监听端口,随后准备选举leader,并开始启动过程。start启动过程里包括了leader选举、变成leader/follower之后的处理过程,方法主要源码如下

try {
    /*
     * 主循环
     */
    while (running) {
        // 判断当前节点状态,初始是LOOKING
        switch (getPeerState()) {
        case LOOKING:    // LOOKING状态,选举leader
            LOG.info("LOOKING");
            setBCVote(null);
            setCurrentVote(makeLEStrategy().lookForLeader());
            break;
        case OBSERVING:    // OBSERVING状态,只同步leader数据,不参与投票
            LOG.info("OBSERVING");
            setObserver(makeObserver(logFactory));
            observer.observeLeader();
            break;
        case FOLLOWING:    // FOLLOWING状态,参与事务投票,同步leader数据
            LOG.info("FOLLOWING");
            setFollower(makeFollower(logFactory));
            follower.followLeader();
            break;
        case LEADING:    // LEADING状态,处理事务,将数据同步给follower
            LOG.info("LEADING");
            setLeader(makeLeader(logFactory));
            leader.lead();
            setLeader(null);
            break;
        }
    }
} finally {
    LOG.warn("QuorumPeer main thread exited");
}

1.3 leader选举

节点间如何通信?

首先要在这些zookeeper节点之中选出一个leader,节点之间需要互相通信,zookeeper具体做法是将节点id大的连接到节点id小的,如下图
image.png
sid=3的zk03节点连接到zk01、zk02,sid=2的zk02节点连接到zk01,这样就在两两之间建立了连接。代码如下

// 如果对方的sid小于自己的sid
if (sid < self.getId()) {   
    SendWorker sw = senderWorkerMap.get(sid);
    if (sw != null) {
        sw.finish();
    }

    // 关闭这个socket
    closeSocket(sock);
    // 自己连接到对方
    connectOne(sid);
} else {
    // 否则启动SendWorker、RecvWorker
    SendWorker sw = new SendWorker(sock, sid);
    RecvWorker rw = new RecvWorker(sock, sid, sw);
    sw.setRecv(rw);

    SendWorker vsw = senderWorkerMap.get(sid);
    senderWorkerMap.put(sid, sw);

    sw.start();
    rw.start();
    
    return true;    
}

如何选举出leader?

zookeeper使用了一种叫FastLeaderElection快速选举算法,具体步骤如下:

  • 每个节点拥有一张选票,选票里包括sid(节点id)、zxid(事务id)、peerEpoch(epoch,类似年号)信息,这张选票一开始投给自己,并把这张选票发送给其他节点
  • 每个节点接收到其他节点的选票,会与自己当前选票进行比较,先比较epoch,epoch相同进一步比较zxid,zxid相同再比较sid,如果对方的选票大于自己当前选票,则将自己的选票投给对方节点,并再次发送给其他节点,否则只记录不改票
  • 每个节点都会记录接收到的投票信息,当某个节点拥有了超过半数的投票,则认定该节点为leader,其余节点为follower。

结合源码来看,节点一开始先投票给自己,如下

org.apache.zookeeper.server.quorum.QuorumPeer

synchronized public void startLeaderElection() {
      // 先投票给自己
      currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
}

然后将选票发送给其他节点,每个节点接收到其他节点的选票后,与自己当前选票进行比较。如果选票变更,需要再次将选票发送给其他节点,以让其他节点感知。部分代码如下

org.apache.zookeeper.server.quorum.FastLeaderElection

public Vote lookForLeader() throws InterruptedException {
    HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();

    LOG.info("New election. My id =  " + self.getId() +
            ", proposed zxid=0x" + Long.toHexString(proposedZxid));
    // 将选票发送给其他节点
    sendNotifications();
    
    // 循环直到找到leader
    while ((self.getPeerState() == QuorumPeer.ServerState.LOOKING) &&
            (!stop)) {
       
        // 接收到其他节点的选票
        FastLeaderElection.Notification n = recvqueue.poll(notTimeout,
                TimeUnit.MILLISECONDS);
        
        switch (n.state) {
            case LOOKING:
                // 将接收到的选票与自己当前选票进行比较
                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));

                // 如果已经有节点获得了半数的投票
                if (termPredicate(recvset,
                        new Vote(proposedLeader, proposedZxid,
                                logicalclock, proposedEpoch))) {

                    // 等待finalizeWait时间,如果又接收到了其他选票,再进行比较
                    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) {
                        self.setPeerState((proposedLeader == self.getId()) ?
                                QuorumPeer.ServerState.LEADING : learningState());

                        Vote endVote = new Vote(proposedLeader,
                                proposedZxid,
                                logicalclock,
                                proposedEpoch);
                        leaveInstance(endVote);
                        return endVote;
                    }
                }
                break;
            case OBSERVING:
            case FOLLOWING:
            case LEADING:
                // 略
            default:
                break;
        }
    }
    return null;
}

可以看到,当某个节点拥有了超过半数的投票,则认定该节点为leader,其余节点为follower。
选票比较的源码如下:

// 比较选票
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
    if(self.getQuorumVerifier().getWeight(newId) == 0){
        return false;
    }
    // 先比较epoch,再比较zxid,最后比较sid
    return ((newEpoch > curEpoch) || 
            ((newEpoch == curEpoch) &&
            ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));
}

1.4 数据同步

leader选举出来之后,每个follower会主动连接到leader,并开始与leader进行数据同步。同步策略有3种,分别为DIFF、TRUNC、SNAP。leader会存储最近的一部分事务,用于快速同步数据,minCommittedLog表示这些事务里的最小zxid,maxCommittedLog表示这些事务里的最大zxid

  • DIFF:当minCommittedLog <= (follower节点的zxid) <= maxCommittedLog时,会将zxid到maxCommittedLog的这一部分事务,发送给follower
  • TRUNC:当(follower节点的zxid)> maxCommittedLog时,说明此时follower节点存在部分事务超过了leader节点,会发送TRUNC命令让follower将这一部分数据删除
  • SNAP(默认):当(follower节点的zxid)< minCommittedLog时,说明此时follower节点与leader节点的数据存在较大差距,leader会发送当前数据快照给follower节点进行同步

部分源码如下:

int packetToSend = Leader.SNAP;
long zxidToSend = 0;
long leaderLastZxid = 0;
ReentrantReadWriteLock.ReadLock rl = lock.readLock();
try {
    rl.lock();
    // 最近事务的最大zxid、最小zxid
    final long maxCommittedLog = leader.zk.getZKDatabase().getmaxCommittedLog();
    final long minCommittedLog = leader.zk.getZKDatabase().getminCommittedLog();
    // 最近事务集合
    LinkedList<Leader.Proposal> proposals = leader.zk.getZKDatabase().getCommittedLog();

    if (proposals.size() != 0) {

        // 当minCommittedLog <= peerLastZxid <= maxCommittedLog时,发送DIFF命令,逐一发送事务并提交
        if ((maxCommittedLog >= peerLastZxid) && (minCommittedLog <= peerLastZxid)) {
            LOG.debug("Sending proposals to follower");

            long prevProposalZxid = minCommittedLog;
            boolean firstPacket=true;

            packetToSend = Leader.DIFF;
            zxidToSend = maxCommittedLog;

            for (Leader.Proposal propose: proposals) {
                // 跳过follower节点已经存在的事务
                if (propose.packet.getZxid() <= peerLastZxid) {
                    prevProposalZxid = propose.packet.getZxid();
                    continue;
                } else {
                    // 逐一发送事务,并提交
                    queuePacket(propose.packet);
                    QuorumPacket qcommit = new QuorumPacket(Leader.COMMIT, propose.packet.getZxid(),
                            null, null);
                    queuePacket(qcommit);
                }
            }
        } else if (peerLastZxid > maxCommittedLog) {
            // 当peerLastZxid > maxCommittedLog,发送TRUNC命令
            packetToSend = Leader.TRUNC;
            zxidToSend = maxCommittedLog;
            updates = zxidToSend;
        } else {
            LOG.warn("Unhandled proposal scenario");
        }
    } else {
        LOG.debug("proposals is empty");
    }

    LOG.info("Sending " + Leader.getPacketType(packetToSend));
    leaderLastZxid = leader.startForwarding(this, updates);
} finally {
    rl.unlock();
}

// 给follower节点发送SNAP命令
if (packetToSend == Leader.SNAP) {
    zxidToSend = leader.zk.getZKDatabase().getDataTreeLastProcessedZxid();
}
oa.writeRecord(new QuorumPacket(packetToSend, zxidToSend, null, null), "packet");
bufferedOutput.flush();

// 给follower发送数据快照
if (packetToSend == Leader.SNAP) {
    leader.zk.getZKDatabase().serializeSnapshot(oa);
    oa.writeString("BenWasHere", "signature");
}
bufferedOutput.flush();

当follower与leader完成数据同步之后,follower会返回ACK给leader,当超过半数的follower返回ACK时,leader才会正式启动服务。

至此大致的启动流程就结束了,后续会介绍zookeeper在启动服务之后,是如何接收并处理请求的。


kamier
1.5k 声望493 粉丝

引用和评论

0 条评论