1. 程式人生 > >轉載:深入淺出Zookeeper(一) Zookeeper架構及FastLeaderElection機制

轉載:深入淺出Zookeeper(一) Zookeeper架構及FastLeaderElection機制

轉載至 http://www.jasongj.com/zookeeper/fastleaderelection/:

 

原創文章,轉載請務必將下面這段話置於文章開頭處。
本文轉發自技術世界原文連結 http://www.jasongj.com/zookeeper/fastleaderelection/

Zookeeper是什麼

Zookeeper是一個分散式協調服務,可用於服務發現,分散式鎖,分散式領導選舉,配置管理等。

這一切的基礎,都是Zookeeper提供了一個類似於Linux檔案系統的樹形結構(可認為是輕量級的記憶體檔案系統,但只適合存少量資訊,完全不適合儲存大量檔案或者大檔案),同時提供了對於每個節點的監控與通知機制。

既然是一個檔案系統,就不得不提Zookeeper是如何保證資料的一致性的。本文將介紹Zookeeper如何保證資料一致性,如何進行領導選舉,以及資料監控/通知機制的語義保證。

Zookeeper架構

角色

Zookeeper叢集是一個基於主從複製的高可用叢集,每個伺服器承擔如下三種角色中的一種

  • Leader 一個Zookeeper叢集同一時間只會有一個實際工作的Leader,它會發起並維護與各Follwer及Observer間的心跳。所有的寫操作必須要通過Leader完成再由Leader將寫操作廣播給其它伺服器。
  • Follower 一個Zookeeper叢集可能同時存在多個Follower,它會響應Leader的心跳。Follower可直接處理並返回客戶端的讀請求,同時會將寫請求轉發給Leader處理,並且負責在Leader處理寫請求時對請求進行投票。
  • Observer 角色與Follower類似,但是無投票權。

Zookeeper Architecture

原子廣播(ZAB)

為了保證寫操作的一致性與可用性,Zookeeper專門設計了一種名為原子廣播(ZAB)的支援崩潰恢復的一致性協議。基於該協議,Zookeeper實現了一種主從模式的系統架構來保持叢集中各個副本之間的資料一致性。

根據ZAB協議,所有的寫操作都必須通過Leader完成,Leader寫入本地日誌後再複製到所有的Follower節點。

一旦Leader節點無法工作,ZAB協議能夠自動從Follower節點中重新選出一個合適的替代者,即新的Leader,該過程即為領導選舉。該領導選舉過程,是ZAB協議中最為重要和複雜的過程。

寫操作

寫Leader

通過Leader進行寫操作流程如下圖所示


Zookeeper Leader Write

由上圖可見,通過Leader進行寫操作,主要分為五步:

  1. 客戶端向Leader發起寫請求
  2. Leader將寫請求以Proposal的形式發給所有Follower並等待ACK
  3. Follower收到Leader的Proposal後返回ACK
  4. Leader得到過半數的ACK(Leader對自己預設有一個ACK)後向所有的Follower和Observer傳送Commmit
  5. Leader將處理結果返回給客戶端

這裡要注意

  • Leader並不需要得到Observer的ACK,即Observer無投票權
  • Leader不需要得到所有Follower的ACK,只要收到過半的ACK即可,同時Leader本身對自己有一個ACK。上圖中有4個Follower,只需其中兩個返回ACK即可,因為(2+1) / (4+1) > 1/2
  • Observer雖然無投票權,但仍須同步Leader的資料從而在處理讀請求時可以返回儘可能新的資料

寫Follower/Observer

通過Follower/Observer進行寫操作流程如下圖所示:


Zookeeper Follower/Observer Write

從上圖可見

  • Follower/Observer均可接受寫請求,但不能直接處理,而需要將寫請求轉發給Leader處理
  • 除了多了一步請求轉發,其它流程與直接寫Leader無任何區別

讀操作

Leader/Follower/Observer都可直接處理讀請求,從本地記憶體中讀取資料並返回給客戶端即可。


Zookeeper Read

由於處理讀請求不需要伺服器之間的互動,Follower/Observer越多,整體可處理的讀請求量越大,也即讀效能越好。

FastLeaderElection原理

術語介紹

myid
每個Zookeeper伺服器,都需要在資料資料夾下建立一個名為myid的檔案,該檔案包含整個Zookeeper叢集唯一的ID(整數)。例如某Zookeeper叢集包含三臺伺服器,hostname分別為zoo1、zoo2和zoo3,其myid分別為1、2和3,則在配置檔案中其ID與hostname必須一一對應,如下所示。在該配置檔案中,server.後面的資料即為myid

1 2 3 server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888

zxid
類似於RDBMS中的事務ID,用於標識一次更新操作的Proposal ID。為了保證順序性,該zkid必須單調遞增。因此Zookeeper使用一個64位的數來表示,高32位是Leader的epoch,從1開始,每次選出新的Leader,epoch加一。低32位為該epoch內的序號,每次epoch變化,都將低32位的序號重置。這樣保證了zkid的全域性遞增性。

支援的領導選舉演算法

可通過electionAlg配置項設定Zookeeper用於領導選舉的演算法。

到3.4.10版本為止,可選項有

  • 0 基於UDP的LeaderElection
  • 1 基於UDP的FastLeaderElection
  • 2 基於UDP和認證的FastLeaderElection
  • 3 基於TCP的FastLeaderElection

在3.4.10版本中,預設值為3,也即基於TCP的FastLeaderElection。另外三種演算法已經被棄用,並且有計劃在之後的版本中將它們徹底刪除而不再支援。

FastLeaderElection

FastLeaderElection選舉演算法是標準的Fast Paxos演算法實現,可解決LeaderElection選舉演算法收斂速度慢的問題。

伺服器狀態

  • LOOKING 不確定Leader狀態。該狀態下的伺服器認為當前叢集中沒有Leader,會發起Leader選舉
  • FOLLOWING 跟隨者狀態。表明當前伺服器角色是Follower,並且它知道Leader是誰
  • LEADING 領導者狀態。表明當前伺服器角色是Leader,它會維護與Follower間的心跳
  • OBSERVING 觀察者狀態。表明當前伺服器角色是Observer,與Folower唯一的不同在於不參與選舉,也不參與叢集寫操作時的投票

選票資料結構

每個伺服器在進行領導選舉時,會發送如下關鍵資訊

  • logicClock 每個伺服器會維護一個自增的整數,名為logicClock,它表示這是該伺服器發起的第多少輪投票
  • state 當前伺服器的狀態
  • self_id 當前伺服器的myid
  • self_zxid 當前伺服器上所儲存的資料的最大zxid
  • vote_id 被推舉的伺服器的myid
  • vote_zxid 被推舉的伺服器上所儲存的資料的最大zxid

投票流程

自增選舉輪次
Zookeeper規定所有有效的投票都必須在同一輪次中。每個伺服器在開始新一輪投票時,會先對自己維護的logicClock進行自增操作。

初始化選票
每個伺服器在廣播自己的選票前,會將自己的投票箱清空。該投票箱記錄了所收到的選票。例:伺服器2投票給伺服器3,伺服器3投票給伺服器1,則伺服器1的投票箱為(2, 3), (3, 1), (1, 1)。票箱中只會記錄每一投票者的最後一票,如投票者更新自己的選票,則其它伺服器收到該新選票後會在自己票箱中更新該伺服器的選票。

傳送初始化選票
每個伺服器最開始都是通過廣播把票投給自己。

接收外部投票
伺服器會嘗試從其它伺服器獲取投票,並記入自己的投票箱內。如果無法獲取任何外部投票,則會確認自己是否與叢集中其它伺服器保持著有效連線。如果是,則再次傳送自己的投票;如果否,則馬上與之建立連線。

判斷選舉輪次
收到外部投票後,首先會根據投票資訊中所包含的logicClock來進行不同處理

  • 外部投票的logicClock大於自己的logicClock。說明該伺服器的選舉輪次落後於其它伺服器的選舉輪次,立即清空自己的投票箱並將自己的logicClock更新為收到的logicClock,然後再對比自己之前的投票與收到的投票以確定是否需要變更自己的投票,最終再次將自己的投票廣播出去。
  • 外部投票的logicClock小於自己的logicClock。當前伺服器直接忽略該投票,繼續處理下一個投票。
  • 外部投票的logickClock與自己的相等。當時進行選票PK。

選票PK
選票PK是基於(self_id, self_zxid)與(vote_id, vote_zxid)的對比

  • 外部投票的logicClock大於自己的logicClock,則將自己的logicClock及自己的選票的logicClock變更為收到的logicClock
  • 若logicClock一致,則對比二者的vote_zxid,若外部投票的vote_zxid比較大,則將自己的票中的vote_zxid與vote_myid更新為收到的票中的vote_zxid與vote_myid並廣播出去,另外將收到的票及自己更新後的票放入自己的票箱。如果票箱內已存在(self_myid, self_zxid)相同的選票,則直接覆蓋
  • 若二者vote_zxid一致,則比較二者的vote_myid,若外部投票的vote_myid比較大,則將自己的票中的vote_myid更新為收到的票中的vote_myid並廣播出去,另外將收到的票及自己更新後的票放入自己的票箱

統計選票
如果已經確定有過半伺服器認可了自己的投票(可能是更新後的投票),則終止投票。否則繼續接收其它伺服器的投票。

更新伺服器狀態
投票終止後,伺服器開始更新自身狀態。若過半的票投給了自己,則將自己的伺服器狀態更新為LEADING,否則將自己的狀態更新為FOLLOWING

幾種領導選舉場景

叢集啟動領導選舉

初始投票給自己
叢集剛啟動時,所有伺服器的logicClock都為1,zxid都為0。

各伺服器初始化後,都投票給自己,並將自己的一票存入自己的票箱,如下圖所示。


Cluster start election step 1


在上圖中,(1, 1, 0)第一位數代表投出該選票的伺服器的logicClock,第二位數代表被推薦的伺服器的myid,第三位代表被推薦的伺服器的最大的zxid。由於該步驟中所有選票都投給自己,所以第二位的myid即是自己的myid,第三位的zxid即是自己的zxid。

此時各自的票箱中只有自己投給自己的一票。

更新選票
伺服器收到外部投票後,進行選票PK,相應更新自己的選票並廣播出去,並將合適的選票存入自己的票箱,如下圖所示。


Cluster start election step 2


伺服器1收到伺服器2的選票(1, 2, 0)和伺服器3的選票(1, 3, 0)後,由於所有的logicClock都相等,所有的zxid都相等,因此根據myid判斷應該將自己的選票按照伺服器3的選票更新為(1, 3, 0),並將自己的票箱全部清空,再將伺服器3的選票與自己的選票存入自己的票箱,接著將自己更新後的選票廣播出去。此時伺服器1票箱內的選票為(1, 3),(3, 3)。

同理,伺服器2收到伺服器3的選票後也將自己的選票更新為(1, 3, 0)並存入票箱然後廣播。此時伺服器2票箱內的選票為(2, 3),(3, ,3)。

伺服器3根據上述規則,無須更新選票,自身的票箱內選票仍為(3, 3)。

伺服器1與伺服器2更新後的選票廣播出去後,由於三個伺服器最新選票都相同,最後三者的票箱內都包含三張投給伺服器3的選票。

根據選票確定角色
根據上述選票,三個伺服器一致認為此時伺服器3應該是Leader。因此伺服器1和2都進入FOLLOWING狀態,而伺服器3進入LEADING狀態。之後Leader發起並維護與Follower間的心跳。


Cluster start election step 3

Follower重啟

Follower重啟投票給自己
Follower重啟,或者發生網路分割槽後找不到Leader,會進入LOOKING狀態併發起新的一輪投票。


Follower restart election step 1

發現已有Leader後成為Follower
伺服器3收到伺服器1的投票後,將自己的狀態LEADING以及選票返回給伺服器1。伺服器2收到伺服器1的投票後,將自己的狀態FOLLOWING及選票返回給伺服器1。此時伺服器1知道伺服器3是Leader,並且通過伺服器2與伺服器3的選票可以確定伺服器3確實得到了超過半數的選票。因此伺服器1進入FOLLOWING狀態。


Follower restart election step 2

Leader重啟

Follower發起新投票
Leader(伺服器3)宕機後,Follower(伺服器1和2)發現Leader不工作了,因此進入LOOKING狀態併發起新的一輪投票,並且都將票投給自己。


Leader restart election step 1

廣播更新選票
伺服器1和2根據外部投票確定是否要更新自身的選票。這裡有兩種情況

  • 伺服器1和2的zxid相同。例如在伺服器3宕機前伺服器1與2完全與之同步。此時選票的更新主要取決於myid的大小
  • 伺服器1和2的zxid不同。在舊Leader宕機之前,其所主導的寫操作,只需過半伺服器確認即可,而不需所有伺服器確認。換句話說,伺服器1和2可能一個與舊Leader同步(即zxid與之相同)另一個不同步(即zxid比之小)。此時選票的更新主要取決於誰的zxid較大

在上圖中,伺服器1的zxid為11,而伺服器2的zxid為10,因此伺服器2將自身選票更新為(3, 1, 11),如下圖所示。


Leader restart election step 2

選出新Leader
經過上一步選票更新後,伺服器1與伺服器2均將選票投給伺服器1,因此伺服器2成為Follower,而伺服器1成為新的Leader並維護與伺服器2的心跳。


Leader restart election step 3

舊Leader恢復後發起選舉
舊的Leader恢復後,進入LOOKING狀態併發起新一輪領導選舉,並將選票投給自己。此時伺服器1會將自己的LEADING狀態及選票(3, 1, 11)返回給伺服器3,而伺服器2將自己的FOLLOWING狀態及選票(3, 1, 11)返回給伺服器3。如下圖所示。


Leader restart election step 4

舊Leader成為Follower
伺服器3瞭解到Leader為伺服器1,且根據選票瞭解到伺服器1確實得到過半伺服器的選票,因此自己進入FOLLOWING狀態。


Leader restart election step 5

一致性保證

ZAB協議保證了在Leader選舉的過程中,已經被Commit的資料不會丟失,未被Commit的資料對客戶端不可見。

Commit過的資料不丟失

Failover前狀態
為更好演示Leader Failover過程,本例中共使用5個Zookeeper伺服器。A作為Leader,共收到P1、P2、P3三條訊息,並且Commit了1和2,且總體順序為P1、P2、C1、P3、C2。根據順序性原則,其它Follower收到的訊息的順序肯定與之相同。其中B與A完全同步,C收到P1、P2、C1,D收到P1、P2,E收到P1,如下圖所示。


Leader Failover step 1

這裡要注意

  • 由於A沒有C3,意味著收到P3的伺服器的總個數不會超過一半,也即包含A在內最多隻有兩臺伺服器收到P3。在這裡A和B收到P3,其它伺服器均未收到P3
  • 由於A已寫入C1、C2,說明它已經Commit了P1、P2,因此整個叢集有超過一半的伺服器,即最少三個伺服器收到P1、P2。在這裡所有伺服器都收到了P1,除E外其它伺服器也都收到了P2

選出新Leader
舊Leader也即A宕機後,其它伺服器根據上述FastLeaderElection演算法選出B作為新的Leader。C、D和E成為Follower且以B為Leader後,會主動將自己最大的zxid傳送給B,B會將Follower的zxid與自身zxid間的所有被Commit過的訊息同步給Follower,如下圖所示。


Leader Failover step 2

在上圖中

  • P1和P2都被A Commit,因此B會通過同步保證P1、P2、C1與C2都存在於C、D和E中
  • P3由於未被A Commit,同時倖存的所有伺服器中P3未存在於大多資料伺服器中,因此它不會被同步到其它Follower

通知Follower可對外服務
同步完資料後,B會向D、C和E傳送NEWLEADER命令並等待大多數伺服器的ACK(下圖中D和E已返回ACK,加上B自身,已經佔叢集的大多數),然後向所有伺服器廣播UPTODATE命令。收到該命令後的伺服器即可對外提供服務。


Leader Failover step 3

未Commit過的訊息對客戶端不可見

在上例中,P3未被A Commit過,同時因為沒有過半的伺服器收到P3,因此B也未Commit P3(如果有過半伺服器收到P3,即使A未Commit P3,B會主動Commit P3,即C3),所以它不會將P3廣播出去。

具體做法是,B在成為Leader後,先判斷自身未Commit的訊息(本例中即P3)是否存在於大多數伺服器中從而決定是否要將其Commit。然後B可得出自身所包含的被Commit過的訊息中的最小zxid(記為min_zxid)與最大zxid(記為max_zxid)。C、D和E向B傳送自身Commit過的最大訊息zxid(記為max_zxid)以及未被Commit過的所有訊息(記為zxid_set)。B根據這些資訊作出如下操作

  • 如果Follower的max_zxid與Leader的max_zxid相等,說明該Follower與Leader完全同步,無須同步任何資料
  • 如果Follower的max_zxid在Leader的(min_zxid,max_zxid)範圍內,Leader會通過TRUNC命令通知Follower將其zxid_set中大於Follower的max_zxid(如果有)的所有訊息全部刪除

上述操作保證了未被Commit過的訊息不會被Commit從而對外不可見。

上述例子中Follower上並不存在未被Commit的訊息。但可考慮這種情況,如果將上述例子中的伺服器數量從五增加到七,伺服器F包含P1、P2、C1、P3,伺服器G包含P1、P2。此時伺服器F、A和B都包含P3,但是因為票數未過半,因此B作為Leader不會Commit P3,而會通過TRUNC命令通知F刪除P3。如下圖所示。


Leader Failover step 4

總結

    • 由於使用主從複製模式,所有的寫操作都要由Leader主導完成,而讀操作可通過任意節點完成,因此Zookeeper讀效能遠好於寫效能,更適合讀多寫少的場景
    • 雖然使用主從複製模式,同一時間只有一個Leader,但是Failover機制保證了叢集不存在單點失敗(SPOF)的問題
    • ZAB協議保證了Failover過程中的資料一致性
    • 伺服器收到資料後先寫本地檔案再進行處理,保證了資料的永續性