微服务系列(二)(2) ZooKeeper源码分析-part-1

本文深入分析ZooKeeper的源码,探讨Zookeeper Server的初始化、通讯原理和选举机制。从启动命令开始,解析配置加载、日志管理器启动、核心服务启动等步骤。重点关注选举过程,包括LOOKING、FOLLOWING和LEADING状态的逻辑,解析节点如何参与选举,确保集群的高可用性和一致性。

微服务系列(二)(2) ZooKeeper源码分析-part-1

本节开始进行ZooKeeper的源码分析,针对Zookeeper Server的初始化过程,通讯原理及选举机制做源码层面上的介绍

作为ZooKeeper的使用者,应该知道如何部署和启动Zookeeper吧

看源码的入口就从这里作为出发点,找到zkServer.sh或者zkServer.cmd

截取zkServer.sh里的一段重要的命令

nohup "$JAVA" $ZOO_DATADIR_AUTOCREATE "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" \
"-Dzookeeper.log.file=${ZOO_LOG_FILE}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" \
-XX:+HeapDumpOnOutOfMemoryError -XX:OnOutOfMemoryError='kill -9 %p' \
-cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &

以及

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

找到了程序入口QuorumPeerMain

进入org.apache.zookeeper.server.quorum.QuorumPeerMain#main

具体链路就不追踪了,做了三件事:

  1. 加载配置文件
  2. 启动清除日志管理器(用于定时清除日志和快照数据)
  3. 启动ZooKeeperServerMain(单机模式下)或org.apache.zookeeper.server.quorum.QuorumPeerMain#runFromConfig(非单机模式下)

继续追踪ZooKeeperServerMain的启动过程

忽略JMX等相关内容,这里的启动同样加载了配置文件,并进入核心启动方法org.apache.zookeeper.server.ZooKeeperServerMain#runFromConfig

  1. MetricsProvider引导程序的初始化和启动,用于度量以及与外部系统交互,目前猜测与选举相关(官方注释:A MetricsProvider is a system which collects Metrics and publishes current values to external facilities.)

  2. FileTxnSnapLog初始化,用于把文件存储的日志和快照载入到JVM并定期写入文件(它会被包裹在ZKDatabase里,由ZKDatabase直接与ZooKeeperServer中的其他组件交互)

  3. ZooKeeperServer初始化和启动,启动有两种模式Netty和NIO,默认采用NIO模式启动,具体的启动步骤就不介绍了,无非是各种工作线程的初始化和启动、参数配置、端口配置、请求处理器配置等

  4. ContainerManager初始化和启动,用于定期检测容器情况,确保容器正常运行

这里我们需要关注的是ZooKeeperServer,关注它的startup()方法

public synchronized void startup() {
    if (sessionTracker == null) {
        createSessionTracker();
    }
    startSessionTracker();
    setupRequestProcessors();

    registerJMX();

    setState(State.RUNNING);
    notifyAll();
}

这里又出现了几个新的类:

  • SessionTracker:用于session信息的保存、创建、销毁、定时检测是否失效

  • RequestProcessor:用于请求处理,包括sync、send、commit等命令执行的操作

差不多到这里,单机模式下的ZooKeeperServerMain类,后面在看集群模式下的过程中会用到它的东西。

回归org.apache.zookeeper.server.quorum.QuorumPeerMain#runFromConfig

与单机模式不同的地方在于,它启动的是QuorumPeer而不是ZooKeeperServer

QuorumPeer初始化过程:

初始化权限相关的类QuorumAuthServerQuorumAuthLearner,分别用于认证server和learner。

QuorumPeer启动过程:

  1. 初始化zkDb(前面看到的包裹了FileTxnSnapLogZKDatabase),这里与单机模式下存在不同的地方,它额外增加了epoch校验的逻辑,不允许出现zxid大于当前epoch的情况。
  2. 启动ServerCnxnFactory(同单机模式下)
  3. 启动AdminServer(同单机模式下)
  4. 启动选举org.apache.zookeeper.server.quorum.QuorumPeer#startLeaderElection,重点来了!

org.apache.zookeeper.server.quorum.QuorumPeer#startLeaderElection

synchronized public void startLeaderElection() {
    try {
        //初始化自己持有的投票信息
        if (getPeerState() == ServerState.LOOKING) {
            currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
        }
    } catch(IOException e) {
        RuntimeException re = new RuntimeException(e.getMessage());
        re.setStackTrace(e.getStackTrace());
        throw re;
    }
	//初始化选举算法类
    this.electionAlg = createElectionAlgorithm(electionType);
}
protected Election createElectionAlgorithm(int electionAlgorithm){
    Election le=null;

    //TODO: use a factory rather than a switch
    switch (electionAlgorithm) {
    case 1:
        //AuthFastLeaderElection已废弃
        le = new AuthFastLeaderElection(this);
        break;
    case 2:
        //AuthFastLeaderElection已废弃
        le = new AuthFastLeaderElection(this, true);
        break;
    case 3:
        QuorumCnxManager qcm = createCnxnManager();
        QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);
        if (oldQcm != null) {
            LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");
            oldQcm.halt();
        }
        QuorumCnxManager.Listener listener = qcm.listener;
        if(listener != null){
            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;
}

这里有一个QuorumCnxManager.Listener类,它是用于监听选举端口,用于选举通信的类,暂且不深入看,继续看FastLeaderElection

它的核心流程是初始化了这样一个类Messenger,并启动了它

Messenger代码过多,就不粘贴出来了,从实现上看,它的大致功能是:

  1. QuorumCnxManager中接受选举信息的数据包并进行选举相关的操作
  2. 发送选举信息(如投票)到QuorumCnxManager,由QuorumCnxManager进行选举相关的通讯

其内部创建了两个阻塞队列sendqueue和recvqueue,由workerSender线程负责选举数据包的发送,由workerReceiver线程负责选举数据包的接收和逻辑处理,典型的生产者/消费者模型。

QuorumCnxManager源码可以看到,在QuorumCnxManager维护了senderWorkerMapqueueSendMap两个map,前者是保存连接到本机的client socket、RecvWorker(用于接受client socket发送回来的数据包)等信息,后者是给每个client socket维护了一个发送队列,可以发现,ZooKeeper通过给每个连接分配了一个队列来保证发送数据包的顺序性。

接下来将会完全关注ZooKeeper的选举算法,深入了解其选举算法如何保证集群的高可用性和一致性。

前面知道Zkserver的启动,最终调用了QuorumPeer.start(),本质上进入的代码块是org.apache.zookeeper.server.quorum.QuorumPeer#run,其中有一个重要的部分:

while (running) {
    switch (getPeerState()) {
    case LOOKING:
        LOG.info("LOOKING");
        ServerMetrics.LOOKING_COUNT.add(1);

        if (Boolean.getBoolean("readonlymode.enabled")) {
            LOG.info("Attempting to start ReadOnlyZooKeeperServer");

            // Create read-only server but don't start it immediately
            final ReadOnlyZooKeeperServer roZk =
                new ReadOnlyZooKeeperServer(logFactory, this, this.zkDb);

            // Instead of starting roZk immediately, wait some grace
            // period before we decide we're partitioned.
            //
            // Thread is used here because otherwise it would require
            // changes in each of election strategy classes which is
            // unnecessary code coupling.
            Thread roZkMgr = new Thread() {
                public void run() {
                    try {
                        // lower-bound grace period to 2 secs
                        sleep(Math.max(2000, tickTime));
                        if (ServerState.LOOKING.equals(getPeerState())) {
                            roZk.startup();
                        }
                    } catch (InterruptedException e) {
                        LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
                    } catch (Exception e) {
                        LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
                    }
                }
            };
            try {
                roZkMgr.start();
                reconfigFlagClear();
                if (shuttingDownLE) {
                    shuttingDownLE = false;
                    startLeaderElection();
                }
                setCurrentVote(makeLEStrategy().lookForLeader());
            } catch (Exception e) {
                LOG.warn("Unexpected exception", e);
                setPeerState(ServerState.LOOKING);
            } finally {
                // If the thread is in the the grace period, interrupt
                // to come out of waiting.
                roZkMgr.interrupt();
                roZk.shutdown();
            }
        } else {
            try {
               reconfigFlagClear();
                if (shuttingDownLE) {
                   shuttingDownLE = false;
                   startLeaderElection();
                   }
                setCurrentVote(makeLEStrategy().lookForLeader());
            } catch (Exception e) {
                LOG.warn("Unexpected exception", e);
                setPeerState(ServerState.LOOKING);
            }                        
        }
        break;
    case OBSERVING:
        try {
            LOG.info("OBSERVING");
            setObserver(makeObserver(logFactory));
            observer.observeLeader();
        } catch (Exception e) {
            LOG.warn("Unexpected exception",e );
        } finally {
            observer.shutdown();
            setObserver(null);
            updateServerState();

            // Add delay jitter before we switch to LOOKING
            // state to reduce the load of ObserverMaster
            if (isRunning()) {
                Observer.waitForReconnectDelay();
            }
        }
        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);
           updateServerState();
        }
        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);
            }
            updateServerState();
        }
        break;
    }
    start_fle = Time.currentElapsedTime();
}

这段代码比较长,分解来看,只要running标志不为false,则一直根据getPeerState()的值来决定运行逻辑,这里有四条分支:LOOKING、OBSERVING、FOLLOWING、LEADING

稍微理一下,按哪种顺序去学习,学习过相关一致性算法的应该熟悉这几个角色:

LOOKING:正在寻找LEADER

OBSERVING:暂且不管,放到最后去了解

FOLLOWING:正在跟随,已经找到了LEADER

LEADING:正在领导,自己已经作为LEADER,让其他节点FOLLOING

最初所有的节点都应是LOOKING,然后进入FOLLOING、LEADING的状态,这样的过程称为选举,那么我们就从LOOKING->FOLLOWING/LEADING->OBSERVING这样的顺序来学习。

首先进入LOOKING的代码块:

找到重要的部分

//重置reconfig标志
reconfigFlagClear();
//保证election初始化
if (shuttingDownLE) {
    shuttingDownLE = false;
    startLeaderElection();
}
//这里是关键部分,makeLEStrategy().lookForLeader()以及将选票保存到QuorumPeer上下文
setCurrentVote(makeLEStrategy().lookForLeader());

makeLEStrategy().lookForLeader()进入org.apache.zookeeper.server.quorum.FastLeaderElection#lookForLeader

分解重要逻辑,第一个重要逻辑:

synchronized(this){
    logicalclock.incrementAndGet();
    updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}

当执行lookForLeader时,首先会更新logicalclock(发起选举的次数),然后从本地QuorumPeer中提取信息来更新Election中的提案(表示本次选举的目标)。

接着会进入while ((self.getPeerState() == ServerState.LOOKING) &&(!stop))循环,只要QuorumPeer的状态依然是LOOKING,则一直执行while内部的代码。

继续看其中第二个重要逻辑:

Notification n = recvqueue.poll(notTimeout,
        TimeUnit.MILLISECONDS);

/*
 * Sends more notifications if haven't received enough.
 * Otherwise processes new notification.
 */
if(n == null){
    ...
}
else if (validVoter(n.sid) && validVoter(n.leader)) {
	...
}else{
    ...
}

recvqueue其实就是我们前面看到的配合Messenger工作的消息队列,当Messenger收到了消息并解析后会将消息投递到该队列,并由Election来poll处理。

这块逻辑会判断是否接收到新的Notification消息,如果没有接收到新的Notification消息,则会重新检查本地是否存在未分发的、积累在队列中的消息,如果队列不存在未分发的消息,则会重新为每个已知的同伴节点(动态变化的,可能有节点宕机,无法连接)发送ToSend消息;如果能接收到新的Notification消息,则进入票据验证、判断、更新提案的逻辑。

也就是说,zk节点在LOOKING状态下会不断的分发本机的投票信息给其他节点,同时接收其他节点的投票信息进行处理和分析,下面继续看zk节点在LOOKING状态下是如何校验、判断、更新提案的。

这就进入了第三个重要逻辑:

validVoter(n.sid) && validVoter(n.leader)
/**
 * Check if a given sid is represented in either the current or
 * the next voting view
 *
 * @param sid     Server identifier
 * @return boolean
 */
private boolean validVoter(long sid) {
    return self.getCurrentAndNextConfigVoters().contains(sid);
}

这里做的事情很简单,验证这条消息中的发送者和他所认为的LEADER是否是本机所认识的节点(否则,消息会被认为不是本集群的机器而被忽略)。

继续进入switch语句,分别处理来自LOOKING、FOLLOWING、LEADING、OBSERVING状态的节点的消息。

进入第四个重要逻辑,处理来自LOOKING状态节点下的消息:

case LOOKING:
    ...
    //代码比较长,就不贴出来了

这段逻辑会处理来自LOOKING状态下的节点的消息:

  1. 判断对方的epoch是否大于本机的logicalclock(前面说了它代表了本机zk节点发起过几次选举),如果大于,则表示该消息是有效的(这里实际上是过滤掉延迟失效的选举消息)。
  2. 判断对方的epoch是否大于本机的epoch,如果大于,则接受对方的投票信息而替换更新本地的投票信息(更新提案)。
  3. 如果epoch相同的情况下,则会判断对方最大zxid是否大于本机的zxid,如果大于,则接受对方的投票信息而替换更新本地的投票信息(更新提案)。
  4. 如果epoch、zxid均相同的情况下,则比较serverId(即myid),如果大于,则接受对方的投票信息而替换更新本地的投票信息(更新提案)。
  5. 接着,还会重新发送ToSend消息(把最新的投票信息重新分发出去)。
  6. 最后,根据接受到的投票做一次投票的统计(具体如何做统计的,请查看org.apache.zookeeper.server.quorum.SyncedLearnerTracker),并返回最终胜出的投票信息。

继续进入第五个重要逻辑,处理来自OBSERVING状态下的节点的消息:

什么都没有做,直接忽略!

继续进入第六个重要逻辑,处理来自FOLLOWING和LEADING状态下的节点的消息:

这两种状态下的消息用了相同的逻辑处理

  1. 检查该投票中的leader与我认为的leader是否吻合
  2. 如果不吻合,我需要进行投票统计并选择是否加入到其中
  3. 如果校验均通过,则最终加入到该投票消息所认为的leader领导的集群中

到这里,了解了LOOKING状态下的节点在选举模式下是如何工作的,接着继续了解FOLLOING和LEADING状态下的节点在选举模式下是如何工作的。

case FOLLOWING:
    try {
       LOG.info("FOLLOWING");
        setFollower(makeFollower(logFactory));
        follower.followLeader();
    } catch (Exception e) {
       LOG.warn("Unexpected exception",e);
    } finally {
       follower.shutdown();
       setFollower(null);
       updateServerState();
    }
    break;

那么,跟LOOKING类似,进入核心逻辑org.apache.zookeeper.server.quorum.Follower#followLeader

老样子,分解成几个重要的逻辑来看。

第一个重要逻辑

QuorumServer leaderServer = findLeader();

这里不贴源码了,findLeader()做的事情就是遍历本机已知的同伴节点并找到自身认为的leader节点后返回。

QuorumServer保存了节点的通信信息,包括hostname、electionAddr、clientAddr等。

第二个重要逻辑

connectToLeader(leaderServer.addr, leaderServer.hostname);

进入代码,只需要关心这一句sock.connect(addr, timeout);

而这里的sockjava.net.Socket,本质上就是用了socket通信

并且zk支持了两种SSLSocket和普通socket

当然,这里指的是zk的消息同步时的通信方式,需要与election通信区分,别忘了zoo.cfg中一般来说是配置了2888和3888两个端口

第三个重要逻辑

syncWithLeader(newEpochZxid);

这里的详细逻辑也不去探究了,大致就是利用了前文提到的ZkDatabase来提取、更新、截断、覆盖内存中的数据,直到达到同步时写入到文件,最终令两者最大的zxid相同,此时就完成了一次同步。

第四个重要逻辑

QuorumPacket qp = new QuorumPacket();
                while (this.isRunning()) {
                    readPacket(qp);
                    processPacket(qp);
                }

同步完消息后并没有直接结束,而是进入while循环,不断的收取来自leader的消息并作出相应的处理,如事务消息的发送、提交等

同样,看看LEADING状态的节点如何工作。

进入org.apache.zookeeper.server.quorum.Leader#lead

第一个重要逻辑

zk.loadData();

当节点成为leader时,会首先清理当前持有已死亡的session信息,并为当前zkDatabase保存信息生成快照文件。(便于follower节点的信息同步)

第二个重要逻辑

cnxAcceptor = new LearnerCnxAcceptor();
cnxAcceptor.start();

这里类似与reactor模型中的reactor线程,单独开启了一个线程来接受accept请求,等待新的follower连接进来,可以联系上的是,前面从follower过程中了解到其通讯方式确实是socket.accept()。

需要注意的是,最终leader与follwer的交互均由这个类org.apache.zookeeper.server.quorum.LearnerHandler来实现

第三个重要逻辑

waitForEpochAck(self.getId(), leaderStateSummary);
waitForNewLeaderAck(self.getId(), zk.getZxid());

当与follwers建立连接后,还需要验证当前的参与者是否达到了集群要求,这个操作就是等待follwers人数达标。

第四个重要逻辑

startZkServer();

达标数量的follwers与leader建立连接后,启动zkServer并初始化新的epoch。

官方注释:Start up Leader ZooKeeper server and initialize zxid to the new epoch

第五个重要逻辑

while (true) {
    synchronized (this) {
        long start = Time.currentElapsedTime();
        long cur = start;
        long end = start + self.tickTime / 2;
        while (cur < end) {
            wait(end - cur);
            cur = Time.currentElapsedTime();
        }

        if (!tickSkip) {
            self.tick.incrementAndGet();
        }

        // We use an instance of SyncedLearnerTracker to
        // track synced learners to make sure we still have a
        // quorum of current (and potentially next pending) view.
        SyncedLearnerTracker syncedAckSet = new SyncedLearnerTracker();
        syncedAckSet.addQuorumVerifier(self.getQuorumVerifier());
        if (self.getLastSeenQuorumVerifier() != null
                && self.getLastSeenQuorumVerifier().getVersion() > self
                        .getQuorumVerifier().getVersion()) {
            syncedAckSet.addQuorumVerifier(self
                    .getLastSeenQuorumVerifier());
        }

        syncedAckSet.addAck(self.getId());

        for (LearnerHandler f : getLearners()) {
            if (f.synced()) {
                syncedAckSet.addAck(f.getSid());
            }
        }

        // check leader running status
        if (!this.isRunning()) {
            // set shutdown flag
            shutdownMessage = "Unexpected internal error";
            break;
        }

        if (!tickSkip && !syncedAckSet.hasAllQuorums()) {
            // Lost quorum of last committed and/or last proposed
            // config, set shutdown flag
            shutdownMessage = "Not sufficient followers synced, only synced with sids: [ "
                    + syncedAckSet.ackSetsToString() + " ]";
            break;
        }
        tickSkip = !tickSkip;
    }
    for (LearnerHandler f : getLearners()) {
        f.ping();
    }
}

这里就是定期的不断的给followers发送心跳包,并统计实时在线数,当集群中节点数不能达到要求(小于集群总节点数的一半)时,则重置isRunning标志以停止节点服务。

最后了解一下OBSERVING做了什么

org.apache.zookeeper.server.quorum.Observer#observeLeader

...
QuorumServer master = findLearnerMaster();
...
connectToLeader(master.addr, master.hostname);
...
syncWithLeader(newLeaderZxid);
...
while (this.isRunning() && nextLearnerMaster.get() == null) {
                    readPacket(qp);
                    processPacket(qp);
                }

熟悉的感觉,这…

跟follwer的操作有啥不一样的吗??

差别在于他们虽然都是连接到leader上,但当收到leader的消息时,做了不同的处理。

比较

org.apache.zookeeper.server.quorum.Observer#processPacket

org.apache.zookeeper.server.quorum.Follower#processPacket

可以看到:

observer处理的内容有:

  • PING
  • REVALIDATE
  • SYNC
  • INFORM
  • INFORMANDACTIVATE

follower处理的内容有:

  • PING
  • PROPOSAL
  • COMMIT
  • COMMITANDACTIVATE
  • REVALIDATE
  • SYNC

observer与leader建立连接并接受心跳、同步等信息,而follower还额外接受事务提交相关的请求,也就是说,observer会同步leader信息,follower则还是需要处理事务消息的参与者(这也是zk为什么具有强一致性的原因,下文会介绍到事务消息的实现)。

到这里,终于完成了不同状态节点在选举模式下的工作内容的解读,下面整理一下各个节点的工作内容并思考为什么能实现集群的高可用。

leader:成为leader节点时,与各个follwer节点建立socket连接,此时leader启动server,可以接受client请求。

follower:成为follower时,与leader节点建立socket连接,当follower从leader同步完消息,启动server,可以接受client请求。(通过心跳判断leader状态,leader离线后会进入LOOKING状态)

observer:成为observer时,与leader节点建立socket连接,当observer从leader同步完消息,启动server,此时可以接受client读请求,写请求将会转发到leader节点。(作为弱一致性的副本存在)

另外,其选举机制保证了在任何时刻只要集群中存在半数以上的节点可以相互通信和正常工作,则这个集群就依然可以对外正常提供服务。

ZooKeeper的选举机制的核心在于zxid,zxid作为事务ID,每个节点同步到的信息都具备了顺序性,并且leader节点与follower节点的zxid必须保证强一致性(下文会讲到zk通过2pc的方式来保证强一致性),这样的情况下,无论什么时刻有节点发生故障,不仅可以选举出一个leader节点正常工作,还能通过zxid来保证了集群数据的两个特性:

  1. 已提交的消息不会丢失
  2. 未提交的消息需要丢弃

另外,超过集群数量的一半这个条件,也保证了集群不会出现脑裂的现象。

当然,虽然ZooKeeper的ZAB协议看似实现了很好的强一致性和可靠性,但也伴随着牺牲了部分性能,毕竟谁也逃不掉CAP原则。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值