Zookeeper Zab 協議解析——Zookeeper 源碼解析之初探領導者選舉算法實現(三)

一、概述

  本篇博文會從 Zookeeper 中 FLE(Fast Leader Election) 算法的代碼實現入口着手,依次分析 FLE 算法的初始化流程和主要的選舉流程的代碼實現,並着重分析 FLE 算法中在 Zookeeper 啓動時的選主流程。

  注:整個代碼基於 Zookeeper 3.5.5 版本。

二、核心類簡介

2.1 QuorumCnxManager

  首先QuorumCnxManager是針對使用TCP進行領導者選舉而實現的一款 連接管理器(connection manager) ,它能夠爲集羣中的每對節點之間維護一條連接,同時可以確保每對運行正常且可以通過網絡進行通信的服務器之間都 只有一條 連接。

  爲什麼上面會強調每對服務器之間僅有一條連接呢,這主要是因爲TCP是雙工的,因此沒有必要在每對節點間重複建立連接。如果兩個服務器同時嘗試建立TCP連接,那麼連接管理器會使用一種比較簡單的根據雙方的 IP 地址決定斷開哪條連接的tie-breaking機制來進行處理。

  在一個 Zookeeper 節點中,對於 其它的每一個 節點連接管理器QuorumCnxManager都爲它們維護了一個發送消息的隊列,具體代碼實現如下,即queueSendMap中的鍵爲節點的標識,而值爲要發送給該節點的數據,而如果與任何特定節點的連接斷開,則發送方線程(Sender Thread)會將消息重新放回列表中。

    final ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>> queueSendMap;

  並且因爲這種實現的方式是使用隊列來維護要發送到另一個節點的消息,所以每次都會將消息添加到隊列的尾部,從而可以保證消息的順序性。這裏先大概講這些,目前大家先對QuorumCnxManager有個印象知道它是做什麼的就可以了,具體的實現可以之後再分析。

三、源碼解析

3.1 選舉入口分析

  在開始對 FLE 算法的 Zookeeper 實現源碼分析之前,我們先來探究一下 Zookeeper 中的選舉入口,首先對於選舉發生的時機一般存在三種:

  1. Zookeeper 集羣啓動時;

  2. Leader 掛掉後(可能爲 Leader 與大多數 Follower 失去網絡連接);

  3. 當 Leader 發現支持自己的 Follower 的數量不滿足法定數量(未過半)時;

  因此我們只需要跟隨 Zookeeper 集羣的啓動流程就可以找到選舉的入口,通過 Zookeeper 啓動的腳本文件 zkServer.sh 我們可以發現 Zookeeper 在啓動時其實是啓動了一個主類 QuorumPeerMain,具體的腳本代碼如下。

# zkServer.sh
ZOOMAIN="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.local.only=
$JMXLOCALONLY org.apache.zookeeper.server.quorum.QuorumPeerMain"

  因此我們直接進入到這個 QuorumPeerMain 類中,並查看它的 Main 方法邏輯,在 Main 方法中首先會創建一個 QuorumPeer 對象,然後利用 Main 方法的入參來初始化並啓動 Zookeeper ,這裏 Main 方法的入參 args 其實就是我們自己配置並傳入的 zoo.cfg 文件。

   在進入到initializeAndRun方法後首先會對我們傳入的配置文件進行解析,將文件中對應的信息解析到QuorumPeerConfig對象中,然後再調用 runFromConfig方法來通過構建好的QuorumPeerConfig對象啓動 Zookeeper (準確來說是啓動 QuorumPeer)。

   在runFromConfig方法中會根據傳入的QuorumPeerConfig對象創建一個新的QuorumPeer對象,對於這個QuorumPeer對象我們可以理解爲它就是本臺服務器在 Zookeeper 集羣中的實際體現,也就是 Zookeeper 中的一個節點,在這個QuorumPeer對象中會保存關於本臺服務器在 Zookeeper 中的所有信息以及當前 Zookeeper 集羣的相關信息。在創建好這個QuorumPeer對象後會調用它的 start 方法來啓動該 Zookeeper 節點,並在成功啓動後調用join阻塞主線程,使主線程進入到TIMED_WAITING的狀態進行等待。

// QuorumPeerMain.java
public static void main(String[] args) {
    QuorumPeerMain main = new QuorumPeerMain();
    try {
        main.initializeAndRun(args);
    }
}

protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException
{
    // 省略解析配置文件流程...
    if (args.length == 1 && config.isDistributed()) {
    	// 根據配置文件啓動 Zookeeper
        runFromConfig(config);
    } else {
        // there is only server in the quorum -- run as standalone
        ZooKeeperServerMain.main(args);
    }
}

public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException
{
    try {
    	// 省略將配置解析到 QuorumPeer 流程...
    	
    	// 啓動 QuorumPeer
        quorumPeer.start();
        // QuorumPeer 啓動完成後阻塞主線程
      	quorumPeer.join();
    }
}

  上面的流程走過後我們就進入QuorumPeer啓動邏輯一探究竟,通過上面的分析我們已經知道了外部是通過調用QuorumPeerstart方法來啓動該節點的,而在start方法中主要進行了下面這六步操作。

  1. 首先檢驗當前節點的myid是否存在於配置文件中,如果不存在則直接拋異常;

  2. 然後進入數據恢復流程,從DB中加載數據集到內存的DataTree中(這裏的DataTree是 Zookeeper 在內存中保存數據的一種形式);

  3. 開啓讀取數據的線程cnxnFactorysecureCnxnFactory,這兩個線程的監聽的端口就是我們在zoo.cfg中配置的 clientPortAddressclientPort以及secureClientPortAddresssecureClientPort,這兩個線程的主要作用就是接收客戶端的請求;

  4. 開啓管理者服務器,首先AdminServer是用於運行命令的嵌入式管理服務器的接口,目前 Zookeeper 中實現的管理者服務器僅有兩個實現,一個是JettyAdminServer另一個是DummyAdminServer,而大多數情況下我們都是直接使用空實現的DummyAdminServer

  5. 從這裏開始進入到領導者選舉流程,但在這一步僅完成了選舉算法的選擇以及相關變量的初始化工作,並未真正開始選舉流程;

  6. 調用父類的 start 方法來運行QuorumPeer對象中的選舉等邏輯,而因爲QuorumPeer本身就是繼承自ZooKeeperThread的一個線程對象,所以這裏當調用了 start 方法後就會執行該類的 run 方法,到這裏爲止相當於該 Zookeeper 節點已經進入了運行狀態;

// QuorumPeer.java
public synchronized void start() {
	// 1.檢驗當前節點的 myid 是否存在於配置文件中
    if (!getView().containsKey(myid)) {
        throw new RuntimeException("My id " + myid + " not in the peer list");
    }
    // 2.加載數據集到內存的 DataTree 中
    loadDataBase();
    // 3.開啓讀取數據的線程
    startServerCnxnFactory();
    try {
    	// 4.開啓管理者服務器
        adminServer.start();
    }
    // 5.初始化領導者選舉(暫未開啓選舉流程)
    startLeaderElection();
    // 6.啓動該節點並開啓選舉流程
    super.start();
}

3.2 選舉初始化

synchronized public void startLeaderElection() {
   try {
       if (getPeerState() == ServerState.LOOKING) {
       	   // 如果當前節點爲 Looking 狀態則首先將票投給自己
           currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
       }
   }
   // 如果選擇的選舉算法爲 LeaderElection 則執行下面邏輯
   if (electionType == 0) {
        try {
            udpSocket = new DatagramSocket(getQuorumAddress().getPort());
            responder = new ResponderThread();
            responder.start();
        }
    }
    // 創建一個新的選舉算法實現對象(策略模式)
    this.electionAlg = createElectionAlgorithm(electionType);
}

  startLeaderElection方法中當其狀態爲LOOKING時(初始化時所有節點狀態均爲LOOKING)首先會將票投給自己,然後創建一個選舉算法的實現。對於electionType的設置需要追溯到起初對於配置文件的解析(相關代碼如下)。在配置文件中我們可以通過配置指定選舉領導者時所採用的算法,但是當我們不設置這個值時,默認會使用QuorumPeerConfigelectionAlg的默認值3,對應到createElectionAlgorithm方法中的選擇也就是默認使用FastLeaderElection算法。

// QuorumPeerMain.java
public void runFromConfig(QuorumPeerConfig config) throws IOException, AdminServerException
{
    try {
    	// 從配置對象 QuorumPeerConfig 中獲取選擇的選舉算法
        quorumPeer.setElectionType(config.getElectionAlg());
    }
}

// QuorumPeerConfig.java
protected int electionAlg = 3;

  在 Zookeeper 中的領導者選舉算法的使用是一種 策略模式 ,即你可以根據自己的需要選擇不同的選舉算法實現策略,但這些算法最終都會輸出一個合格的Leader。上面我們已經提到在 Zookeeper 中默認的領導者選舉算法爲FastLeaderElection,所以我們直接看方法中這個算法的執行流程。

  1. 創建並初始化連接管理器QuorumCnxManager

  2. 開啓QuorumCnxManager中的監聽器對選舉端口進行監聽;

  3. 創建FastLeaderElection算法實現實例;

  4. 啓動FastLeaderElection中的接收(receiver)和發送(sender)線程;

  5. 將創建並初始化完成後的選舉算法實現返回;

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:
    	// 1.創建一個連接管理器
        QuorumCnxManager qcm = createCnxnManager();
        // 2.獲取舊的連接管理器
        QuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);
        // 3.如果發現存在舊的連接管理器則調用它的 halt 方法(停止其 Listener 的監聽並關閉該連接管理器)
        if (oldQcm != null) {
            oldQcm.halt();
        }
        // 4.獲取新的連接管理器的 Listener(Listener 的主要作用就是監聽選舉端口)
        QuorumCnxManager.Listener listener = qcm.listener;
        if(listener != null){
        	// 5.啓動 Listener 對端口進行監聽
            listener.start();
            // 6.創建 FastLeaderElection 實現並通過構造器將連接管理器傳入
            FastLeaderElection fle = new FastLeaderElection(this, qcm);
            // 7.啓動 FastLeaderElection 中的發送線程(sender)和接收線程(receiver)
            fle.start();
            le = fle;
        } else {
            LOG.error("Null listener when initializing cnx manager");
        }
        break;
    default:
        assert false;
    }
    return le;
}

  在執行流程中我們提到了 選舉端口(electionAddr),這個端口就是我們在配置文件中配置節點端口時配置的第二個端口(如下圖中的 3888 端口,且第一個端口 2888 爲集羣間內部通訊使用),而當我們要使用 FLE 算法時必須在文件中配置該端口。

	server.1=localhost:2888:3888

  還要強調的一點是 Zookeeper 採用分層的結構,因此數據會在應用層和網絡層之間進行傳遞,而這個傳遞的操作落實到FastLeaderElection中就是其messager屬性所進行的數據管理,正如上述流程中接收線程和發送線程的啓動,實際就是調用的messager對兩個線程進行啓動。因此在後續的選舉流程分析中,爲了保證整個分析過程的流暢性我們不會過多的去關注於網絡層的實現。

// FastLeaderElection.java
public void start() {
    this.messenger.start();
}

// Messenger
void start(){
    this.wsThread.start();
    this.wrThread.start();
}

3.3 選舉流程

  當完成了startLeaderElection的初始化工作後,QuorumPeer就會通過調用其父類 ZooKeeperThreadstart方法來執行自己run方法中的邏輯(QuorumPeer的本質就是一個線程),而在run方法中存在一個主循環來執行包括選舉和數據同步在內的所有邏輯,而當節點當前的狀態爲LOOKING時就會進入到領導者選舉的流程,即通過下面這條語句來設置自己當前的投票對象。

	setCurrentVote(makeLEStrategy().lookForLeader());

  通過makeLEStrategy可以獲取到選舉的算法實現,再調用lookForLeader方法就會進入到特定選舉算法的實現中,所以我們直接進入到FastLeaderElectionlookForLeader方法中梳理選舉邏輯,同時爲了簡化分析流程,我們這裏默認節點會從recvqueue隊列中獲取到選票(選票的具體發送和接收流程涉及很多的網絡層代碼,所以這裏暫且放在一邊)。

  因爲lookForLeader方法的整體邏輯比較繁雜,因此我們將對於選票分狀態處理的代碼拆分出來,而對於lookForLeader方法的邏輯概括一下可以分爲以下這幾步:

  1. 創建保存選票的recvset和用於驗證領導者合法性的outofelection

  2. 首先遞增自己的Epoch,然後更新選票提議自己爲領導者併發送選票;

  3. 之後通過循環不斷與其它節點交換選票直至選出Leader

  4. 如果無法從recvqueue中獲取到新的選票(此時選舉還未完成),則首先判斷當前底層中維護的爲每個節點發送消息的隊列是否爲空,如果爲空則直接再次發送選票,如果不爲空就嘗試修復連接;

  5. 如果可以從recvqueue中獲取到新的選票並且投票人和選票中的領導者均合法,則進入分狀態處理邏輯中;

public Vote lookForLeader() throws InterruptedException {
	// 省略 JMX 處理邏輯...
    try {
    // 接收到的選票集合
    HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
	// 用於驗證是否大多數節點都在跟隨一個相同的領導者以及領導者的合法性
    HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
	// 超時時間
    int notTimeout = finalizeWait;

    synchronized(this){
    	// 遞增自己的 Epoch
        logicalclock.incrementAndGet();
        // 更新選票提議爲自己
        updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
        }

        // 發送選票
        sendNotifications();

		// 通過循環不斷與其它節點交換選票直至選出領導者
        while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){
            
        	// 從 recvqueue 隊列中獲取並移除下一個選票,並在終止時間的兩倍後超時
            Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);

            // 如果從 recvqueue 隊列中沒能獲取到足夠的選票則繼續發送更多的選票
            if(n == null){
            	// 檢查底層 queueSendMap 中所有節點所對應的隊列是否爲空(所有消息是否都已傳遞)
            	if(manager.haveDelivered()){
            		// 如果所有隊列爲空就直接發送選票
                	sendNotifications();
                } else {
                	// 嘗試與每臺未建立連接的服務器建立連接
                    manager.connectAll();
                }

             	// 指數回退,每一次的超時時間爲上一次的兩倍
             	int tmpTimeOut = notTimeout*2;
             	notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval);
             } 
             // 如果從 recvqueue 隊列中獲取到新的選票則對其進行驗證
             // 驗證選票的投票者是否有效以及驗證選票所投的候選人是否有效
             else if (validVoter(n.sid) && validVoter(n.leader)) {
             	switch (n.state) {
                case LOOKING:
					// Looking...
                    break;
                case OBSERVING:
                // Logging...
                    break;
                case FOLLOWING:
                case LEADING:
					// Following And Leading...
                	break;
                default:
                    // Logging...
                    break;
                }
             } else {
             	if (!validVoter(n.leader)) {
                	LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
                }
                if (!validVoter(n.sid)) {
                    LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
                }
        	}
    	}
    	return null;
    } finally {
    	// 省略 JMX 邏輯..
	}
}

  在分狀態處理中首先進行的是LOOKING狀態的判斷,每個節點初始使的默認狀態均爲LOOKING,當接受到選票的投票人的狀態爲LOOKING時,即認爲該投票人也處於選流程中,因此執行下面的處理邏輯:

  1. 首先如果選票的Epoch大於當前節點的Epoch,意味着已經開始了新一屆選舉,因此更新當前節點的Epoch,並清空節點的recvset選票集合,且如果選票中候選人的條件優於當前節點的條件,則當前節點更改選票提議,併發送新的選票;

  2. 其次如果選票的Epoch小於當前節點的Epoch,意味着這是一張無效的選票,所以直接忽略;

  3. 最後如果選票的Epoch等於當前節點的Epoch,且如果選票中候選人的條件優於當前節點的條件,則當前節點更改選票提議,併發送新的選票;

  4. 完成上述選票更新和發送後,將選票以投票人的sid爲鍵保存到recvset集合中,且因爲是以投票人的sid爲鍵進行保存,所以該投票人後續的選票會直接覆蓋掉之前的選票,即每個投票人在recvset集合中僅保存一張有效選票;

  5. 完成選票保存後通過termPredicate方法驗證是否已經具備了足夠數量的選票來結束選舉,方法的實現主要是統計recvset集合中與當前節點預期領導者相同的選票數量是否過半;

  6. 如果已經具備了足夠數量的選票來結束選舉則再次驗證當前節點所預期的領導者是否發生了變化,實現的方式就是從recvset集合中獲取選票,如果選票中的領導者優於當前節點的預期領導者,說明當前節點的預期領導者已經發生變化,因此將該選票再次放回recvset集合中並在break後繼續循環;

  7. 如果在第六步中將recvset集合中所有的選票都判斷後沒有發現優於當前預期領導者的候選人,說明選舉已經完成,因此首先判斷自己是否爲最終的領導者並修改節點的狀態;

  8. 創建選舉結果選票endVote,並在清空recvset集合後將選舉結果返回,完成領導者選舉;

  在上面的判斷過程中存在一個判斷優先級的過程(也就是上面所說的 優於 ),判斷的過程主要是由totalOrderPredicate方法來完成,而具體的判斷邏輯如下:

  1. 首先兩者中Epoch大者優先;
  2. 其次當兩者的Epoch相等時,zxid大者優先;
  3. 最後當兩者的Epochzxid均相等時,myid大者優先;
// Looking...
case LOOKING:
	// 1. 如果選票的 Epoch 大於節點當前的 Epoch
	if (n.electionEpoch > logicalclock.get()) {
	
		// 1.1 更新節點當前的 Epoch
		logicalclock.set(n.electionEpoch);
		// 1.2 清空已接收到的選票集合
		recvset.clear();
		
		if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
			// 1.3 如果選票中候選人的條件優於當前節點的條件,則當前節點更新選票提議
			updateProposal(n.leader, n.zxid, n.peerEpoch);
		} else {
			// 1.3 否則還是投給自己(這裏主要是因爲前面更新了 Epoch 所以需要重新更新選票)
			updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
		}
		// 1.4 發送選票
		sendNotifications();
                            
	} else if (n.electionEpoch < logicalclock.get()) {
	
		// 2. 如果選票的 Epoch 小於節點當前的 Epoch 則直接忽略該選票
		break;             
		
	} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
		
		// 3. 如果選票中候選人的條件優於當前節點預期的領導者的條件
		
		// 3.1 則節點更新選票支持剛剛接收到的選票中的候選人
		updateProposal(n.leader, n.zxid, n.peerEpoch);
		// 3.2 發送選票
		sendNotifications();
	}

	// 將獲取到的選票放入到 recvset 選票集合中
	recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

	// 驗證是否已經具備了足夠的選票來結束選舉
	if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) {

		// 驗證自己提議的領導者是否有任何變化
		while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null){
			// 如果選票集合中存在選票的候選人優於當前自己提議的領導者則將該選票重新放回 recvqueue 集合中並 break
			// 繼續接收選票直到選票集合中所有選票的候選人都弱於或等於自己提議的領導者
			if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){
				recvqueue.put(n);
				break;
			}
		}	

		// 因爲上面會將合格的選票(選票候選人弱於或等於自己提議的領導者)直接從 recvqueue 中移除
		// 所以當從 recvqueue 隊列中無法再取更多選票時意味着選舉結束
		if (n == null) {
			// 判斷自己是否爲最終的領導者並修改節點的狀態
			self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState());
			// 創建終止選票(用於輸出 FLE 算法所選擇出的領導者信息)
			Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
			// 執行終止選舉的相關邏輯(主要是清空 recvqueue 隊列)
			leaveInstance(endVote);
			// 返回終止選票(輸出選舉結果)
			return endVote;
		}
	}
break;

  在分狀態處理中是將對FOLLOWINGLEADING狀態放在一起進行處理的,這主要是因爲當節點接收到的選票處於該狀態時證明其它節點的選舉可能已經完成,因此當前節點在 驗證 後應該跟隨其它節點所選出的那個領導者。

  具體的驗證流程基本如下代碼的註釋所示,這裏需要強調的是outofelection這個集合的作用,這個集合主要是用來驗證是否大多數節點已經跟隨了某個節點,而對於它的統計流程其實和上面LOOKING中的統計流程是一樣的,只不過在這裏被統計的集合是outofelection

  對於上面的描述可以理解爲在正常情況下因爲領導者只需要超半數的選票即可獲選,因此可能在其獲選後或者當有新的節點加入時,會出現當節點的狀態爲LOOKING時卻收到狀態爲FOLLOWINGLEADING的選票,因此這時它只需要按照正常的統計邏輯,在接收到 超半數且支持某個相同領導者 的選票後即可確認存在大量的節點已經跟隨這個領導者,所以它也可以放心的跟隨。

case FOLLOWING:
case LEADING:
// 如果選票的 Epoch 等於當前節點的 Epoch
if(n.electionEpoch == logicalclock.get()){
	// 將該選票添加到選票集合中
	recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
	// 驗證是否已經具備了足夠的選票來結束選舉並驗證選票中領導者的合法性
	if(termPredicate(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)) && checkLeader(outofelection, n.leader, n.electionEpoch)) {
		// 判斷自己是否爲最終的領導者並修改節點的狀態
		self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState());
		// 創建終止選票(用於輸出 FLE 算法所選擇出的領導者信息)
        Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
        // 執行終止選舉的相關邏輯(主要是清空 recvqueue 隊列)
        leaveInstance(endVote);
        // 返回終止選票(輸出選舉結果)
        return endVote;
	}
}

// 該集合用於當前節點在決定跟隨某個領導者之前,驗證是否大多數節點都跟隨了這個領導者
outofelection.put(n.sid, new Vote(n.version, n.leader,  n.zxid, n.electionEpoch, n.peerEpoch, n.state));
// 驗證選票中的領導者是否已經具備了足夠數量的跟隨者並驗證選票中領導者的合法性
if (termPredicate(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)) && checkLeader(outofelection, n.leader, n.electionEpoch)) {
	synchronized(this){
		// 更新當前節點的 Epoch
		logicalclock.set(n.electionEpoch);
		// 判斷自己是否爲最終的領導者並修改節點的狀態
		self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState());
	}
	// 創建終止選票(用於輸出 FLE 算法所選擇出的領導者信息
	Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
	// 執行終止選舉的相關邏輯(主要是清空 recvqueue 隊列)
    leaveInstance(endVote);
    // 返回終止選票(輸出選舉結果)
    return endVote;
}

  而在整個的驗證過程中,如果有一個當選的領導者,並且存在超過法定數量的節點支持該領導者,則必須通過checkLeader方法檢查該領導者是否 已投票 並確認其當前處於 領導狀態 中,而進行此檢查的目的,主要是避免其它節點繼續選舉 已崩潰不再處於領導狀態 的節點。

protected boolean checkLeader(Map<Long, Vote> votes, long leader, long electionEpoch){

	// 如果所有節點都認爲我是一個領導者,那我就是一個領導者
	boolean predicate = true;

	if(leader != self.getId()){
		// 如果我不是領導者,而且選票集合中沒有領導者的選票則返回 false
		// 這也就意味着我沒有接收到領導者開始領導的信號
		if(votes.get(leader) == null) predicate = false;
		// 如果我不是領導者,而且領導者在我這裏的狀態不是 LEADING 則返回 false
        else if(votes.get(leader).getState() != ServerState.LEADING) predicate = false;
	} else if(logicalclock.get() != electionEpoch) {
		// 如果我當前的 Epoch 不等於選舉的 Epoch 返回 false
		predicate = false;
	}

	return predicate;
}

四、內容總結

  在本篇博文裏我們主要分析了 Zookeeper 中 FLE 算法的代碼入口以及其邏輯的部分代碼實現,對於 FLE 算法的更多代碼實現細節會放在之後的博文中,並且在後面的博文中也會仔細分析一下 Zookeeper 中的數據收發流程即網絡層的代碼實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章