Raft 演算法之叢集成員變更

qeesung發表於2020-05-31

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

Raft 叢集成員變更

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

上面的討論都是基於Raft叢集成員恆定不變的,然而在很多時候,叢集的節點可能需要進行維護,或者是因為需要擴容,那麼就難以避免的需要向Raft叢集中新增和刪除節點。最簡單的方式就是停止整個叢集,更改叢集的靜態配置,然後重新啟動叢集,但是這樣就喪失了叢集的可用性,往往是不可取的,所以Raft提供了兩種在不停機的情況下,動態的更改叢集成員的方式:

  • 單節點成員變更: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

所以為了解決上面的問題,在叢集成員變更的時候需要作出一些限定。

單節點成員變更

所謂單節點成員變更,就是每次只想叢集中新增或移除一個節點。比如說以前叢集中存在三個節點,現在需要將叢集擴充為五個節點,那麼就需要一個一個節點的新增,而不是一次新增兩個節點。

這個為什麼安全呢?很容易列舉出所有情況,原有叢集奇偶數節點情況下,分別新增和刪除一個節點。在下圖中可以看出,如果每次只增加和刪除一個節點,那麼Cold的Majority和Cnew的Majority之間一定存在交集,也就說是在同一個Term中,Cold和Cnew中交集的那一個節點只會進行一次投票,要麼投票給Cold,要麼投票給Cnew,這樣就避免了同一Term下出現兩個Leader。
raft-single-server.png

變更的流程如下:

  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

其他

自動的成員變更

如果我們給叢集增加一些監控,比如在檢測到機器當機的情況下,動態的向系統中增加新的節點,這樣就可以做到自動化,增加系統的節點數。

叢集動態配置

一般情況下,我們都是使用靜態檔案的方式來描述叢集中的成員資訊,但是有了成員變更的演算法,我們就可以動態配置的方式來設定叢集的配置資訊

相關文章