1. 程式人生 > >ZooKeeper原始碼學習筆記(3)--Cluster模式下的ZooKeeper

ZooKeeper原始碼學習筆記(3)--Cluster模式下的ZooKeeper

Cluster叢集模式

判斷啟動模式

前一篇文章 介紹了當配置檔案中只有一個server地址時,Standalone模式的啟動流程以及ZooKeeper的節點模型和執行邏輯。在本節中,我會針對Cluster的執行模式進行詳細講解。

啟動流程

public synchronized void start() {
    loadDataBase();
    cnxnFactory.start();        
    startLeaderElection();
    super.start();
}

QuorumPeerMain::runFromConfig會構造一個QuorumPeer

物件,並呼叫start方法啟動整個Server。

QuorumPeer::start經過了三個步驟:

  1. 通過loadDatabase將磁碟中的Snapshot和TxnLog載入到記憶體中,構造一個DataTree物件
  2. 啟動一個ServerCnxnFactory物件,預設啟動一個Daemon執行緒執行NIOServerCnxnFactory,負責接收來自各個Client端的指令。
  3. 啟動選舉過程

前兩步在Standalone的啟動模式也有出現,我們不再做過多介紹,需要的朋友可以看一看前面的文章。在這裡,我們需要留意第三步。

第三步的名字叫做startLeaderElection, 看到這個名字,我的第一反應是他會啟動一個獨立執行緒去負責Leader的選舉,但其實不然。通過原始碼走讀,我們看到startLeaderElection

中其實只是對選舉做了初始化設定,真正的選舉執行緒其實是在QuorumPeer這個執行緒類中啟動的。

選舉演算法我在這裡先不做過多介紹,這會是一個獨立的文章,放在下一篇進行講解,讓我們先看一下選舉之後的狀態。

ZooKeeper Server的三種狀態

Cluster模式顧名思義是由多個server節點組成的一個叢集,在叢集中存在一個唯一的leader節點負責維護節點資訊,其他節點只負責接收轉發Client請求,或者更甚,只是監聽Leader的變化狀態。

ZooKeeper Server的三種狀態

在配置檔案中,我們可以通過peerType對當前節點型別進行配置。目前支援兩種型別:

  1. PARTICIPANT: participant 具有選舉權和被選舉權,可以被選舉成為Leader,如果未能成功被選舉,則成為Follower。
  2. OBSERVER: observer只具備選舉權,他可以投票選舉Leader,但是他本身只能夠成為observer監聽leader的變化。

選舉完成之後,節點從PARTICIPANTOBSERVER兩種狀態變成了LEADER,FOLLOWEROBSERVER三種狀態,每種狀態對應一個ZooKeeperServer子類。

節點啟動圖

選舉結束之後,三種類型的節點根據自身的型別進入啟動流程,啟動對應的ZooKeeperServer

  1. Leader 作為整個叢集中的主節點,會啟動一個 LearnerCnxAcceptor 的執行緒負責同其他節點進行通訊。
  2. Follower和Observer的大致邏輯類似,首先通過配置資訊連線上Leader節點,再向Leader節點發送ACK請求,告知連結成功。
  3. 當Leader中檢測到大部分的Follower都已經成功連結到Leader之前,socket訪問會被阻塞;直到檢測到大部分Follower連結上之後,才退出阻塞狀態,令Leader,FollowerObserver啟動對應的ZooKeeperServer

維護節點的一致性

節點的一致性

上圖中使用黃色的節點表示 Observer 上的操作,藍色的節點表示 Follower 中的操作,紫色的節點表示 Leader 上的操作。

如圖所示,不論是Follower還是Observer在接受到Request請求後,都通過一個RequestProcessor將請求分發給Leader進行處理。單節點的處理邏輯能夠保證資料在各個節點是一致的。

Leader中,通過proposal方法將需要提交的Request加入 outstandingProposals 佇列。

每個Follower或者ObserverLeader建立連結之後會建立一個LearnerHandler執行緒,對於Follower型別的LearnerHandler,線上程的迴圈中,會將outstandingProposals中的Request請求分發回對應的Follower 進行消費,消費完畢後,再通過 SendAckRequestProcessor 發回 Leader

Leader 的任務鏈中存在一個AckRequestProcessor節點,監聽Follower 響應的結果,當大部分Follower都響應了某次提交之後,會認為該提交有效,再通過 CommitProcessor 正式提交到記憶體中。

LeaderZooKeeperServer

Leader的RequestProcessor任務鏈

和Standalone模式的ZooKeeperServer一樣,在LeaderZooKeeperServer中也是通過一個RequestProcessor任務鏈處理來自Client的請求。

  1. PrepRequestProcessor: 在outstandingChanges中建立臨時節點,便於後續請求快速訪問,詳細解析請參看上一篇文章
  2. ProposalRequestProcessor: 在Proposal的構造方法中,會傳入一個RequestProcessor物件,同時他自身也會構造一個包裹了 ACKRequestProcessorSyncRequestProcessor物件。如上圖所示,ProposalRequestProcessornextProcessor消費了Request之後,還會使用SyncReqeustProcessor進行二次消費,這使得任務在這個節點產生了兩個分支。
  3. CommitProcessor: 本節點執行在一個獨立執行緒中,每一次輪詢都會將 queuedRequests 中的請求資訊加入toProcess佇列中,然後在輪詢的開始處,對toProcess佇列進行批量處理。在方法內部有一個區域性變數 nextPending 儲存從 queuedRequests 取出的最後一個 Request 請求,如果nextPending遲遲沒有被提交,則進入等待的邏輯,與此同時,queuedRequests會一直積累請求資訊,直到nextPending的請求通過 CommitProcessor::commit 被提交到 committedRequests 中,才能夠退出等待邏輯,批量消費queuedRequests 中的請求資料。
  4. ToBeAppliedRequestProcessor:呼叫FinalRequestProcessor進行處理,並將請求從toBeApplied佇列中移除。
  5. FinalRequestProcessor:將請求資訊合併到DataTree中,具體操作見前一篇文章
  6. SyncRequestProcessor: 如上一節所說,在SyncRequestProcessor中會將Request 請求封裝成一個Transaction,並寫入 TxnLog, 同時定期備份 Snapshot。
  7. ACKRequestProcessor: 在這個節點中,通過執行leader:processAck 檢查滿足條件的request請求,呼叫CommitProcessor::commit 將滿足響應條件的 Request 提交給 CommitProcessor 處理。

FollowerZooKeeperServer

Follower的RequestProcessor任務鏈

如圖,和之前的任務鏈不同,在FollowerZooKeeperServer中同時存在兩個並行的任務鏈處理。

第一個任務鏈負責將Follower接受到的Request請求分發給 Leader

第二個任務鏈通過接收 Leader 轉發來的 Request 請求,當資料被同步到 disk 之後,通過 SendAckRequestProcessor 將接收結果反饋給 Leader

ObserverZooKeeperServer

Observer的RequestProcessor任務鏈

Observer 中的任務結構和 Follower中的第一個任務鏈很相似。都是負責把接收到的 Request 請求轉發給 Leader

叢集間的互動

節點心跳

Leader::lead() 中有一個 while 迴圈負責維持 Leader 和其他節點之間的心跳關係。

while (true) {
  Thread.sleep(self.tickTime / 2);
  for (LearnerHandler f : getLearners()) {
    f.ping();
  }
  if (!tickSkip && !self.getQuorumVerifier().containsQuorum(syncedSet)) {
    shutdown("Not sufficient followers synced, only synced with sids: [ " + getSidSetString(syncedSet) + " ]");
    return;
  }
}

可以看到,在 while 迴圈中,每個tick週期中會觸發兩次心跳。

每次心跳都是由 Leader 主動傳送給各個節點,節點中也存在一個while 迴圈讀取socket 中的資訊。

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

processPacket中,會處理接收到的 QuorumPacket 物件,當接受到心跳資訊 PING 時,同Leader進行一次socket互動,告知存活。

sync 同步

Client 可以通過API介面傳送一個sync訊號給叢集中的任意節點。 不論Leader 是直接或是間接接收到 sync 訊號,都會將這個訊號通過叢集內部的 socket 連結分發給各個節點。

Follower或者Observer 接收到叢集廣播的 sync 訊號時,會在內部呼叫 commit 方法,確保 CommitProcessor 能夠呼叫 FinalRequestProcessor 將Transaction Log 合併到 DataTree 中。

Leader異常的重新選主

如果因為 Leader 異常導致叢集中的其他節點無法正常訪問Leader,則會重新進入選主的流程。

我們在 QuorumPeer 中看到,不論是 Leader , Follower 還是 Observer 他們在出現異常之後,都會 setPeerState(ServerState.LOOKING),從而進入選主流程。

總結

ZooKeeper 的 Cluster 模式中,允許我們同時啟動多個 ZooKeeper 服務。 在整個叢集中會選出一個Leader Server 負責整個節點的維護。

FollowerObserver 會將自己接收到的 Request 分發給 Leader 進行消費,在Leader 中會通過內部廣播將請求資訊通知回Follower

Cluster 模式和 Standalone 模式在節點模型方面沒有什麼區別,主要就是在RequestProcessor的任務鏈邏輯更加複雜,需要通過ProposalRequestProcessor將資料分發給叢集中的Follower,當大多數Follower都認可記錄後,才將Transaction Log 同步到 DataTree 中,他的處理細節會更加的複雜。