面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

Java架構技術棧發表於2019-04-09

ZooKeeper是一個開源分散式協調服務、分散式資料一致性解決方案。可基於ZooKeeper實現命名服務、叢集管理、Master選舉、分散式鎖等功能。

高可用

為了保證ZooKeeper的可用性,在生產環境中我們使用ZooKeeper叢集模式對外提供服務,並且叢集規模至少由3個ZooKeeper節點組成。

叢集至少由3個節點組成

ZooKeeper其實2個節點也可以組成叢集並對外提供服務,但我們使用叢集主要目的是為了高可用。如果2個節點組成叢集,其中1個節點掛了,另外ZooKeeper節點不能正常對外提供服務。因此也失去了叢集的意義。

如果3個節點組成叢集,其中1個節點掛掉後,根據ZooKeeper的Leader選舉機制是可以從另外2個節點選出一個作為Leader的,叢集可以繼續對外提供服務。

並非節點越多越好

  • 節點越多,使用的資源越多

  • 節點越多,ZooKeeper節點間花費的通訊成本越高,節點間互連的Socket也越多。影響ZooKeeper叢集事務處理

  • 節點越多,造成腦裂的可能性越大

叢集規模為奇數

叢集規模除了考慮自身成本和資源外還要結合ZooKeeper特性考慮:

  • 節省資源

    3節點叢集和4節點叢集,我們選擇使用3節點叢集;5節點叢集和6節點叢集,我們選擇使用5節點叢集。以此類推。因為生產環境為了保證高可用,3節點叢集最多隻允許掛1臺,4節點叢集最多也只允許掛1臺(過半原則中解釋了原因)。同理5節點叢集最多允許掛2臺,6節點叢集最多也只允許掛2臺。

    出於對資源節省的考慮,我們應該使用奇數節點來滿足相同的高可用性。

  • 叢集可用性

    當叢集中節點間網路通訊出現問題時奇數和偶數對叢集的影響

面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

叢集配置

ZooKeeper叢集配置至少需要2處變更:

1、增加叢集配置

在{ZK_HOME}/conf/zoo.cfg中增加叢集的配置,結構以server.id=ip:port1:port2為標準。

比如下面配置檔案中表示由3個ZooKeeper組成的叢集:

server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
複製程式碼
面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

2、配置節點id

zoo.cfg中配置叢集時需要指定server.id,這個id需要在dataDir(zoo.cfg中配置)指定的目錄中建立myid檔案,檔案內容就是當前ZooKeeper節點的id。

叢集角色

ZooKeeper沒有使用Master/Slave的概念,而是將叢集中的節點分為了3類角色:

  • Leader

    在一個ZooKeeper叢集中,只能存在一個Leader,這個Leader是叢集中事務請求唯一的排程者和處理者,所謂事務請求是指會改變叢集狀態的請求;Leader根據事務ID可以保證事務處理的順序性。

    如果一個叢集中存在多個Leader,這種現象稱為「腦裂」。試想一下,一個叢集中存在多個Leader會產生什麼影響?

    相當於原本一個大叢集,裂出多個小叢集,他們之間的資料是不會相互同步的。「腦裂」後叢集中的資料會變得非常混亂。

  • Follower

    Follower角色的ZooKeeper服務只能處理非事務請求;如果接收到客戶端事務請求會將請求轉發給Leader伺服器;參與Leader選舉;參與Leader事務處理投票處理。

    Follower發現叢集中Leader不可用時會變更自身狀態,併發起Leader選舉投票,最終叢集中的某個Follower會被選為Leader。

  • Observer

    Observer與Follower很像,可以處理非事務請求;將事務請求轉發給Leader伺服器。

    與Follower不同的是,Observer不會參與Leader選舉;不會參與Leader事務處理投票。

    Observer用於不影響叢集事務處理能力的前提下提升叢集的非事務處理能力。

Leader選舉

Leader在叢集中是非常重要的一個角色,負責了整個事務的處理和排程,保證分散式資料一致性的關鍵所在。既然Leader在ZooKeeper叢集中這麼重要所以一定要保證叢集在任何時候都有且僅有一個Leader存在。

如果叢集中Leader不可用了,需要有一個機制來保證能從叢集中找出一個最優的服務晉升為Leader繼續處理事務和排程等一系列職責。這個過程稱為Leader選舉。

選舉機制

ZooKeeper選舉Leader依賴下列原則並遵循優先順序:

1、選舉投票必須在同一輪次中進行

如果Follower服務選舉輪次不同,不會採納投票。

2、資料最新的節點優先成為Leader

資料的新舊使用事務ID判定,事務ID越大認為節點資料約接近Leader的資料,自然應該成為Leader。

3、比較server.id,id值大的優先成為Leader

如果每個參與競選節點事務ID一樣,再使用server.id做比較。server.id是節點在叢集中唯一的id,myid檔案中配置。

不管是在叢集啟動時選舉Leader還是叢集執行中重新選舉Leader。叢集中每個Follower角色服務都是以上面的條件作為基礎推選出合適的Leader,一旦出現某個節點被過半推選,那麼該節點晉升為Leader。

過半原則

ZooKeeper叢集會有很多型別投票。Leader選舉投票;事務提議投票;這些投票依賴過半原則。就是說ZooKeeper認為投票結果超過了叢集總數的一半,便可以安全的處理後續事務。

  • 事務提議投票

    假設有3個節點組成ZooKeeper叢集,客戶端請求新增一個節點。Leader接到該事務請求後給所有Follower發起「建立節點」的提議投票。如果Leader收到了超過叢集一半數量的反饋,繼續給所有Follower發起commit。此時Leader認為叢集過半了,就算自己掛了叢集也是安全可靠的。

  • Leader選舉投票

    假設有3個節點組成ZooKeeper叢集,這時Leader掛了,需要投票選舉Leader。當相同投票結果過半後Leader選出。

  • 叢集可用節點

    ZooKeeper叢集中每個節點有自己的角色,對於叢集可用性來說必須滿足過半原則。這個過半是指Leader角色 + Follower角色可用數大於叢集中Leader角色 + Follower角色總數。
    假設有5個節點組成ZooKeeper叢集,一個Leader、兩個Follower、兩個Observer。當掛掉兩個Follower或掛掉一個Leader和一個Follower時叢集將不可用。因為Observer角色不參與任何形式的投票。

所謂過半原則演算法是說票數 > 叢集總節點數/2。其中叢集總節點數/2的計算結果會向下取整。

在ZooKeeper原始碼QuorumMaj.java中實現了這個演算法。下面程式碼片段有所縮減。

public boolean containsQuorum(HashSet<Long> set) {
  /** n是指叢集總數 */
  int half = n / 2;
  return (set.size() > half);
}
複製程式碼

回過頭我們看一下奇數和偶數叢集在Leader選舉的結果

面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

所以3節點和4節點組成的叢集在ZooKeeper過半原則下都最多隻能掛1節點,但是4比3要多浪費一個節點資源。

場景實戰

我們以兩個場景來了解叢集不可用時Leader重新選舉的過程。

3節點叢集重選Leader

假設有3節點組成的叢集,分別是server.1(Follower)、server.2(Leader)、server.3(Follower)。此時server.2不可用了。叢集會產生以下變化:

1、叢集不可用

因為Leader掛了,叢集不可用於事務請求了。

2、狀態變更

所有Follower節點變更自身狀態為LOOKING,並且變更自身投票。投票內容就是自己節點的事務ID和server.id。我們以(事務ID, server.id)表示。

假設server.1的事務id是10,變更的自身投票就是(10, 1);server.3的事務id是8,變更的自身投票就是(8, 3)。

3、首輪投票

將變更的投票發給叢集中所有的Follower節點。server.1將(10, 1)發給叢集中所有Follower,包括它自己。server.3也一樣,將(8, 3)發給所有Follower。

所以server.1將收到(10, 1)和(8, 3)兩個投票,server.3將收到(8, 3)和(10, 1)兩個投票。

4、投票PK

每個Follower節點除了發起投票外,還接其他Follower發來的投票,並與自己的投票PK(比較兩個提議的事務ID以及server.id),PK結果決定是否要變更自身狀態並再次投票。

對於server.1來說收到(10, 1)和(8, 3)兩個投票,與自己變更的投票比較後沒有一個比自身投票(10, 1)要大的,所以server.1維持自身投票不變。

對於server.3來說收到(10, 1)和(8, 3)兩個投票,與自身變更的投票比較後認為server.1發來的投票要比自身的投票大,所以server.3會變更自身投票並將變更後的投票發給叢集中所有Follower。

5、第二輪投票

server.3將自身投票變更為(10, 1)後再次將投票發給叢集中所有Follower。

對於server.1來說在第二輪收到了(10, 1)投票,server.1經過PK後繼續維持不變。

對於server.3來說在第二輪收到了(10, 1)投票,因為server.3自身已變更為(10, 3)投票,所以本次也維持不變。

此時server.1和server.3在投票上達成一致。

6、投票接收桶

節點接收的投票儲存在一個接收桶裡,每個Follower的投票結果在桶內只記錄一次。ZooKeeper原始碼中接收桶用Map實現。

下面程式碼片段是ZooKeeper定義的接收桶,以及向桶內寫入資料。Map.Key是Long型別,用來儲存投票來源節點的server.id,Vote則是對應節點的投票資訊。節點收到投票後會更新這個接收桶,也就是說桶裡儲存了所有Follower節點的投票並且僅存最後一次的投票結果。

HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
複製程式碼

7、統計投票

接收到投票後每次都會嘗試統計投票,投票統計過半後選舉成功。

投票統計的資料來源於投票接收桶裡的投票資料,我們從頭描述這個場景,來看一下接收桶裡的資料變化情況。

server.2掛了後,server.1和server.3發起第一輪投票。

server.1接收到來自server.1的(10, 1)投票和來自server.3的(8, 3)投票。

server.3同樣接收到來自server.1的(10, 1)投票和來自server.3的(8, 3)投票。此時server.1和server.3接收桶裡的資料是這樣的:

面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

server.3經過PK後認為server.1的選票比自己要大,所以變更了自己的投票並重新發起投票。

server.1收到了來自server.3的(10, 1)投票;server.3收到了來自sever.3的(10, 1)投票。此時server.1和server.3接收桶裡的資料變成了這樣:

面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

基於ZooKeeper過半原則:桶內投票選舉server.1作為Leader出現2次,滿足了過半 2 > 3/2 即 2>1。

最後sever.1節點晉升為Leader,server.3變更為Follower。

叢集擴容Leader啟動時機

ZooKeeper叢集擴容需要在zoo.cfg配置檔案中加入新節點。擴容流程在ZooKeeper擴容中介紹。這裡我們以3節點擴容到5節點時,Leader啟動時機做一個討論。

假設目前有3個節點組成叢集,分別是server.1(Follower)、server.2(Leader)、server.3(Follower),假設叢集中節點事務ID相同。配置檔案如下。

server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
複製程式碼

1、新節點加入叢集

叢集中新增server.4和server.5兩個節點,首先修改server.4和server.5的zoo.cfg配置並啟動。節點4和5在啟動後會變更自身投票狀態,發起一輪Leader選舉投票。server.1、server.2、server.3收到投票後由於叢集中已有選定Leader,所以會直接反饋server.4和server.5投票結果:server.2是Leader。server.4和server.5收到投票後基於過半原則認定server.2是Leader,自身便切換為Follower。

#節點server.1、server.2、server.3配置
server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883

#節點server.4、server.5配置
server.1=localhost:2881:3881
server.2=localhost:2882:3882
server.3=localhost:2883:3883
server.4=localhost:2884:3884
server.5=localhost:2885:3885
複製程式碼

2、停止Leader

server.4和server.5的加入需要修改叢集server.1、server.2、server.3的zoo.cfg配置並重啟。但是Leader節點何時重啟是有講究的,因為Leader重啟會導致叢集中Follower發起Leader重新選舉。在server.4和server.5兩個新節點正常加入後,叢集不會因為新節點加入變更Leader,所以目前server.2依然是Leader。

我們以一個錯誤的順序啟動,看一下叢集會發生什麼樣的變化。修改server.2zoo.cfg配置檔案,增加server.4和server.5的配置並停止server.2服務。停止server.2後,Leader不存在了,叢集中所有Follower會發起投票。當server.1和server.3發起投票時並不會將投票發給server.4和server.5,因為在server.1和server.3的叢集配置中不包含server.4和server.5節點。相反,server.4和server.5會把選票發給叢集中所有節點。也就是說對於server.1和server.3他們認為叢集中只有3個節點。對於server.4和server.5他們認為叢集中有5個節點。

根據過半原則,server.1和server.3很快會選出一個新Leader,我們這裡假設server.3晉級成為了新Leader。但是我們沒有啟動server.2的情況下,因為投票不滿足過半原則,server.4和server.5會一直做投票選舉Leader的動作。截止到現在叢集中節點狀態是這樣的:

面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

3、啟動Leader

現在,我們啟動server.2。因為server.2zoo.cfg已經是server.1到serverv.5的全量配置,在server.2啟動後會發起選舉投票,同時serverv.4和serverv.5也在不斷的發起選舉投票。當server.2的選舉輪次和serverv.4與serverv.5選舉輪次對齊後,最終server.2會變更自己的狀態,認定server.5是Leaader。

意想不到的事情發生了,出現兩個Leader:

面試題:說說你對ZooKeeper叢集與Leader選舉的理解?

ZooKeeper叢集擴容時,如果Leader節點最後啟動就可以避免這類問題發生,因為在Leader節點重啟前,所有的Follower節點zoo.cfg配置已經是相同的,他們基於同一個叢集配置兩兩互聯,做投票選舉。

最後

大家覺得不錯可以點個贊在關注下,以後還會分享更多文章!


相關文章