ZooKeeper原始碼解讀

爱吃麦辣鸡翅發表於2024-06-15

ZooKeeper原始碼分析

1. 伺服器構成

群首(leader),追隨者(follower),觀察者(observer)本質上都是伺服器。在實現伺服器主要抽象概念是請求處理器。請求處理器是對處理流水線上不同階段的抽象,每個伺服器實現一個請求處理器的序列。

zookeeper服務端有兩種模式:單機的獨立模式和叢集的仲裁模式,所謂仲裁是指一切事件只要滿足多數派同意就執行,不需要等到叢集中的每個節點反饋才執行。

獨立伺服器

1)PrepRequestProcessor 接受客戶端的請求並執行這個請求,處理結果則是生成一個事務。不過只有改變 ZooKeeper 狀態的操作才會產生事務,對於讀操作並不會產生任何事務。

2)SyncRequestProcessor 負責將事務持久化到磁碟上。實際上就是將事務資料按照順序追加到事務日誌中,並形成快照資料。

3)FinalRequestProcessor檢查如果 Request 物件包含事務資料,該處理器就會接受對 ZooKeeper 資料樹的修改,否則,該處理器會從資料樹中讀取資料並返回客戶端。

群首伺服器

在切換到仲裁模式時,伺服器的流水線則有一些變化。

image-20230831144408132

群首伺服器流水線:

第一個處理器同樣是 PrepRequestProcessor,而之後的處理器則為 ProposalRequestProcessor,該處理器會準備一個提議,並將該提議傳送給跟隨者,並且會把所有請求轉發給 CommitRequestProcessor,對於寫操作請求,還會把請求轉發給 SyncRequestProcessor 處理器。

SyncRequestProcessor 和獨立伺服器的功能一樣,是持久化事務到磁碟上,執行完後會觸發 AckRequestProcessor 處理器,它僅僅生成確認訊息並返回給自己。

CommitRequestProcessor 會將收到足夠多的確認訊息的提議進行提交。

追隨者和觀察者伺服器

Follower 伺服器是先從 FollowerRequestProcessors 處理器開始,該處理器接收並處理客戶端請求。如果是 讀請求 ,FollowerRequestProcessors 處理器之後轉發請求給 CommitRequestProcessor,CommitRequestProcessor 會直接轉發到 FinalRequestProcessor 處理器;如果是 寫請求 ,FollowerRequestProcessors 會將寫請求轉發到 CommitRequestProcessor 和群首伺服器,之後會轉發到 FinalRequestProcessor,在轉發到 FinalRequestProcessor 前會等待群首伺服器提交事務。而群首接收到一個新的寫請求時會生成一個提議,之後轉發到追隨者伺服器(PROPOSAL訊息),在收到提議後,追隨伺服器會傳送這個提議到 SyncRequestProcessor,SendRequestProcessor 會向群首傳送確認訊息(ACK訊息)。

當群首伺服器接收到足夠多確認訊息來提交這個提議,群首就會傳送提交事務訊息給追隨者(COMMIT訊息),當收到提交的事務訊息後,追隨者才會透過 CommitRequestProcessor 處理器進行處理。為了保證執行的順序,CommitRequestProcessor 處理器會在收到一個寫請求處理器時暫停後續的請求處理。

對於觀察者伺服器不需要確認提議訊息,因此觀察者伺服器並不需要傳送確認訊息給群首伺服器,一般情況下,也不用持久化事務到磁碟。對於觀察者伺服器是否持久化事務到磁碟,以便加速觀察者伺服器的恢復速度,可以根據具體情況決定。

2. 本地儲存

SyncRequestProcessor 處理器是用於處理提議寫入的日誌和快照。

日誌和磁碟的使用

伺服器透過事務日誌來持久化事務。在接受一個提議時(收到PROPOSAL訊息),一個伺服器就會將提議的事務持久化到事務日誌中,該事務日誌儲存在伺服器本地磁碟中,事務將會按照順序在末尾追加。寫事務日誌是寫請求操作的關鍵,因此 ZooKeeper 必須有效處理寫日誌問題。在持久化事務到磁碟時,還有一個重要說明:現代作業系統通常會快取髒頁(Dirty Page),並將他們非同步寫入磁碟。然而,我們需要在繼續之前,要確保事務已經被持久化。因此我們需要衝刷(Flush)事務到磁碟。

沖刷在這裡就是指我們告訴作業系統把髒頁寫入到磁碟,並在操作完成後返回。同時為了提高 ZooKeeper 系統的執行速度,也會使用組提交和補白的。其中組提交是指一次磁碟寫入時追加多個事務,可以減少磁碟定址的開銷。補白是指在檔案中預分配磁碟儲存塊。

快照

快照是 ZooKeeper 資料樹的複製副本,每一個伺服器會經常以序列化整個資料樹的方式來提取快照,並將這個提取的快照儲存到檔案。伺服器在進行快照時不需要進行協作,也不需要暫停處理請求。因此伺服器在進行快照時還會繼續處理請求,所以當快照完成時,資料樹可能又發生了變化,稱為快照是模糊的,因為它們不能反映出在任意給定的時間點資料樹的準確的狀態。

3. 伺服器與會話

會話(session)是 ZooKeeper 的一個重要的抽象。保證請求有序,臨時 znode 節點,監控點都與會話密切相關。因此會話的跟蹤機制對 ZooKeeper 來說也是非常重要的。

在獨立模式下,單個伺服器會跟蹤所有的會話,而在仲裁模式下則由群首伺服器來跟蹤和維護。而追隨者伺服器僅僅是簡單地把客戶端連線的會話資訊轉發到群首伺服器。

為了保證會話的存活,伺服器需要接收會話的心跳資訊。心跳的形式可以是一個新的請求或者顯式的 ping 資訊。兩種情況下,伺服器透過更新會話的過期時間來觸發會話活躍,在仲裁模式下,群首伺服器傳送一個 PING 資訊給它的追隨者們,追隨者們返回自從最新一次 PING 訊息之後的一個 session 列表。群首伺服器每半個時鐘週期就會傳送一個 ping 資訊給追隨者們。

4. 伺服器與監視點

監視點是由讀取操作所設定的一次性觸發器,每個監視點有一個特定操作來觸發,即透過監視點,客戶端可以對指定的 znode 節點註冊一個通知請求,在發生時就會收到一個單次的通知。監視點只會存在記憶體,而不會持久化到硬碟,當客戶端與服務端的連線斷開時,它的所有的監視點會從記憶體中清除。因為客戶端也會維護一份監視點的資料,在重連之後,監視點資料會再次同步到服務端。

5. 客戶端

在客戶端庫中有 2 個主要的類:ZooKeeper 和 ClientCnxn,寫客戶端應用程式時透過例項化 ZooKeeper 類來建立一個會話。一旦建立起一個會話,ZooKeeper 就會使用一個會話識別符號來關聯這個會話。這個會話識別符號實際上是有服務端所生產的。

ClientCnxn 類管理連線到 server 的 socket 連線。該類維護一個可連線的 ZooKeeper 的服務列表,並當連線斷掉的時候無縫地切換到其他伺服器,當重連到一個其他的伺服器時會使用同一個會話,客戶端也會重置所有的監視點到剛連線的伺服器上。

6. 群首選舉

群首為叢集中的伺服器選擇出來的一個伺服器,並會一直被叢集所認可。設定群首的目的是為了對客戶端所發起的 ZooKeeper 狀態更新請求進行排序,包括 create,setData 和 delete 操作。群首將每一個請求轉換為一個事務,將這些事務傳送給追隨者,確保叢集按照群首確定的順序接受並處理這些事務。

每個伺服器啟動後進入 LOOKING 狀態,開始選舉一個新的群首或者查詢已經存在的群首。如果群首已經存在,其他伺服器就會通知這個新啟動的伺服器,告知哪個伺服器是群首,於此同時,新伺服器會與群首建立連線,以確保自己的狀態與群首一致。如果群首中的所有的伺服器均處於 LOOKING 狀態,這些伺服器之間就會進行通訊來選舉一個群首,透過資訊交換對群首選舉達成共識的選擇。在本次選舉過程中勝出的伺服器將進入 LEADING 狀態,而叢集中其他伺服器將會進入 FOLLOWING 狀態。

具體看,一個伺服器進入 LOOKING 狀態,就會向叢集中每個伺服器傳送一個通知資訊,該訊息中包括該伺服器的投票(vote)資訊,投票中包含伺服器識別符號(sid)和最近執行事務的 zxid 資訊。

當一個伺服器收到一個投票資訊,該伺服器將會根據以下規則修改自己的投票資訊:

將接收的 voteId 和 voteZxid 作為一個識別符號,並獲取接收方當前的投票中的 zxid,用 myZxid 和 mySid 表示接收方伺服器自己的值。

如果(voteZxid > myZxid)或者(voteZxid == myZxid 且 voteId >mySid),保留當前的投票資訊。

否則,修改自己的投票資訊,將 voteZxid 賦值給 myZxid,將 voteId 賦值給 mySid。

從上面的投票過程可以看出,只有最新的伺服器將贏得選舉,因為其擁有最近一次的 zxid。如果多個伺服器擁有的最新的 zxid 值,其中的 sid 值最大的將會贏得選舉。(因為有些事物可能沒有同步到)

當一個伺服器連線到仲裁數量的伺服器發來的投票都一樣時,就表示群首選舉成功,如果被選舉的群首為某個伺服器自己,該伺服器將會開始行使群首角色,否則就會成為一個追隨者並嘗試連線被選舉的群首伺服器。一旦連線成功,追隨者和群首之間將會進行狀態同步,在同步完成後,追隨者才可以進行新的請求。

7. 序列化

對於網路傳輸和磁碟儲存的序列化訊息和事務,ZooKeeper 使用了 Hadoop 中的 Jute 來做序列化。

8. 服務端啟動過程

分散式模式的啟動主要經過 Leader 選舉,叢集資料同步,啟動伺服器。

分散式模式下的啟動過程包括如下階段:

  • 解析 config 檔案;
  • 資料恢復;
  • 監聽 client 連線(但還不能處理請求);
  • bind 選舉埠監聽 server 連線;
  • 選舉;
  • 初始化 ZooKeeperServer;
  • 資料同步;
  • 同步結束,啟動 client 請求處理能力。

zookeeper的啟動入口:

ZooKeeperMain.java客戶端主程式,ZooKeeperServer.java服務端單機主程式,QuorumPeerMain.java服務端分散式主程式

8.1 分散式服務啟動

透過配置檔案啟動

image-20230901133649008

解析配置檔案路徑,並把檔案讀到記憶體,最後給屬性賦值

image-20230901134028223

從property物件中解析配置鍵值對

image-20230901134211435

選舉演算法只支援這一個

image-20230901134348067

QuorumPeerMain類中的方法

DatadirCleanupManager 執行緒,由於 ZooKeeper 的任何一個變更操作都產生事務,事務日誌需要持久化到硬碟,同時當寫操作達到一定量或者一定時間間隔後,會對記憶體中的資料進行一次快照並寫入到硬碟上的 snapshop 中,快照為了縮短啟動時載入資料的時間從而加快整個系統啟動。而隨著執行時間的增長生成的 transaction log 和 snapshot 將越來越多,所以要定期清理,DatadirCleanupManager 就是啟動一個 TimeTask 定時任務用於清理 DataDir 中的 snapshot 及對應的 transaction log。

image-20230901135512206

進入runFromConfig

image-20230901141906592

配置完之後,初始化並啟動這個執行緒

image-20230901141207207

進入quorum.initialize()

image-20230901142135219

進入quorum.start()

這個方法包含5個start方法和一個loadDatabase方法

image-20230901161631776

  • getView()

image-20230901143847402

  • loadDatabase():構建dataTree

image-20230901144949570

  • startServerCnxnFactory()

ServerCnxn 這個類代表了一個客戶端與一個 server 的連線,每個客戶端連線過來都會被封裝成一個 ServerCnxn 例項用來維護了伺服器與客戶端之間的 Socket 通道。

image-20230901153212873

  • adminServer.start()

image-20230901153822571

AdminServer是一個內建的Jettry服務,它提供了一個HTTP介面為四字母單詞命令。預設的,服務被啟動在8080埠,並且命令被髮起透過URL "/commands/[command name]",例如,http://localhost:8080/commands/stat。命令響應以JSON的格式返回。為了檢視所有可用命令的列表,可以使用URL /commands (例如, http://localhost:8080/commands)。AdminServer預設開啟,但是可以被關閉透過下面的方法:設定java系統屬性zookeeper.admin.enableServer為false.

image-20230901154629631

  • startLeaderElection()

image-20230901161159120

Leader 選舉涉及到節點間的網路 IO,QuorumCnxManager 就是負責叢集中各節點的網路 IO,QuorumCnxManager 包含一個內部類 Listener,Listener 是一個執行緒,這裡啟動 Listener 執行緒,主要啟動選舉監聽埠並處理連線進來的 Socket;FastLeaderElection 就是封裝了具體選舉演算法的實現。

QuorumPeer.createElectionAlgorithm() ——> QuorumCnxManager.Listener.start() ——> QuorumCnxManager.Listener.ListenerHandler.run() ——> QuorumCnxManager.Listener.ListenerHandler.acceptConnections()

image-20230901163850384

    • listener:在選舉埠監聽
    • QuorumCnxManager負責處理選舉時的連線與資料交換

image-20230901170759059

listenerHandler的run

image-20230901170922821

image-20230904090718579

image-20230904140020443

image-20230904110929623

image-20230904134532334

connectOne將在下面單獨介紹

image-20230904141216149

image-20230911083816092

如果佇列中沒有要傳送的內容,那麼我們傳送lastMessage以確保對等端接收到最後一條訊息,對等方可以正確處理重複的訊息。如果傳送佇列不是空的,那麼我們有一個比lastMessage中儲存的訊息更新的訊息。為了避免傳送過時的訊息,我們應該在傳送佇列中傳送訊息。

image-20230904133114619

image-20230904133600527

image-20230904133342032

image-20230906090607459

QuorumCnxManager結束

    • FastLeaderElection.start

image-20230901173002857

fle中的WorkerSender和WorkerRecvier阻塞從佇列中拉取/加入訊息,由QuorumCnxManager中的RecvWorker和SendWorker負責實現位元組流寫入與讀取

image-20230906091532181

image-20230906091722151

image-20230901173703900

image-20230906105257040

  • startJvmPauseMonitor()

    JVMPauseMonitor開啟一個內部守護執行緒,JVMMonitor是JVMPauseMonitor的內部類,他也是一個執行緒

image-20230901151942712

JVM暫停監視器

這是一個監視JVM暫停情況的服務。該服務建立一個簡單的執行緒。在此執行緒中,在迴圈中執行sleep一段時間方法,如果sleep花費的時間比傳遞給sleep方法的時間長(執行sleep500ms但是實際上用了1000ms),就意味著JVM或者宿主機已經出現了停頓處理現象,可能會導致其它問題,如果這種停頓被監測出來達到一定的閾值,執行緒會列印相應級別的訊息。還可以把額外sleep的時間進行metrics統計,外部的監控系統可以監控zookeeper的健康狀態以及進行報警。

image-20230901151415123

image-20230904145217197

  • super.start :執行QuorumPeer的run方法。

    QuorumPeer 執行緒進入到一個無限迴圈模式,不停的透過 getPeerState 方法獲取當前節點狀態,然後執行相應的分支邏輯。大致流程可以簡單描述如下:

    1. 首先系統剛啟動時 serverState 預設是 LOOKING,表示需要進行 Leader 選舉,這時進入 Leader 選舉狀態中,會呼叫 FastLeaderElection.lookForLeader 方法,lookForLeader 方法內部也包含了一個迴圈邏輯,直到選舉出 Leader 才會跳出 lookForLeader 方法,如果選舉出的 Leader 就是本節點,則將 serverState=LEADING 賦值,否則設定成 FOLLOWING 或 OBSERVING。
    2. 然後 QuorumPeer.run 進行下一輪次迴圈,透過 getPeerState 獲取當前 serverState 狀態,如果是 LEADING,則表示當前節點當選為 LEADER,則進入 Leader 角色分支流程,執行作為一個 Leader 該乾的任務;如果是 FOLLOWING 或 OBSERVING,則進入 Follower 或 Observer 角色,並執行其相應的任務。注意:進入分支路程會一直阻塞在其分支中,直到角色轉變才會重新進行下一輪次迴圈,比如 Follower 監控到無法與 Leader 保持通訊了,會將 serverState 賦值為 LOOKING,跳出分支並進行下一輪次迴圈,這時就會進入 LOOKING 分支中重新進行 Leader 選舉。

image-20230904163449432

image-20230904163403939

QuorumCnxManager 有一個內部類 Listener,初始化一個 ServerSocket,然後在一個 while 迴圈中呼叫 accept 接收客戶端(注意:這裡的客戶端指的是叢集中其它伺服器)連線。當有客戶端連線進來後,會將該客戶端 Socket 封裝成 RecvWorker 和 SendWorker,它們都是執行緒,分別負責和該 Socket 所代表的客戶端進行讀寫。其中,RecvWorker 和 SendWorker 是成對出現的,每對負責維護和叢集中的一臺伺服器進行網路 IO 通訊。

FastLeaderElection 負責 Leader 選舉核心規則演算法實現,包含了兩個內部類 WorkerSender 和 WorkerReceiver 執行緒。

FastLeaderElection 中進行選舉時廣播投票資訊時,將投票資訊寫入到對端伺服器大致流程如下:

  1. 將資料封裝成 ToSend 格式放入到 sendqueue;
  2. WorkerSender 執行緒會一直輪詢提取 sendqueue 中的資料,當提取到 ToSend 資料後,會獲取到叢集中所有參與 Leader 選舉節點(除 Observer 節點外的節點)的 sid,如果 sid 即為本機節點,則轉成 Notification 直接放入到 recvqueue 中,因為本機不再需要走網路 IO;否則放入到 queueSendMap 中,key 是要傳送給哪個伺服器節點的 sid,ByteBuffer 即為 ToSend 的內容,queueSendMap 維護的著當前節點要傳送的網路資料資訊,由於傳送到同一個 sid 伺服器可能存在多條資料,所以 queueSendMap 的 value 是一個 queue 型別;
  3. QuorumCnxManager 中的 SendWorkder 執行緒不停輪詢 queueSendMap 中是否存在自己要傳送的資料,每個 SendWorkder 執行緒都會繫結一個 sid 用於標記該 SendWorkder 執行緒和哪個對端伺服器進行通訊,因此,queueSendMap.get(sid)即可獲取該執行緒要傳送資料的 queue,然後透過 queue.poll()即可提取該執行緒要傳送的資料內容;
  4. 然後透過呼叫 SendWorkder 內部維護的 socket 輸出流即可將資料寫入到對端伺服器。

FastLeaderElection 中進行選舉時廣播投票資訊時,從對端伺服器讀取投票資訊的大致流程如下:

  • QuorumCnxManager 中的 RecvWorker 執行緒會一直從 Socket 的輸入流中讀取資料,當讀取到對端傳送過來的資料時,轉成 Message 格式並放入到 recvQueue 中;
  • FastLeaderElection.WorkerReceiver 執行緒會輪詢方式從 recvQueue 提取資料並轉成 Notification 格式放入到 recvqueue 中;
  • FastLeaderElection 從 recvqueu 提取所有的投票資訊進行比較 最終選出一個 Leader。

更新自己期望投票資訊,即自己期望選哪個伺服器作為 Leader(用 sid 代替期望伺服器節點)以及該伺服器 zxid、epoch 等資訊,第一次投票預設都是投自己當選 Leader,然後呼叫 sendNotifications 方法廣播該投票到叢集中所有可以參與投票伺服器,廣播涉及到網路 IO 流程前面已講解,這裡就不再細說;

image-20230905102704694

image-20230905101915118

8.2 主要選舉邏輯

image-20230905102109041

image-20230905143354138

最後將新選票放入到recvset投票箱中,並判斷投票箱中的投票是否有超過一半已經和自身的選票內容一致,如果未超過一半則再次重新進行上面選舉流程,如果已經達到一半,則進行最後的判斷,把recvqueue中的投票資訊全部取出來進行判斷,判斷是否還存在優於當前自身選票的投票訊息,如果有的話,則將當前選票重新放入recvqueue中,重新進行選舉流程,沒有的話則直接結束選舉.

image-20230905144617315

image-20230905171351166

image-20230905171609189

image-20230905101411253

image-20230905092521886

首先對之前提到的選舉輪次 electionEpoch 進行判斷,這裡分為三種情況:

  • 只有對方發過來的投票的 electionEpoch 和當前節點相等表示是同一輪投票,即投票有效,然後呼叫 totalOrderPredicate()對投票進行 PK,返回 true 代表對端勝出,則表示第一次投票是錯誤的(第一次都是投給自己),更新自己投票期望對端為 Leader,然後呼叫 sendNotifications()將自己最新的投票廣播出去。返回 false 則代表自己勝出,第一次投票沒有問題,就不用管。
  • 如果對端發過來的 electionEpoch 大於自己,則表明重置自己的 electionEpoch,然後清空之前獲取到的所有投票 recvset,因為之前獲取的投票輪次落後於當前則代表之前的投票已經無效了,然後呼叫 totalOrderPredicate()將當前期望的投票和對端投票進行 PK,用勝出者更新當前期望投票,然後呼叫 sendNotifications()將自己期望頭破廣播出去。注意:這裡不管哪一方勝出,都需要廣播出去,而不是步驟 a 中己方勝出不需要廣播,這是因為由於 electionEpoch 落後導致之前發出的所有投票都是無效的,所以這裡需要重新傳送
  • 如果對端發過來的 electionEpoch 小於自己,則表示對方投票無效,直接忽略不進行處理
  • 最後將新選票放入到recvset投票箱中,並判斷投票箱中的投票是否有超過一半已經和自身的選票內容一致,如果未超過一半則再次重新進行上面選舉流程,如果已經達到一半,則進行最後的判斷,把recvqueue中的投票資訊全部取出來進行判斷,判斷是否還存在優於當前自身選票的投票訊息,如果有的話,則將當前選票重新放入recvqueue中,重新進行選舉流程,沒有的話則直接結束選舉.

8.2.1 OBSERVING狀態邏輯

進入這個分支,證明已經選舉結束,進入觀察者角色,將當前對等端設定為觀察者,然後執行觀察者的任務

image-20230906152401401

makeOberver

image-20230906152625981

obsereLeader

image-20230906162944650

image-20230906163054159

根據當前投票結果找出leader地址

image-20230906155027620

8.2.2 FOLLOWING狀態邏輯

image-20230907142219879

image-20230906164122484

image-20230907141851837

zabState在各節點初始化時為discovery,節點同步leader資料時為synchronization,節點準備就緒時為broadcast

image-20230907095112696

image-20230907134235712

syncWithLeader:

image-20230907111623966

image-20230907112431737

image-20230907140639528

image-20230907134125929

image-20230907140320494

followLeader中的:

image-20230907132228331

image-20230907132610822

image-20230907132701266

8.2.3 LEADING狀態邏輯

image-20230907084406065

image-20230907091629023

image-20230907152403977

image-20230907160222186

對於 Follower 和 Observer 而言,主要的初始化工作是要建立與 Leader 的連線並同步 epoch 資訊,最後完成與 Leader 的資料同步。而 Leader 會啟動 LearnerCnxAcceptor 執行緒,該執行緒會接受來自 Follower 和 Observer(統稱為 Learner)的連線請求併為每個連線建立一個 LearnerHandler 執行緒,該執行緒會負責包括資料同步在內的與 learner 的一切通訊。learnerHandler執行資料包的傳送,會有個queuedPackets佇列,執行sendPackets迴圈將資料包出隊傳送。

Learner(Follower 或 Observer)節點會主動向 Leader 發起連線,ZooKeeper 就會進入叢集同步階段,叢集同步主要完成叢集中各節點狀態資訊和資料資訊的一致。選出新的 Leader 後的流程大致分為:計算 epoch、統一 epoch、同步資料、廣播模式等四個階段。其中其前三個階段:計算 epoch、統一 epoch、同步資料就是這一節主要介紹的叢集同步階段的主要內容,這三個階段主要完成新 Leader 與叢集中的節點完成同步工作,處於這個階段的 zk 叢集還沒有真正做好對外提供服務的能力,可以看著是新 leader 上任後進行的內部溝通、前期準備工作等,只有等這三個階段全部完成,新 leader 才會真正的成為 leader,這時 zk 叢集會恢復正常可執行狀態並對外提供服務。

lead方法的流程:

image-20230907153204594

image-20230907153235790

eader 分支大致可以分為 5 個階段:啟動 LearnerCnxAcceptor 執行緒、計算 newEpoch、廣播 newEpoch、資料同步和叢集狀態監測。

Leader.lead()方法控制著 Leader 角色節點的主體流程,其實現較為簡單,大致模式都是透過阻塞方法阻塞當前執行緒,直到該階段完成 Leader 執行緒才會被喚醒繼續執行下一個階段;而每個階段實現的具體細節及大量的網路 IO 操作等都在 LearnerHandler 中實現。比如計算 newEpoch,Leader 中只會判斷 newEpoch 計算完成沒,沒有計算完成就會進入阻塞狀態掛起當前 Leader 執行緒,直到叢集中一半以上的節點同步了 epoch 資訊後 newEpoch 正式產生才會喚醒 Leader 執行緒繼續向下執行;而計算 newEpoch 會涉及到 Leader 去收集叢集中大部分 Learner 伺服器的 epoch 資訊,會涉及到大量的網路 IO 通訊等內容,這些細節部分都在 LearnerHandler 中實現。

配置中“server.0=10.80.8.3:2888:2999”這裡的 2999 就是叢集選舉埠,2888就是叢集同步埠;

啟動 LearnerCnxAcceptor 執行緒

Leader 首先會啟動一個 LearnerCnxAcceptor 執行緒,這個執行緒不停的迴圈 accept 接收 Learner 端的網路請求(這裡的監聽埠就是上面說的同步監聽埠,而不是選舉埠),Leader 選舉結束後被分配為 Follower 或 Observer 角色的節點會主動向 Leader 發起連線,Leader 端接收到一個網路連線就會封裝成一個 LearnerHandler 執行緒。

Leader 類可以看成一個總管,和每個 Learner 伺服器的互動任務都會被分派給 LearnerHandler 這個助手完成,當 Leader 檢測到一個任務被一半以上的 LearnerHandler 處理完成,即認為該階段結束,進入下一個階段。

計算 epoch

epoch 在 ZooKeeper 中是一個很重要的概念,前面也介紹過了:epoch 就相當於 Leader 的身份編號,就如同身份證編號一樣,每次選舉產生一個新 Leader 時,都會為該 Leader 重新計算出一個新 epoch。epoch 被設計成一個遞增值,比如上一個 Leader 的 epoch 是 1,假如重新選舉新的 Leader 就會被分配 epoch+1。

epoch 作用:可以防止舊 Leader 活過來後繼續廣播之前舊提議造成狀態不一致問題,只有當前 Leader 的提議才會被 Follower 處理。ZooKeeper 叢集所有的事務請求操作都要提交由 Leader 伺服器完成,Leader 伺服器將事務請求轉成一個提議(Proposal)並分配一個事務 ID(zxid)後廣播給 Learner,zxid 就是由 epoch 和 counter(遞增)組成,當存在舊 leader 向 follower 傳送命令的時候,follower 發現 zxid 所在的 epoch 比當前的小,則直接拒絕,防止出現不一致性。

統一 epoch

newEpoch 計算完成後,該值只有 Leader 知道,現在需要將 newEpoch 廣播到叢集中所有的伺服器節點上,讓他們都更新下新 Leader 的 epoch 資訊,這樣他們在處理請求時會根據 epoch 判斷該請求是不是當前新 Leader 發出的,可以防止舊 Leader 活過來後繼續廣播之前舊提議造成狀態不一致問題,只有當前 Leader 的提議才會被 Follower 處理。

總結:廣播 newEpoch 流程也比較簡單,就是將之前計算出來的 newEpoch 封裝到 LEADERINFO 資料包中,然後廣播到叢集中的所有節點,同時會收到 ACKEPOCH 回覆資料包,當叢集中一半以上的節點進行了回覆則可以認為 newEpoch 廣播完成,則進入下一階段。同樣,為避免執行緒一直阻塞,休眠執行緒依然會被新增超時時間,超時後仍未完成則丟擲 InterruptedException 異常重新進入 Leader 選舉狀態。

image-20230907164718597

資料同步

之前分析過 Leader 的選舉策略:lastZxid 越大越會被優先選為 Leader。lastZxid 是節點上最大的事務 ID,由於 zxid 是遞增的,lastZxid 越大,則表示該節點處理的資料越新,即資料越完整。所以,被選為 Leader 的節點資料完整性越高,為了資料一致性,這時就需要其它節點和 Leader 進行資料同步保持資料一致性。

資料同步四種情況:

  • DIFF,learner 比 leader 少一些資料;
  • TRUNC,learner 資料比 leader 多;
  • DIFF+TRUNC,learner 對 leader 多資料又少資料;
  • SNAP,learner 比 leader 少很多資料。

8.3 請求處理器

請求處理器是對處理流水線上不同階段的抽象,每個伺服器在初始化時實現一個請求處理器的序列。對於請求處理器,ZooKeeper 程式碼裡有一個叫 RequestProcessor 的介面,這個介面的主要方法是processRequest,它接受一個 Request 引數,在一個請求處理器的流水線中,對於相鄰處理器的請求的處理是透過佇列實現解耦合。當一個處理器有一條請求需要下一個處理器進行處理時,它將這條請求加入佇列中。然後,它將處於等待狀態直到下一個處理器處理完此訊息。本節主要看看各個伺服器的請求處理器序列初始化和對佇列的使用與處理,處理器的細節可以參考原始碼。

image-20230908105314159

8.3.1 leader的請求處理器

leader.lead() —— startZkServer() —— zk.startup() —— ZooKeeper.startupWithServerState(State.RUNNING) —— setupRequestProcessors()

LeaderZooKeeperServer:

image-20230908102337192

image-20230908110155906

CommitProcessor.run:

image-20230908105926376

除了finalProcessor,每個processor都會存一個next指向下一個處理器

image-20230908105819906

8.3.2 follower的請求處理器

image-20230908110605002

8.3.3 observer的請求處理器

image-20230908111021316

9 客戶端啟動流程

從整體看,客戶端啟動的入口時 ZooKeeperMain,在 ZooKeeperMain 的 run()中,建立出控制檯輸入物件(jline.console.ConsoleReader),然後它進入 while 迴圈,等待使用者的輸入。同時也呼叫 connectToZK 連線伺服器並建立會話(session),在 connect 時建立 ZooKeeper 物件,在 ZooKeeper 的建構函式中會建立客戶端使用的 NIO socket,並啟動兩個工作執行緒 sendThread 和 eventThread,兩個執行緒被初始化為守護執行緒。

sendThread 的 run()是一個無限迴圈,除非運到了 close 的條件,否則他就會一直迴圈下去,比如向服務端傳送心跳,或者向服務端傳送我們在控制檯輸入的資料以及接受服務端傳送過來的響應。

eventThread 執行緒負責佇列事件和處理 watch。

客戶端也會建立一個 clientCnxn,由 ClientCnxnSocketNIO.java 負責 IO 資料通訊。

image-20230908111609945

客戶端入口方法:ZookeeperMain.main()

image-20230908133708202

image-20230908131813694

ZooKeeperAdmin繼承ZooKeeper

image-20230908141623527

image-20230908142652524

createConnection會返回一個ClientCnxn物件,ClientCnxn中有兩個重要的資料結構:

image-20230908142935423

image-20230908150820929

ZooKeeper 類會將使用者的輸入引數轉換為對 ZK 操作,呼叫 cnxn.submitRequest()提交請求,在 ClientCnxn 中會把請求封裝為 Packet 並寫入 outgoingQueue,待 sendThread 執行緒消費傳送給服務端,呼叫 cnxn.submitRequest()會阻塞,其中客戶端等待是自旋鎖。

image-20230908155117331

傳送執行緒ClientCnxn.SendThread#run中,會迴圈執行doTransport

image-20230911152115626

其實現ClientCnxnSocketNIO.doTransport 呼叫 dcIO()

image-20230911152856254

isReadable 邏輯中,會呼叫 sendThread.readResponse(), 在 sendThread.readResponse()函式中的 finally 中呼叫 finshPacket()設定 finished 為 true,進而客戶端阻塞解除,返回讀取結果。

image-20230908160253339

image-20230908160324734

image-20230908160652886

image-20230908160740636

在doIO處理時,會根據 sockKey 判斷客戶端發出的讀操作還是寫操作,對於寫操作

image-20230911151025152

sun.nio.ch.SocketChannelImpl#write(java.nio.ByteBuffer):

image-20230911151104605

10 服務端和客戶端結合部分

10.1 會話(Session)

Client 建立會話的流程如下,

  1. 服務端啟動,客戶端啟動;
  2. 客戶端發起 socket 連線;
  3. 服務端 accept socket 連線,socket 連線建立;
  4. 客戶端傳送 ConnectRequest 給 server;
  5. server 收到後初始化 ServerCnxn,代表一個和客戶端的連線,即 session,server 傳送 ConnectResponse 給 client;
  6. client 處理 ConnectResponse,session 建立完成。

10.1.1 客戶端

在 clientCnxn.java 中,run 是一個 while 迴圈,只要 client 沒有被關閉會一直迴圈,每次迴圈判斷當前 client 是否連線到 server,如果沒有則發起連線,發起連線呼叫了 startConnect。

image-20230908163544837

image-20230908163704146

image-20230908163804411

image-20230908163837810

image-2023090816504971010.1.2 服務端

server 在啟動後,會暴露給客戶端連線的地址和埠以提供服務。先看一下NIOServerCnxnFactory,主要是啟動三個執行緒。

  1. AcceptThread:用於接收 client 的連線請求,建立連線後交給 SelectorThread 執行緒處理
  2. SelectorThread:用於處理讀寫請求
  3. ConnectionExpirerThread:檢查 session 連線是否過期

image-20230908170812125

image-20230908170844921

image-20230908171114538

client 發起 socket 連線的時候,server 監聽了該埠,AcceptThread 接收到 client 的連線請求,然後把建立連線的 SocketChannel 放入佇列裡面,交給 SelectorThread 處理。

SelectorThread 是一個不斷迴圈的執行緒,每次迴圈都會處理剛剛建立的 socket 連線。

session生成演算法:

image-20230908171617561

10.2 監視器(Watcher)

ZooKeeper 可以定義不同型別的通知,如監控 znode 的資料變化,監控 znode 子節點的變化,監控 znode 的建立或者刪除。ZooKeeper 的服務端實現了監視點管理器(watch manager)。

一個 WatchManager 類的例項負責管理當前已經註冊的監視點列表,並負責觸發他們,監視點只會存在記憶體且為本地服務端的概念,所有型別的伺服器都是使用同樣的方式處理監控點。

DataTree 類中持有一個監視點管理器來負責子節點監控和資料的監控。

在服務端觸發一個監視點,最終會傳播到客戶端,負責處理傳播的為服務端的 cnxn 物件(ServerCnxn 類),此物件表示客戶端和服務端的連線並實現了 Watcher 介面。Watch.process 方法序列化了監視點事件為一定的格式,以便於網路傳送。ZooKeeper 客戶端接收序列化的監視點事件,並將其反序列化為監控點事件的物件,並傳遞給應用程式。

以getData為例說明

Watcher 介面的定義,如果設定了監視點,我們要實現 process 函式。

客戶端watcher註冊:

在客戶端 GetData 時,如果註冊 watch 監控點到服務端,在 watch 的 path 的 value 變化時,服務端會通知客戶端該變化。

在客戶端的 GetData 方法中(ZooKeeper 類的 GetData):

  • 建立 WatchRegistration wcb= new DataWatchRegistration(watcher, clientPath),path 和 watch 封裝進了一個物件;
  • 建立一個 request,設定 type 、path、watch;
  • request.setWatch(watcher != null),是否設定了watch。
  • 呼叫 ClientCnxn.submitRequest(...) , 將請求包裝為 Packet,queuePacket()方法的引數中存在建立的 path+watcher 的封裝類 WatchRegistration,請求會被 sendThread 消費傳送到服務端。

image-20230911093748287

org.apache.zookeeper.ClientCnxn#submitRequest:

image-20230911141543720

queuePacket方法就是建立Packet並賦值,然後將其加入待傳送佇列

在SendThread中會迴圈執行doTransport,也就是會迴圈執行到readResponse,其又會執行對應packet的finishPacket方法,在finishPacket中會註冊watch,並且會置finished為true,使submitRequest方法停止阻塞

image-20230908160652886

image-20230911160335966

服務端watcher註冊:

直接看 FinalRequestProcessor 處理器的 public void processRequest(Request request){}方法,看它針對 GetData()方式的請求做出了哪些動作。

zks.getZKDatabase().getData(getDataRequest.getPath(), stat, getDataRequest.getWatch() ? cnxn : null)

根據 watcher 的“有無”給服務端新增不同的 Watcher。服務端 GetData()函式,在服務端維護了一份 path+watcher 的 map,如果設定 watcher,服務端會儲存該 path 的 watcher。

FinalRequestProcessor#processRequest:

image-20230911101306630

ZKDatabase#getData:

image-20230911101626599

DataTree#getData:

image-20230911102315777

WatchManager#addWatch:

image-20230911102559002

客戶端watcher觸發:

當在客戶端輸入set /path newValue時,會呼叫setData方法

image-20230911103420030

服務端watcher觸發:

從 SetData 的原始碼看,本次的 submitRequest 引數中,WatchRegistration=null,可以推斷,服務端在 FinalRequestProcessor 中再處理時取出的 watcher=null,也就不會將 path+watcher 儲存進 maptable 中,其他的處理過程和上面 GetData 類似。(因為getData也是在processRequest中將watch加入到maptable)

FinalRequestProcessor#processRequest:

image-20230911165437785

在服務端事務處理的processTxn方法中, 最終會呼叫到dataTree的processTxn方法,processTxn會將請求頭的zxid,cxid,type,clientId原封不動的賦值給rc

image-20230911165847963

會呼叫到dataTree的SetData()函式,Set 數值後,會觸發 watch 回撥,即 triggerWatch()。

image-20230911170129028

image-20230911170430055

org.apache.zookeeper.server.watch.WatchManager#triggerWatch:

image-20230912111633774

image-20230912111138188

watcher的mode分為:STANDARD、PERSISTENT、PERSISTENT_RECURSIVE,預設為標準

從watchTable中取出path對應的watcher,並透過 cnxn 的 process()方法處理(NIOServerCnxn 類)通知到客戶端。響應頭設定為NOTIFICATION_XID,此處與客戶端對應。

image-20230911174209391

傳送響應到客戶端,觸發服務端的watcher是手動傳送響應,而不是加入到傳送佇列

image-20230911174522459

客戶端回撥邏輯:

客戶端使用 SendThread.readResponse() 方法來統一處理服務端的響應。

image-20230911175336791

org.apache.zookeeper.ClientCnxn.EventThread#queueEvent:

image-20230912083800072

將查詢到的Watcher儲存到waitingEvents佇列中,呼叫EventThread類中的run方法會迴圈取出在waitingEvents佇列中等待的Watcher事件進行處理。
處理的過程就是呼叫watcher介面的process()介面:

image-20230912090304972

image-20230912084651207

至此,zookeeper客戶端和服務端的部分原始碼解讀結束。


本部落格內容僅供個人學習使用,禁止用於商業用途。轉載需註明出處並連結至原文。

相關文章