Apache Dubbo 原始碼搭建與解讀(八)—— Dubbo 註冊中心之ZooKeeper

D_A_I_H_A_O發表於2020-10-15

Dubbo Provider 在啟動時會將自身的服務資訊整理成 URL 註冊到註冊中心,Dubbo Consumer 在啟動時會向註冊中心訂閱感興趣的 Provider 資訊,之後 Provider 和 Consumer 才能建立連線,進行後續的互動。可見,一個穩定、高效的註冊中心對基於 Dubbo 的微服務來說是至關重要的。

Dubbo 註冊中心

Dubbo 目前支援 Consul、etcd、Nacos、ZooKeeper、Redis 等多種開源元件作為註冊中心,並且在 Dubbo 原始碼也有相應的接入模組,如下圖所示:

在這裡插入圖片描述
Dubbo 官方推薦使用 ZooKeeper 作為註冊中心,它是在實際生產中最常用的註冊中心實現

要與 ZooKeeper 叢集進行互動,我們可以使用 ZooKeeper 原生客戶端或是 ZkClient、Apache Curator 等第三方開源客戶端。在後面介紹 dubbo-registry-zookeeper 模組的具體實現時你會看到,Dubbo 底層使用的是 Apache Curator。Apache Curator 是實踐中最常用的 ZooKeeper 客戶端。

ZooKeeper 核心概念

Apache ZooKeeper 是一個針對分散式系統的可靠的可擴充套件的協調服務,它通常作為統一命名服務、統一配置管理、註冊中心(分散式叢集管理)、分散式鎖服務、Leader 選舉服務等角色出現。很多分散式系統都依賴與 ZooKeeper 叢集實現分散式系統間的協調排程,例如:Dubbo、HDFS 2.x、HBase、Kafka 等。ZooKeeper 已經成為現代分散式系統的標配。

ZooKeeper 本身也是一個分散式應用程式,下圖展示了 ZooKeeper 叢集的核心架構

在這裡插入圖片描述

  • Client 節點:從業務角度來看,這是分散式應用中的一個節點,通過 ZkClient 或是其他 ZooKeeper 客戶端與 ZooKeeper 叢集中的一個 Server 例項維持長連線,並定時傳送心跳。從 ZooKeeper 叢集的角度來看,它是 ZooKeeper 叢集的一個客戶端,可以主動查詢或操作 ZooKeeper 叢集中的資料,也可以在某些 ZooKeeper 節點(ZNode)上新增監聽。當被監聽的 ZNode 節點發生變化時,例如,該 ZNode 節點被刪除、新增子節點或是其中資料被修改等,ZooKeeper 叢集都會立即通過長連線通知 Client。

  • Leader 節點:ZooKeeper 叢集的主節點,負責整個 ZooKeeper 叢集的寫操作,保證叢集內事務處理的順序性。同時,還要負責整個叢集中所有 Follower 節點與 Observer 節點的資料同步。

  • Follower 節點:ZooKeeper 叢集中的從節點,可以接收 Client 讀請求並向 Client 返回結果,並不處理寫請求,而是轉發到 Leader 節點完成寫入操作。另外,Follower 節點還會參與 Leader 節點的選舉。

  • Observer 節點:ZooKeeper 叢集中特殊的從節點,不會參與 Leader 節點的選舉,其他功能與 Follower 節點相同。引入 Observer 角色的目的是增加 ZooKeeper 叢集讀操作的吞吐量,如果單純依靠增加 Follower 節點來提高 ZooKeeper 的讀吞吐量,那麼有一個很嚴重的副作用,就是 ZooKeeper 叢集的寫能力會大大降低,因為 ZooKeeper 寫資料時需要 Leader 將寫操作同步給半數以上的 Follower 節點。引入 Observer 節點使得 ZooKeeper 叢集在寫能力不降低的情況下,大大提升了讀操作的吞吐量。

ZooKeeper 叢集儲存資料的邏輯結構。

ZooKeeper 邏輯上是按照樹型結構進行資料儲存的(如下圖),其中的節點稱為 ZNode。每個 ZNode 有一個名稱標識,即樹根到該節點的路徑(用 “/” 分隔),ZooKeeper 樹中的每個節點都可以擁有子節點,這與檔案系統的目錄樹類似。

在這裡插入圖片描述
ZNode 節點型別有如下四種:

  • 持久節點。 持久節點建立後,會一直存在,不會因建立該節點的 Client 會話失效而刪除。

  • 持久順序節點。 持久順序節點的基本特性與持久節點一致,建立節點的過程中,ZooKeeper 會在其名字後自動追加一個單調增長的數字字尾,作為新的節點名。

  • 臨時節點。 建立臨時節點的 ZooKeeper Client 會話失效之後,其建立的臨時節點會被 ZooKeeper 叢集自動刪除。與持久節點的另一點區別是,臨時節點下面不能再建立子節點。

  • 臨時順序節點。 基本特性與臨時節點一致,建立節點的過程中,ZooKeeper 會在其名字後自動追加一個單調增長的數字字尾,作為新的節點名

在每個 ZNode 中都維護著一個 stat 結構,記錄了該 ZNode 的後設資料,其中包括版本號、操作控制列表(ACL)、時間戳和資料長度等資訊,如下表所示:

在這裡插入圖片描述
我們除了可以通過 ZooKeeper Client 對 ZNode 進行增刪改查等基本操作,還可以註冊 Watcher 監聽 ZNode 節點、其中的資料以及子節點的變化。一旦監聽到變化,則相應的 Watcher 即被觸發,相應的 ZooKeeper Client 會立即得到通知。Watcher 有如下特點:

1、主動推送。 Watcher 被觸發時,由 ZooKeeper 叢集主動將更新推送給客戶端,而不需要客戶端輪詢。

2、一次性。 資料變化時,Watcher 只會被觸發一次。如果客戶端想得到後續更新的通知,必須要在 Watcher 被觸發後重新註冊一個 Watcher。

3、可見性。 如果一個客戶端在讀請求中附帶 Watcher,Watcher 被觸發的同時再次讀取資料,客戶端在得到 Watcher 訊息之前肯定不可能看到更新後的資料。換句話說,更新通知先於更新結果。

4、順序性。 如果多個更新觸發了多個 Watcher ,那 Watcher 被觸發的順序與更新順序一致

訊息廣播流程概述

ZooKeeper 叢集中三種角色的節點(Leader、Follower 和 Observer)都可以處理 Client 的讀請求,因為每個節點都儲存了相同的資料副本,直接進行讀取即可返回給 Client。

對於寫請求,如果 Client 連線的是 Follower 節點(或 Observer 節點),則在 Follower 節點(或 Observer 節點)收到寫請求將會被轉發到 Leader 節點。下面是 Leader 處理寫請求的核心流程

1、Leader 節點接收寫請求後,會為寫請求賦予一個全域性唯一的 zxid(64 位自增 id),通過 zxid 的大小比較就可以實現寫操作的順序一致性。

2、Leader 通過先進先出佇列(會給每個 Follower 節點都建立一個佇列,保證傳送的順序性),將帶有 zxid 的訊息作為一個 proposal(提案)分發給所有 Follower 節點。

3、當 Follower 節點接收到 proposal 之後,會先將 proposal 寫到本地事務日誌,寫事務成功後再向 Leader 節點回一個 ACK 響應。

4、當 Leader 節點接收到過半 Follower 的 ACK 響應之後,Leader 節點就向所有 Follower 節點傳送 COMMIT 命令,並在本地執行提交。

5、當 Follower 收到訊息的 COMMIT 命令之後也會提交操作,寫操作到此完成。

6、最後,Follower 節點會返回 Client 寫請求相應的響應。

寫操作的核心流程:

在這裡插入圖片描述

崩潰恢復

上面寫請求處理流程中,如果發生 Leader 節點當機,整個 ZooKeeper 叢集可能處於如下兩種狀態:

1、當 Leader 節點收到半數以上 Follower 節點的 ACK 響應之後,會向各個 Follower 節點廣播 COMMIT 命令,同時也會在本地執行 COMMIT 並向連線的客戶端進行響應。如果在各個 Follower 收到 COMMIT 命令前 Leader 就當機了,就會導致剩下的伺服器沒法執行這條訊息。

2、當 Leader 節點生成 proposal 之後就當機了,而其他 Follower 並沒有收到此 proposal(或者只有一小部分 Follower 節點收到了這條 proposal),那麼此次寫操作就是執行失敗的。

在 Leader 當機後,ZooKeeper 會進入崩潰恢復模式,重新進行 Leader 節點的選舉。

ZooKeeper 對新 Leader 有如下兩個要求:

1、對於原 Leader 已經提交了的 proposal,新 Leader 必須能夠廣播並提交,這樣就需要選擇擁有最大 zxid 值的節點作為 Leader。

2、對於原 Leader 還未廣播或只部分廣播成功的 proposal,新 Leader 能夠通知原 Leader 和已經同步了的 Follower 刪除,從而保證叢集資料的一致性。

ZooKeeper 選主的大致流程

  • ZooKeeper 選主使用的是 ZAB 協議

  • 比如,當前叢集中有 5 個 ZooKeeper 節點構成,sid 分別為 1、2、3、4 和 5,zxid 分別為 10、10、9、9 和 8,此時,sid 為 1 的節點是 Leader 節點。實際上,zxid 包含了 epoch(高 32 位)和自增計數器(低 32 位) 兩部分。其中,epoch 是“紀元”的意思,標識當前 Leader 週期,每次選舉時 epoch 部分都會遞增,這就防止了網路隔離之後,上一週期的舊 Leader 重新連入叢集造成不必要的重新選舉。該示例中我們假設各個節點的 epoch 都相同。

  • 某一時刻,節點 1 的伺服器當機了,ZooKeeper 叢集開始進行選主。由於無法檢測到叢集中其他節點的狀態資訊(處於 Looking 狀態),因此每個節點都將自己作為被選舉的物件來進行投票。於是 sid 為 2、3、4、5 的節點,投票情況分別為(2,10)、(3,9)、(4,9)、(5,8),同時各個節點也會接收到來自其他節點的投票(這裡以(sid, zxid)的形式來標識一次投票資訊)。

  • 對於節點 2 來說,接收到(3,9)、(4,9)、(5,8)的投票,對比後發現自己的 zxid 最大,因此不需要做任何投票變更。

  • 對於節點 3 來說,接收到(2,10)、(4,9)、(5,8)的投票,對比後由於 2 的 zxid 比自己的 zxid 要大,因此需要更改投票,改投(2,10),並將改投後的票發給其他節點。

  • 對於節點 4 來說,接收到(2,10)、(3,9)、(5,8)的投票,對比後由於 2 的 zxid 比自己的 zxid 要大,因此需要更改投票,改投(2,10),並將改投後的票發給其他節點。

  • 對於節點 5 來說,也是一樣,最終改投(2,10)。

  • 經過第二輪投票後,叢集中的每個節點都會再次收到其他機器的投票,然後開始統計投票,如果有過半的節點投了同一個節點,則該節點成為新的 Leader,這裡顯然節點 2 成了新 Leader節點。

  • Leader 節點此時會將 epoch 值加 1,並將新生成的 epoch 分發給各個 Follower 節點。各個 Follower 節點收到全新的 epoch 後,返回 ACK 給 Leader 節點,並帶上各自最大的 zxid 和歷史事務日誌資訊。Leader 選出最大的 zxid,並更新自身歷史事務日誌,示例中的節點 2 無須更新。Leader 節點緊接著會將最新的事務日誌同步給叢集中所有的 Follower 節點,只有當半數 Follower 同步成功,這個準 Leader 節點才能成為正式的 Leader 節點並開始工作。

相關文章