zookeeper(四)領導者選舉

xqcode發表於2020-10-23

先上圖:
在這裡插入圖片描述
首先看到這個圖肯定就會很懵,確實比較多,體系結構也比較複雜,目前但是如果是根據原始碼除錯加上這篇部落格應該問題就不是很大。

我們首先從判定為叢集開始說起:即從org.apache.zookeeper.server.quorum.QuorumPeerMain#runFromConfig開始。這裡重點說一下quorumPeer.start();方法。

    @Override
    public synchronized void start() {
        //載入事務和快照。其實就是恢復的意思。主要就是把資料載入到記憶體,拿到紀元號之類的
        loadDataBase();
        //就是啟動thread執行緒,跟單機的那個執行緒做的一樣的事
        cnxnFactory.start();
        //領導者選舉。(重要)
        //https://www.cnblogs.com/johnvwan/p/9546909.html 這篇部落格講得很好。
        startLeaderElection();
        //使用選舉演算法選出領導並開始同步。
        super.start();
    }

前面2個不說,就跟單機啟動一模一樣。重點是第三個和第四個。
跟蹤到這個方法中:org.apache.zookeeper.server.quorum.QuorumPeer#startLeaderElection

    synchronized public void startLeaderElection() {
    	try {
    	    //引數:1、myid。2.最新的zxid。3、當前的紀元號
            //首先投自己一票。在投票箱中寫上自己的資訊。
    		currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
    	} catch(IOException e) {
    		RuntimeException re = new RuntimeException(e.getMessage());
    		re.setStackTrace(e.getStackTrace());
    		throw re;
    	}
    	//getView().values()就是獲取到配置檔案中的所有以server.開頭的資訊。包括observer
        for (QuorumServer p : getView().values()) {
            if (p.id == myid) {//如果id號是自己
                //為什麼要有這個判斷,因為你不但要告訴其它伺服器投的誰,你還要告訴其它伺服器是你投的
                myQuorumAddr = p.addr;
                break;
            }
        }
        if (myQuorumAddr == null) {
            throw new RuntimeException("My id " + myid + " not in the peer list");
        }
        if (electionType == 0) {
            try {
                udpSocket = new DatagramSocket(myQuorumAddr.getPort());
                responder = new ResponderThread();
                responder.start();
            } catch (SocketException e) {
                throw new RuntimeException(e);
            }
        }
        //建立選舉演算法,預設是3.electionAlg可以更改。
        //此外還有0,1,2.但是現在一般都不推薦了。所以一般不會動
        //步驟如下:
        //1、初始化QuorumCnxManager
        //2、初始化QuorumCnxManager.Listener
        //3、執行QuorumCnxManager.Listener
        //4、執行QuorumCnxManager
        //5、返回FastLeaderElection物件
        //流程如下:
        //1.把自己的投票放入queueSendMap中:用於傳送自己的投票資訊。首先肯定是投自己。
        //2.queueSendMap中傳送給其它伺服器,如果是自己這一臺就直接放到recvQueue中,如果是其它伺服器就通過socket傳送投票資訊。
        //3.不斷的檢測recvQueue中的投票資訊,如果在某一時刻recvQueue中過半的伺服器(通過sid標識),那個被投票的那個人就設定為leader,其它就設定為follower。
        //伺服器連線方式id大的去連線id小的,不允許小的連線大的。
        this.electionAlg = createElectionAlgorithm(electionType);
    }

這個方法的整體邏輯已經在方法註釋中體現出來了。最複雜的是哪個方法呢?看註釋就知道是createElectionAlgorithm這個方法了。

    protected Election createElectionAlgorithm(int electionAlgorithm){
        Election le=null;

        //TODO: use a factory rather than a switch
        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 3:
                //建立一個QuorumCnxManager,裡面存在幾個重要的屬性。
                //ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap:
                //ConcurrentHashMap<Long, SendWorker> senderWorkerMap:就是用來記錄其他伺服器id以及對應的SendWorker的
                //ArrayBlockingQueue<Message> recvQueue:儲存選票
                //QuorumCnxManager.Listener
                qcm = createCnxnManager();
                //姑且認為就是一個監聽器。因為是一個執行緒。
                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;
    }

由於0 1 2都是不建議使用了,所以我們們就直接看3的情況。當然這裡面哪裡很複雜呢?當然是le = new FastLeaderElection(this, qcm);這個了。這個也是今天的重點,快速領導者選舉,會很複雜,我們一步一步來。

FastLeaderElection初始化

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

幹了什麼呢?就是初始化的賦值,然後進入starter方法。好像也沒什麼,我們看看這個方法。

starter(self, manager)

    private void starter(QuorumPeer self, QuorumCnxManager manager) {
        this.self = self;
        proposedLeader = -1;
        proposedZxid = -1;

        sendqueue = new LinkedBlockingQueue<ToSend>();
        recvqueue = new LinkedBlockingQueue<Notification>();
        this.messenger = new Messenger(manager);
    }

前面3行就是初始化的。重點是後面3行。看起來好像也沒有什麼,但是意義非常重大。你現在不需要記住什麼,就只需要記住一點,初始化了兩個queue。sendqueue 和recvqueue ,用處顧名思義。牢記,後面能用上。

this.messenger = new Messenger(manager)

        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();
        }

看起也簡單。new WorkerSender和new WorkerReceiver而已,然後就開子執行緒啟動了。那麼現在開始涉及到多執行緒了。我們先看WorkerSender,再看WorkerReceiver。

WorkerSender

            public void run() {
                while (!stop) {
                    try {
                        //這裡取了是把所有的參與者都放進去了。什麼時候放進去的呢?
                        // 通過sendNotifications函式放進去的。
                        ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
                        if(m == null) continue;
                        //開始處理
                        process(m);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
                LOG.info("WorkerSender is down");
            }

sendqueue有印象沒有,前面初始化提到過哦。這個是取方法。那麼肯定就有放的方法。從哪裡放的呢?當然你看到了我的註釋。但是啥時候呼叫的這個方法?神奇不?
當然除錯就知道啦!
一直往前推就能追溯到這個方法:org.apache.zookeeper.server.quorum.QuorumPeer#run,這個方式是什麼時候啟動的呢?第一個程式碼段的super.start();方法啟動的。我們看這段程式碼:

    public void run() {
        setName("QuorumPeer" + "[myid=" + getId() + "]" +
                cnxnFactory.getLocalAddress());

        System.out.println("當前執行緒:" + Thread.currentThread().getName());
        LOG.debug("Starting quorum peer");
        //這個try的內容看不懂要幹啥?
        try {
            jmxQuorumBean = new QuorumBean(this);
            MBeanRegistry.getInstance().register(jmxQuorumBean, null);
            for(QuorumServer s: getView().values()){
                ZKMBeanInfo p;
                if (getId() == s.id) {
                    p = jmxLocalPeerBean = new LocalPeerBean(this);
                    try {
                        MBeanRegistry.getInstance().register(p, jmxQuorumBean);
                    } catch (Exception e) {
                        LOG.warn("Failed to register with JMX", e);
                        jmxLocalPeerBean = null;
                    }
                } else {
                    p = new RemotePeerBean(s);
                    try {
                        MBeanRegistry.getInstance().register(p, jmxQuorumBean);
                    } catch (Exception e) {
                        LOG.warn("Failed to register with JMX", e);
                    }
                }
            }
        } catch (Exception e) {
            LOG.warn("Failed to register with JMX", e);
            jmxQuorumBean = null;
        }

        try {
            /*
             * Main loop
             */
            //這裡我有一個誤區,我以為break是直接跳出了while迴圈,但實際是隻是跳出了switch,還是基礎不夠紮實。break不但用於跳出迴圈,也用於跳出switch。
            //測試檔案:com.xq.test.TestWhile
            while (running) {
                switch (getPeerState()) {
                case LOOKING:
                    LOG.info("LOOKING");

                    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,
                                new ZooKeeperServer.BasicDataTreeBuilder(),
                                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();
                            setBCVote(null);
                            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 {
                            setBCVote(null);
                            //不斷更新投票,直到選出leader
                            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);
                        setPeerState(ServerState.LOOKING);
                    }
                    break;
                case FOLLOWING:
                    try {
                        LOG.info("FOLLOWING");
                        //產出一個適合follower的類
                        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 {
                        //產出一個適合leader的類
                        setLeader(makeLeader(logFactory));
                        //這個只是選擇過半能接受的epoch,假設有更新的zxid沒有啟動
                        //在這個確定epoch的時候它啟動了,那麼這個leader會直接丟失多的資料嗎?看程式碼好像是的
                        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 {
            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;
        }
    }

從長度和註釋就知道這是一塊非常核心的程式碼。因為它就是真正的選舉核心以及選舉完了之後的角色分配。這段程式碼實在是非常複雜,建議結合下面的圖理解。
為了便於理解再用文字補充一下:
我們所瞭解到的WorkerSender執行緒是從sendqueue取的資料。而我們現在就是在看是那裡放的資料?放的應該是什麼資料?這個應該理解一下就能想到是你投票的資料了。關於多執行緒這塊需要有一定的基礎才方便理解。
在這裡插入圖片描述
現在我們回到WorkerSender執行緒裡面,再來回顧一下這段程式碼:

            public void run() {
                while (!stop) {
                    try {
                        //這裡取了是把所有的參與者都放進去了。什麼時候放進去的呢?
                        // 通過sendNotifications函式放進去的。
                        ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
                        if(m == null) continue;
                        //開始處理
                        process(m);
                    } catch (InterruptedException e) {
                        break;
                    }
                }
                LOG.info("WorkerSender is down");
            }

取出來之後重點就是process(m);處理了。

            void process(ToSend m) {
                ByteBuffer requestBuffer = buildMsg(m.state.ordinal(), 
                                                        m.leader,
                                                        m.zxid, 
                                                        m.electionEpoch, 
                                                        m.peerEpoch);
                //構造除byte的陣列傳送訊息
                manager.toSend(m.sid, requestBuffer);
            }

主要是看怎麼傳送的,定位到 manager.toSend(m.sid, requestBuffer)。

    public void toSend(Long sid, ByteBuffer b) {
        /*
         * If sending message to myself, then simply enqueue it (loopback).
         */
        if (this.mySid == sid) {//如果是本機的話
             b.position(0);
             //直接新增到recvQueue中
             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.
              */
             ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
             //putIfAbsent 如果傳入key對應的value已經存在,就返回存在的value,不進行替換。如果不存在,就新增key和value,返回null
             ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
             if (bqExisting != null) {//如果不是null,說明已經存在了。證明已經投過一票了。就把b加入到bqExisting中
                 addToSendQueue(bqExisting, b);
             } else {//如果為null,說明目前為止還沒有投過。就把b加入到bq中。為啥新增到不同的佇列中?
                 addToSendQueue(bq, b);
             }
             connectOne(sid);
                
        }
    }

這段程式碼有細節沒懂,不過不是很影響大局。
對照著圖下一個應該是SendWorker執行緒了。這個是啥時候被造出來的?
這個時候分為兩種情況。
1.當前就一臺啟動了,因為目前就一臺,所以不需要像其它伺服器發訊息,所以這個時候並不會造出這個執行緒,而如果第二臺起來了,它才會造這個執行緒。下面給個呼叫鏈。它是怎麼實現的?就是不斷的探測能否與其它伺服器通訊,只有能通訊才造執行緒。
org.apache.zookeeper.server.quorum.QuorumPeer#run—>org.apache.zookeeper.server.quorum.QuorumPeer#setCurrentVote–>org.apache.zookeeper.server.quorum.QuorumCnxManager#connectAll–>org.apache.zookeeper.server.quorum.QuorumCnxManager#connectOne–>org.apache.zookeeper.server.quorum.QuorumCnxManager#initiateConnection–>org.apache.zookeeper.server.quorum.QuorumCnxManager#startConnection

2.當前不是第一個啟動的,即直接就能與其它伺服器通訊。
這個又需要追溯到前面了。QuorumCnxManager.Listener listener = qcm.listener;這個在本頁面搜一下。在啟動listener之後。在這裡面就已經造好了SendWorker和RecvWorker執行緒。這個又是怎麼實現的?因為一啟動就開啟了埠,此時另外一臺在向這邊發資料,所以是被迫造的執行緒!
org.apache.zookeeper.server.quorum.QuorumCnxManager.Listener#run–>org.apache.zookeeper.server.quorum.QuorumCnxManager#receiveConnection–>org.apache.zookeeper.server.quorum.QuorumCnxManager#handleConnection
RecvWorker執行緒也是在這個時候造出來的。
WorkerReceiver也比較類似。雖然細節不一樣,但是大體流程也應該清楚了。
主要是要對投票的邏輯圖熟悉。在看這些稍微好點。
這篇長了點,先這樣吧。後面介紹是怎麼驗證過半的,以及選出後做了一些什麼。

相關文章