原文地址: https://qeesung.github.io/202...html
在前面三個章節中,咱們介紹了Raft的:git
上面的討論都是基於Raft集羣成員恆定不變的,然而在不少時候,集羣的節點可能須要進行維護,或者是由於須要擴容,那麼就難以免的須要向Raft集羣中添加和刪除節點。最簡單的方式就是中止整個集羣,更改集羣的靜態配置,而後從新啓動集羣,可是這樣就喪失了集羣的可用性,每每是不可取的,因此Raft提供了兩種在不停機的狀況下,動態的更改集羣成員的方式:github
One Server ConfChange
Joint Consensus
在Raft中有一個很重要的安全性保證就是隻有一個Leader,若是咱們在不加任何限制的狀況下,動態的向集羣中添加成員,那麼就可能致使同一個任期下存在多個Leader的狀況,這是很是危險的。算法
以下圖所示,從Cold遷移到Cnew的過程當中,由於各個節點收到最新配置的實際不同,那麼肯能致使在同一任期下多個Leader同時存在。安全
好比圖中此時Server3宕機了,而後Server1和Server5同時超時發起選舉:服務器
換句話說,以Cold和Cnew做爲配置的節點在同一任期下能夠分別選出Leader。網絡
因此爲了解決上面的問題,在集羣成員變動的時候須要做出一些限定。spa
所謂單節點成員變動
,就是每次只想集羣中添加或移除一個節點。好比說之前集羣中存在三個節點,如今須要將集羣拓展爲五個節點,那麼就須要一個一個節點的添加,而不是一次添加兩個節點。日誌
這個爲何安全呢?很容易枚舉出全部狀況,原有集羣奇偶數節點狀況下,分別添加和刪除一個節點。在下圖中能夠看出,若是每次只增長和刪除一個節點,那麼Cold的Majority和Cnew的Majority之間必定存在交集,也就說是在同一個Term中,Cold和Cnew中交集的那一個節點只會進行一次投票,要麼投票給Cold,要麼投票給Cnew,這樣就避免了同一Term下出現兩個Leader。code
變動的流程以下:
ConfChange
的日誌,其中包含了Cnew,後續這些日誌會隨着AppendEntries
的RPC同步全部的Follower節點中ConfChange
的日誌被添加到日誌中是當即生效(注意:不是等到提交之後才生效)ConfChange
的日誌被複制到Cnew的Majority服務器上時,那麼就能夠對日誌進行提交了以上就是整個單節點的變動流程,在日誌被提交之後,那麼就能夠:
在咱們向集羣添加或者刪除一個節點之後,可能會致使服務的不可用,好比向一個有三個節點的集羣中添加一個乾淨的,沒有任何日誌的新節點,在添加節點之後,原集羣中的一個Follower宕機了,那麼此時集羣中還有三個節點可用,知足Majority,可是由於其中新加入的節點是乾淨的,沒有任何日誌的節點,須要花時間追趕最新的日誌,因此在新節點追趕日誌期間,整個服務是不可用的。
在接下來的子章節中,咱們將會討論三個服務的可用性問題:
在添加服務器之後,若是新的服務器須要花很長時間來追趕日誌,那麼這段時間內服務不可用。
以下圖所示:
新加入集羣中的節點可能並非由於須要追趕大量的日誌而不可用,也有多是由於網絡不通,或者是網速太慢,致使須要花很長的時間追趕日誌。
在Raft中提供了兩種解決方案:
Leaner
,Leaner
只對集羣的日誌進行復制,並不參加投票和提交決定,在須要添加新節點的狀況下,添加Leaner
便可。ElectionTimeout
, 那麼說明追遇上了,不然就拋出異常下面咱們就詳細討論一下第二種方案。
若是須要添加的新的節點在很短期內能夠追遇上最新的日誌,那麼就能夠將該節點添加到集羣中。那要怎麼判斷這個新的節點是否能夠很快時間內追遇上最新的日誌呢?
Raft提供了一種方法,在配置變動以前引入一個新的階段,這個階段會分爲多個Rounds(好比10)向Leader同步日誌,若是新節點可以正常的同步日誌,那麼每一輪的日誌同步時間都將縮短,若是在最後一輪Round同步日誌消耗的時間小於ElectionTimeout
,那麼說明新節點的日誌和Leader的日誌已經足夠接近,能夠將新節點加入到集羣中。可是若是最後一輪的Round的日誌同步時間大於ElectionTimeout
,就應該當即終止成員變動。
若是在Cnew中不包含當前的Leader所在節點,那麼若是Leader在收到Cnew配置之後,立刻退位成爲Follower,那麼將會致使下面的問題:
ConfChange
的日誌還沒有複製到Cnew中的大多數的節點爲了解決以上的問題,一種很簡單的方式就是經過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中提供了另外一種方式來避免這個問題,若是每個服務器若是在ElectionTimeout內收到現有Leader的心跳(換句話說,在租約期內,仍然臣服於其餘的Leader),那麼就不會更新本身的現有Term以及贊成投票。這樣每個Follower就會變得很穩定,除非本身已經知道的Leader已經不發送心跳給本身了,不然會一直臣服於當前的leader,儘管收到其餘更高的Term的服務器投票請求。
上面咱們提到單節點的成員變動,不少時候這已經能知足咱們的需求了,可是有些時候咱們可能會須要隨意的的集羣成員變動,每次變動多個節點,那麼咱們就須要Raft的Joint Consensus
, 儘管這會引入不少的複雜性。
Joint Consensus
會將集羣的配置轉換到一個臨時狀態,而後開始變動:
AppendEntries
請求複製到Follower中,收到該ConfChange的節點立刻應用該配置做爲當前節點的配置AppendEntries
請求複製到Follower中,收到該ConfChange的節點立刻應用該配置做爲當前節點的配置爲了理解上面的流程,咱們有幾個概念須要解釋一下:
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:
若是咱們給集羣增長一些監控,好比在檢測到機器宕機的狀況下,動態的向系統中增長新的節點,這樣就能夠作到自動化,增長系統的節點數。
通常狀況下,咱們都是使用靜態文件的方式來描述集羣中的成員信息,可是有了成員變動的算法,咱們就能夠動態配置的方式來設置集羣的配置信息