zookeeper核心之ZAB協議就這麼簡單!
阿新 • • 發佈:2020-10-21
### 背景
我們都知道 Zookeeper 是基於 ZAB 協議實現的,在介紹 ZAB 協議之前,先回顧一下 Zookeeper 的起源與發展。
Zookeeper 究竟是在什麼樣的時代背景下被提出?為了解決了哪些棘手的問題?
Zookeeper 最早起源於雅虎研究院的一個研究小組。當時,研究人員發現,在雅虎的很多大型系統基本都需要依賴一個類似的系統來進行分散式協調,但是這些系統都存在分散式單點問題,所以雅虎的開發人員就試圖開發出一個通用的無單點問題的分散式協調框架,以便讓開發人員將精力集中在處理業務邏輯上。
於是,Zookeeper 就誕生了!
Zookeeper 的出現不僅解決了分散式系統下資料一致性的問題,而且經歷過線上驗證,無論是從效能、易用性、穩定性上來說,都是工業級產品的標準。可以說在分散式系統中具有不可替代的核心地位,Hadoop、HBase、Storm 和 Solr 等大型分散式系統都已經將 Zookeeper 作為其核心元件,用於分散式協調。即便 Zookeeper 如此優秀,但是 Zookeeper 依然是免費且開源的,全世界成千上萬的開發者都關注著它,陪同著一起成長和發展。
作為一個開發者無論是為了應付面試、晉升還是個人技術成長的需要,都需要對 Zookeeper 有所瞭解,而學習 Zookeeper 的關鍵就是理解其核心部分:
**ZAB 協議, 全稱(Zookeeper Atomic Broadcast)Zookeeper 原子訊息廣播協議。**
它與 Paxos 類似,也是一種資料一致性的演算法。
### Zookeeper應該具備的特性
在 ZAB 協議的開發設計人員在協議設計之初並沒有要求 ZAB 具有很好的擴充套件性,最初只是為了雅虎公司內部哪些高吞吐量、低延遲、健壯、簡單的分散式系統場景設計的。基於 ZAB 協議,Zookeeper 實現了一種主備模式的系統架構來保持叢集中各副本之間資料的一致性,簡單架構圖如下:
Zookeeper 用一個單一的主程序來接收並處理客戶端的所有事物請求,並採用 ZAB 的原子廣播協議將伺服器資料狀態以事物 Proposal 的形式廣播到所有的副本程序中去。這樣的模式就保證了,在同一時刻只有一個主程序來廣播伺服器的狀態更變,因此能夠很好地處理客戶端大量的併發請求,這在 ZAB 協議中叫:訊息廣播。
除此之外,在分散式環境中事物的執行順序也會存在一定的先後關係,比如:事務 C 的寫入需要依賴事務 B 的寫入,而事務 B 寫入需要依賴事務 A 寫入。這種前後依賴的順序也對 ZAB 協議提出了一個要求:ZAB 協議需要保證如果一個狀態的變更被處理了,那麼所有其依賴的狀態變更都已經被提前處理了。也就是需要順序執行。
另外除了能正常廣播訊息、訊息的順序執行,主程序也可能隨時會因為斷電、機器宕機等異常情況無法提供服務,因此,ZAB 協議還需要做到在當前主程序出現上述異常情況的時候依然能夠正常工作,這在 ZAB 協議中叫:崩潰恢復。
所以整個 ZAB 協議需要具備的核心特性已經被描述出來了,處理事務的請求的方式可以簡單理解為:
> 所有的事務請求必須由一個全域性唯一的伺服器來協調處理,這樣的伺服器叫:Leader 伺服器。其他的伺服器被稱為 Follower 伺服器。Leader 伺服器將客戶端事務請求轉化成一個事務 Prososal(提議),並將改 Proposal 分發給叢集中所有的 Follower 伺服器。之後 Leader 伺服器接收了正確的反饋後,那麼 Leader 就會再次向所有的 Follower 伺服器分發 Commit 訊息,要求將前一個 Proposal 提交。
這就簡單闡述了**ZAB 協議中訊息廣播模式的部分內容**。
### ZAB協議的兩種模式
ZAB 協議的包括兩種模式:**崩潰恢復**、**訊息廣播**。
既然有兩種模式,那 Zookeeper 叢集什麼時候進入奔潰恢復模式?什麼時候進入訊息廣播模式呢?
在進入**奔潰恢復**模式時 Zookeeper 叢集會進行 Leader 選舉,一般有兩種情況會發生選舉:
* 當伺服器啟動時期會進行 Leader 選舉。
* 當伺服器執行期 Leader 伺服器的出現網路中斷、奔潰退出、重啟等異常情況,或者當叢集中半數的伺服器與該 Leader 伺服器無法通訊時,進入崩潰恢復模式,開始 Leader 選舉。
選舉出 Leader 伺服器後,會進入**訊息廣播模式**,開始接收處理客戶端的請求,前文已經描述,這裡不再贅述。
### 相關名詞概念
在深入講解 ZAB 協議的兩個模式之前,先解釋 Zookeeper 的幾個相關概念,方便理解 ZAB 協議:
#### 三種角色
在前面提到 Zookeeper 的叢集中的伺服器有 Leader 和 Follower ,但實際在 ZAB 協議中 Zookeeper 有三種角色,分別是 Leader、Folower、Observer,它們的分工各有不同:
* **Leader** :負責整個Zookeeper 叢集工作機制中的核心,主要工作有一下兩個:
* 事務請求的唯一排程和處理者,保證叢集事務處理的順序性
* 叢集內部各伺服器的排程者
* **Follower** :它是 Leader 的追隨者,其主要工作有三個:
* 處理客戶端的非實物請求,轉發事務請求給 Leader 伺服器
* 參與事務請求 Proposal 的投票
* 參與 Leader 選舉投票
* **Observer** :是 zookeeper 自 3.3.0 開始引入的一個角色,它不參與事務請求 Proposal 的投票,也不參與 Leader 選舉投票,只提供非事務的服務(查詢),通常在不影響叢集事務處理能力的前提下提升叢集的非事務處理能力。
#### 三種狀態
在知道了 Zookeeper 中有三種角色後,不經提問: Zookeeper 是如何知道自己目前是什麼角色的呢?
在 ZAB 協議中定義:通過自身的狀態來區分自己的角色的,在執行期間各個程序可能出現以下三種狀態之一:
* **LOOKING**:處在這個狀態時,會進入 Leader 選舉狀態
* **FOLLOWER**:Follower 伺服器和 Leader 伺服器保持同步時的狀態
* **LEADING**:Leader 伺服器作為主程序領導者的狀態
在組成 ZAB 協議的所有程序啟動的時候,初始化狀態都是 LOOKING 狀態,此時程序組中不存在 Leader,選舉之後才有,在進行選舉成功後,就進入訊息廣播模式,此時 Zookeeper 叢集中的角色狀態就不再是 LOOKING 狀態。
#### ZXID
前文我們知道 zookeeper 訊息有嚴格的因果關係,因此必須將每一個事務請求按照先後順序來進行排序與處理。那 Zookeeper 是如何保持請求處理的順序的呢?其中非常關鍵的點就是 ZXID。
那 ZXID 究竟是怎麼發揮作用的呢?
Leader 伺服器在接收到事務請求後,會為每個事務請求生成對應的 Proposal 來進行廣播,並且在廣播事務 Proposal 之前,Leader 伺服器會首先為這個事務 Proposal 分配一個全域性單調遞增的唯一 ID ,我們稱之為事務 ID(即 ZXID)。
ZXID 的設計也很有特點,是一個全域性有序的 64 位的數字,可以分為兩個部分:
* 高 32 位是: epoch(紀元),代表著週期,每當選舉產生一個新的 Leader 伺服器時就會取出其本地日誌中最大事務的 ZXID ,解析出 epoch(紀元)值操作加 1作為新的 epoch ,並將低 32 位置零。
* 低 32 位是: counter(計數器),它是一個簡單的單調遞增的計數器,針對客戶端的每個事務請求都會進行加 1 操作;
這裡低 32 位 counter(計數器)單調遞增還好理解,高 32 位 epoch(紀元)每次選舉加 1 也許有些同學就有疑問了,為什麼 epoch(紀元)每次選需要舉加 1 ,它在整個 ZAB 協議中有什麼作用?
我們知道每當選舉產生一個新的 Leader 伺服器時生成一個新的 epoch(紀元)值,而在前文我們知道,服務執行過程中觸發選舉 Leader 的條件是:**Leader 伺服器的出現網路中斷、奔潰退出、重啟等異常情況,或者當叢集中半數的伺服器與該 Leader 伺服器無法通訊時**。
這說明整個 Zookeeper 叢集此時處於一個異常的情況下,而在發生異常前,訊息廣播進行到哪一步驟我們根本不知道,叢集中的其他 Follower 節點從這種崩潰恢復狀態重新選舉出 Leader 後,如果老 Leader 又恢復了連線進入叢集。此時老 Leader 的 epoch 肯定會小於新 Leader 的 epoch,這時就將老 Leader 變成 Follower,對新的 Leader 進行資料同步。即便這時老 Leader 對其他的 Follower 節點發送了請求,Follower 節點也會比較 ZXID 的值,因為高 32 位加 1 了, Follower 的 epoch(紀元)大於老 Leader 的 epoch(紀元),所以 Follower 會忽略這個請求。
這像改朝換代一樣,前朝的劍不能斬本朝的官。
### 訊息廣播模式
知道了這些名詞,和上文提到的零散的知識點,其實崩潰恢復模式和訊息廣播模式的過程大家大致有所瞭解了。
先看看訊息廣播模式吧!
訊息廣播的模式的過程簡圖如下所示:
整個過程類似一個二階段提交的過程,但卻有所不同,ZAB 協議簡化了二階段提交模型,在超過半數的 Follower 伺服器已經反饋 ACK 之後就開始提交事務 Prososal 了,無需等待所有伺服器響應。
結合上圖,看看訊息廣播的具體細節:
* Leader 伺服器接收到請求後在進行廣播事務 Proposal 之前會為這個事務分配一個 ZXID,再進行廣播。
* Leader 伺服器會為每個 Follower 伺服器都各自分配一個單獨的佇列,然後將需要廣播的事務 Proposal 依次放入這些佇列中去,並根據 FIFO 策略進行訊息的傳送。
* 每個Follower 伺服器在接收到後都會將其以事務日誌的形式寫入到本地磁碟中,並且在成功寫入後返回 Leader 伺服器一個 ACK 響應。
* 當有超過半數的伺服器 ACK 響應後,Leader 就會廣播一個 Commit 訊息給所有的 Follower 伺服器,Follower 接收到後就完成對事務的提交操作。
在畫一張詳細點的流程圖,更直觀:
這就完成了整個訊息廣播了!
### 崩潰恢復模式
前文已經反覆提過崩潰恢復模式了,其實就是重新選舉出新的 Leader 伺服器,選舉完成後 Follower 伺服器在再去同步 Leader 的資料。
執行中的服務再次進行重新選舉,一定是出現某種異常,我們知道在出現異常情況之前 Leader 的訊息廣播可能會處在任何一個階段,有可能客戶端的請求只是在 Leader 伺服器上提出並未被提交,也可能請求已經被 Leader 伺服器提交。
ZAB 協議對於不同階段的出現的資料不一致的情況做了相容,保證:
* 已經在 Leader 伺服器上**提交**的事務,最終會被所有伺服器都提交
* 只在 Leader 伺服器上**提出**的事務,要丟棄
針對以上的兩個要求,在進行 Leader 選舉時,之需要選舉出叢集中 ZXID 最大的事務 Proposal 即可,這樣就可以省去 Leader 伺服器檢查 Proposal 的提交和丟棄工作了。因為 Leader 伺服器的事務是最大的,一切以 Leader 伺服器的資料為標準即可。
ZXID 在叢集中其實並不是唯一的,所以也有可能出現多 Follower 伺服器 ZXID 相同的情況,這時候就需要比較 Zookeeper 的 SID 值。什麼是 SID?SID 是一個數字,和 zookeeper 的 myid 一致,myid 就不要解釋了,安裝過 Zookeeper 的都知道,每臺伺服器都需要配置一個這樣的檔案,裡面只有一個數字,用來標識這臺伺服器。因為每臺機器的 myid 配置都不一樣,所以叢集選舉的時不會出現相等的情況。
選舉時,比較大小的原始碼如下:
前面已經說過,出現選舉 Leader 可能會出現兩種情況:
* 服務啟動時期,發起選舉
* 服務執行期間,出現異常,發起選舉
但無論是啟動期還是執行期進行 Leader 選舉,其選舉過程都差不太多,我簡單畫個流程圖:
結合上圖,奔潰恢復模式下 Leader 選舉的過程細節如下:
* 檢測節點處於 LOOKING 階段,開發選舉 Leader
* 發起投票時有兩種情況:
* 在服務啟動的初始階段,每個伺服器都會投票給自己以(myid,zxid)的資訊形式傳送,那初始階段沒有 zxid 值,就會發送(myid,0)
* 在伺服器執行期間,每個伺服器上的 zxid 都有值,且 zxid 都不相同,所以就正常傳送(myid,zxid)
* 各節點收到資訊後將收到的(myid,zxid)和自己的比較,比較的過程前面已經說過,這裡不再贅述
* 然後判斷是否有半數的機器投票選出 Leader,如果否,在進入新一輪投票,直到選出
* 選出 Leader 後,其他節點就變成 Follower 角色,並向 Leader 傳送自己伺服器的最大 zxid ,Leader 伺服器收到後會和自己本地的提議快取佇列進行比較,判斷使用那種策略進行同步(後面詳細說明同步的四種策略)
* 當同步完成,叢集就可以正常的處理請求了,就進入訊息廣播模式了。
這就是崩潰恢復模式下選舉 Leader 的過程了!
下面再簡單介紹下資料同步的四種策略,這四種同步策略保證了Zookeeper 叢集中的資料一致性,也解決了前文提出的兩個問題,相容了各種資料不一致的場景。
#### 資料同步的四種策略
在資料同步之前,Leader 伺服器會進行資料同步的初始化,首先會從 Zookeeper 的記憶體資料庫中提取出事務前期對應的提議快取佇列,同時會初始化三個 ZXID 的值:
* peerLastZxid:這是 Follower 的最後處理 ZXID
* minCommittedLog:Leader 伺服器的提議快取佇列中 最小的 ZXID
* maxCommittedLog:Leader 伺服器的提議快取佇列中 最大的 ZXID
根據這三個引數,就可以確定四種同步方式,分別為:
* 直接差異化同步
* 場景:當 minCommittedLog < peerLastZxid < maxCommittedLog 時
* 先回滾在差異化同步
* 場景:假如叢集有 A、B、C 三臺機器,此時 A 是 Leader 但是 A 掛了,在掛之前 A 生成了一個提議假設是:03,然後叢集有重新選舉 B 為新的 Leader,此時生成的的提議快取佇列為:01~02,B 和 C 進行同步之後,生成新的紀元,ZXID 從 10 開始計數,叢集進入廣播模式處理了部分請求,假設現在 ZXID 執行到 15 這個值,此時 A 恢復了加入叢集,這時候就比較 A 最後提交的 ZXID:peerLastZxid 與 minCommittedLog、maxCommittedLog 的關係。此時雖然符合直接差異化同步:minCommittedLog < peerLastZxid < maxCommittedLog 這樣的關係,但是提議快取佇列中卻沒有這個 ZXID ,這時候就需要先回滾,在進行同步。
* 僅回滾同步
* 場景:這裡和先回滾在差異化同步類似,直接回滾就可以。
* 全量同步
* 場景:peerLastZxid < minCommittedLog,當遠遠落後 Leader 的資料時,直接全量同步。
這就是四種同步策略,這幾種同步方式也解決了上文提出的問題:
* **只在 Leader 伺服器上提出的事務,要丟棄**(這個問題會在同步時,會進行回滾,使得只在 Leader 伺服器上提出的事務丟棄)
這些就是整個 ZAB 協議中崩潰恢復的內容。
### ZAB協議和Paxos演算法的區別
ZAB協議看起來和Paxos有著相同之處,但它並不是Paxos的典型實現,其實還是有一些區別,ZAB協議中額外添加了一個同步的階段,兩者設計目標也不太一樣,ZAB協議主要用於構建一個高可用的分散式資料主備系統,而Paxos演算法則是用於構建一個分散式一致性的狀態機。
### 總結
Zookeeper 作為出色的分散式協調服務,目前讀 QPS 達到 12w,出色的效能也讓開發者更加青睞,其 ZAB 協議的核心分為兩個部分:崩潰恢復、訊息廣播。
典型的應用場景有:
* 資料釋出/訂閱、負載均衡
* 命名服務
* 分散式協調通知
* 叢集管理
* Master選舉
* 分散式鎖
* 分散式佇列
* 用 Zookeeper 避免腦裂
除此之外在大資料領域也有應用,例如:
* Hadoop
* HBase
* Kafka
在阿里巴巴集團內部實踐的 Zookeeper 的產品也有很多,如:
* 訊息中介軟體:Metamorphosis
* RPC 服務框架:Dubbo
* 基於 MySQL Binlog 的增量訂閱和消費元件:Cancel
* 分散式資料庫同步系統:Otter
* 實時計算引擎:JStorm
歡迎關注我的個人微信公眾號:jav