Raft 算法之集羣成員變動

原文地址: https://qeesung.github.io/202...html

Raft 集羣成員變動

在前面三個章節中,咱們介紹了Raft的:git

上面的討論都是基於Raft集羣成員恆定不變的,然而在不少時候,集羣的節點可能須要進行維護,或者是由於須要擴容,那麼就難以免的須要向Raft集羣中添加和刪除節點。最簡單的方式就是中止整個集羣,更改集羣的靜態配置,而後從新啓動集羣,可是這樣就喪失了集羣的可用性,每每是不可取的,因此Raft提供了兩種在不停機的狀況下,動態的更改集羣成員的方式:github

  • 單節點成員變動:One Server ConfChange
  • 多節點聯合共識:Joint Consensus

動態成員變動存在的問題

在Raft中有一個很重要的安全性保證就是隻有一個Leader,若是咱們在不加任何限制的狀況下,動態的向集羣中添加成員,那麼就可能致使同一個任期下存在多個Leader的狀況,這是很是危險的。算法

以下圖所示,從Cold遷移到Cnew的過程當中,由於各個節點收到最新配置的實際不同,那麼肯能致使在同一任期下多個Leader同時存在。安全

好比圖中此時Server3宕機了,而後Server1和Server5同時超時發起選舉:服務器

  • Server1:此時Server1中的配置仍是Cold,只須要Server1和Server2就可以組成集羣的Majority,所以能夠被選舉爲Leader
  • Server5:已經收到Cnew的配置,使用Cnew的配置,此時只須要Server3,Server4,Server5就能夠組成集羣的Majority,由於能夠被選舉爲Leader

換句話說,以Cold和Cnew做爲配置的節點在同一任期下能夠分別選出Leader。網絡

raft-multi-leader.png

因此爲了解決上面的問題,在集羣成員變動的時候須要做出一些限定。spa

單節點成員變動

所謂單節點成員變動,就是每次只想集羣中添加或移除一個節點。好比說之前集羣中存在三個節點,如今須要將集羣拓展爲五個節點,那麼就須要一個一個節點的添加,而不是一次添加兩個節點。日誌

這個爲何安全呢?很容易枚舉出全部狀況,原有集羣奇偶數節點狀況下,分別添加和刪除一個節點。在下圖中能夠看出,若是每次只增長和刪除一個節點,那麼Cold的Majority和Cnew的Majority之間必定存在交集,也就說是在同一個Term中,Cold和Cnew中交集的那一個節點只會進行一次投票,要麼投票給Cold,要麼投票給Cnew,這樣就避免了同一Term下出現兩個Leader。
raft-single-server.pngcode

變動的流程以下:

  1. 向Leader提交一個成員變動請求,請求的內容爲服務節點的是添加仍是移除,以及服務節點的地址信息
  2. Leader在收到請求之後,迴向日誌中追加一條ConfChange的日誌,其中包含了Cnew,後續這些日誌會隨着AppendEntries的RPC同步全部的Follower節點中
  3. ConfChange的日誌被添加到日誌中是當即生效(注意:不是等到提交之後才生效)
  4. ConfChange的日誌被複制到Cnew的Majority服務器上時,那麼就能夠對日誌進行提交了

以上就是整個單節點的變動流程,在日誌被提交之後,那麼就能夠:

  1. 立刻響應客戶端,變動已經完成
  2. 若是變動過程當中移除了服務器,那麼服務器能夠關機了
  3. 能夠開始下一輪的成員變動了,注意在上一次變動沒有結束以前,是不容許開始下一次變動的

可用性

可用性問題

在咱們向集羣添加或者刪除一個節點之後,可能會致使服務的不可用,好比向一個有三個節點的集羣中添加一個乾淨的,沒有任何日誌的新節點,在添加節點之後,原集羣中的一個Follower宕機了,那麼此時集羣中還有三個節點可用,知足Majority,可是由於其中新加入的節點是乾淨的,沒有任何日誌的節點,須要花時間追趕最新的日誌,因此在新節點追趕日誌期間,整個服務是不可用的。

在接下來的子章節中,咱們將會討論三個服務的可用性問題:

  • 追趕新的服務器
  • 移除當前的Leader
  • 中斷服務器

追趕新的服務器

在添加服務器之後,若是新的服務器須要花很長時間來追趕日誌,那麼這段時間內服務不可用。

以下圖所示:

  • 左圖:向集羣中添加新的服務器S4之後,S3宕機了,那麼此時由於S4須要追趕日誌,此時不可用
  • 右圖:向集羣中添加多個服務器,那麼添加之後Majority確定是包含新的服務器的,那麼此時S4,S5,S6須要追趕日誌,確定也是不可用的

raft-catch-up-server.png

新加入集羣中的節點可能並非由於須要追趕大量的日誌而不可用,也有多是由於網絡不通,或者是網速太慢,致使須要花很長的時間追趕日誌。

在Raft中提供了兩種解決方案:

  • 在集羣中加入新的角色LeanerLeaner只對集羣的日誌進行復制,並不參加投票和提交決定,在須要添加新節點的狀況下,添加Leaner便可。
  • 加入一個新的Phase,這個階段會在固定的Rounds(好比10)內嘗試追趕日誌,最後一輪追趕日誌的時間若是小於ElectionTimeout, 那麼說明追遇上了,不然就拋出異常

下面咱們就詳細討論一下第二種方案。

在固定Rounds內追趕日誌

若是須要添加的新的節點在很短期內能夠追遇上最新的日誌,那麼就能夠將該節點添加到集羣中。那要怎麼判斷這個新的節點是否能夠很快時間內追遇上最新的日誌呢?

Raft提供了一種方法,在配置變動以前引入一個新的階段,這個階段會分爲多個Rounds(好比10)向Leader同步日誌,若是新節點可以正常的同步日誌,那麼每一輪的日誌同步時間都將縮短,若是在最後一輪Round同步日誌消耗的時間小於ElectionTimeout,那麼說明新節點的日誌和Leader的日誌已經足夠接近,能夠將新節點加入到集羣中。可是若是最後一輪的Round的日誌同步時間大於ElectionTimeout,就應該當即終止成員變動。

raft-replicate-round.png

移除當前的Leader

若是在Cnew中不包含當前的Leader所在節點,那麼若是Leader在收到Cnew配置之後,立刻退位成爲Follower,那麼將會致使下面的問題:

  • ConfChange的日誌還沒有複製到Cnew中的大多數的節點
  • 立刻退位成爲Follower的可能由於超時成爲新的Leader,由於該節點上的日誌是最新的,由於日誌的安全性,該節點並不會爲其餘節點投票

爲了解決以上的問題,一種很簡單的方式就是經過Raft的拓展Leadership transfer首先將Leader轉移到其餘節點,而後再進行成員變動,可是對於不支持Leadership transfer的服務來講就行不通了。

Raft中提供了一種策略,Leader應該在Cnew日誌提交之後才退位。

中斷的服務器

若是Cnew中移除了原有集羣中的節點,由於被移除的節點是不會再收到心跳信息,那麼將會超時發起一輪選舉,將會形成當前的Leader成爲Follower,可是由於被移除的節點不包含Cnew的配置,因此最終會致使Cnew中的部分節點超時,從新選舉Leader。如此反反覆覆的選舉將會形成不好的可用性。

一種比較直觀的方式是採用Pre-Vote方式,在任何節點發起一輪選舉以前,就應該提早的發出一個Pre-Vote的RPC詢問是否當前節點會贊成給當前節點投票,若是超過半數的節點贊成投票,那麼才發生真正的投票流程的,有點相似於Two-Phase-Commit,這種方式在正常狀況下,由於被移除的節點沒有包含Cnew的ConfChange日誌,因此在Pre-Vote狀況下,大多數節點都會拒絕已經被移除節點的Pre-Vote請求。

可是上面只能處理大多數正常的狀況,若是Leader收到Cnew的請求後,還沒有將Cnew的ConfChange日誌複製到集羣中的大多數,Cnew中被移除的節點就超時開始選舉了,那麼Pre-Vote此時是沒有用的,被移除的節點仍有可能選舉成功。順便一說,這裏的Pre-Vote雖然不能解決目前的問題,可是針對腦裂而產生的任期爆炸式增加和頗有用的,這裏就不展開討論了。

就以下圖所示,S4收到Cnew成員變動的請求,立馬將其寫入日誌中,Cnew中並不包含S1節點,因此在S4將日誌複製到S2,S3以前,若是S1超時了,S2,S3中由於沒有最新的Cnew日誌,仍讓會投票給S1,此時S1就能選舉成功,這不是咱們想看到的。

raft-disruptive-server.png

Raft中提供了另外一種方式來避免這個問題,若是每個服務器若是在ElectionTimeout內收到現有Leader的心跳(換句話說,在租約期內,仍然臣服於其餘的Leader),那麼就不會更新本身的現有Term以及贊成投票。這樣每個Follower就會變得很穩定,除非本身已經知道的Leader已經不發送心跳給本身了,不然會一直臣服於當前的leader,儘管收到其餘更高的Term的服務器投票請求。

任意節點的Joint Consensus

上面咱們提到單節點的成員變動,不少時候這已經能知足咱們的需求了,可是有些時候咱們可能會須要隨意的的集羣成員變動,每次變動多個節點,那麼咱們就須要Raft的Joint Consensus, 儘管這會引入不少的複雜性。

Joint Consensus會將集羣的配置轉換到一個臨時狀態,而後開始變動:

  1. Leader收到Cnew的成員變動請求,而後生成一個Cold,new的ConfChang日誌,立刻應用該日誌,而後將日誌經過AppendEntries請求複製到Follower中,收到該ConfChange的節點立刻應用該配置做爲當前節點的配置
  2. 在將Cold,new日誌複製到大多數節點上時,那麼Cold,new的日誌就能夠提交了,在Cold,new的ConfChange日誌被提交之後,立刻建立一個Cnew的ConfChange的日誌,並將該日誌經過AppendEntries請求複製到Follower中,收到該ConfChange的節點立刻應用該配置做爲當前節點的配置
  3. 一旦Cnew的日誌複製到大多數節點上時,那麼Cnew的日誌就能夠提交了,在Cnew日誌提交之後,就能夠開始下一輪的成員變動了

爲了理解上面的流程,咱們有幾個概念須要解釋一下:

  • Cold,new:這個配置是指Cold,和Cnew的聯合配置,其值爲Cold和Cnew的配置的交集,好比Cold爲[A, B, C], Cnew爲[B, C, D],那麼Cold,new就爲[A, B, C, D]
  • Cold,new的大多數:是指Cold中的大多數和Cnew中的大多數,以下表所示,第一列由於Cnew的C, D沒有Replicate到日誌,因此並不能達到一致
Cold Cnew Replicate結果 是不是Majority
A, B, C B, C, D A+, B+, C-, D-
A, B, C B, C, D A+, B+, C+, D-
A, B, C B, C, D A-, B+, C+, D-

由上能夠看出,整個集羣的變動分爲幾個過渡期,就以下圖所示,在每個時期,每個任期下都不可能出現兩個Leader:

  1. Cold,new日誌在提交以前,在這個階段,Cold,new中的全部節點有可能處於Cold的配置下,也有可能處於Cold,new的配置下,若是這個時候原Leader宕機了,不管是發起新一輪投票的節點當前的配置是Cold仍是Cold,new,都須要Cold的節點贊成投票,因此不會出現兩個Leader
  2. Cold,new提交以後,Cnew下發以前,此時全部Cold,new的配置已經在Cold和Cnew的大多數節點上,若是集羣中的節點超時,那麼確定只有有Cold,new配置的節點才能成爲Leader,因此不會出現兩個Leader
  3. Cnew下發之後,Cnew提交以前,此時集羣中的節點可能有三種,Cold的節點(可能一直沒有收到請求), Cold,new的節點,Cnew的節點,其中Cold的節點由於沒有最新的日誌的,集羣中的大多數節點是不會給他投票的,剩下的持有Cnew和Cold,new的節點,不管是誰發起選舉,都須要Cnew贊成,那麼也是不會出現兩個Leader
  4. Cnew提交以後,這個時候集羣處於Cnew配置下運行,只有Cnew的節點才能夠成爲Leader,這個時候就能夠開始下一輪的成員變動了

raft-multi-server.png

其餘

自動的成員變動

若是咱們給集羣增長一些監控,好比在檢測到機器宕機的狀況下,動態的向系統中增長新的節點,這樣就能夠作到自動化,增長系統的節點數。

集羣動態配置

通常狀況下,咱們都是使用靜態文件的方式來描述集羣中的成員信息,可是有了成員變動的算法,咱們就能夠動態配置的方式來設置集羣的配置信息

相關文章
相關標籤/搜索