ETCD的Raft一致性演算法原理
前言
關於Raft的文章很多,本文是參考了很多的文章之後,總結出來的,寫的不對之處歡迎賜教。
Raft原理了解
Raft 是一種為了管理複製日誌的一致性演算法。它提供了和 Paxos 演算法相同的功能和效能,但是它的演算法結構和 Paxos 不同,使得 Raft 演算法更加容易理解並且更容易構建實際的系統。
Raft是一種分散式一致性演算法。它被設計得易於理解, 解決了即使在出現故障時也可以讓多個伺服器對共享狀態達成一致的問題。共享狀態通常是通過日誌複製支援的資料結構。只要大多數伺服器是正常運作的,系統就能全面執行。
Raft的工作方式是在叢集中選舉一個領導者。領導者負責接受客戶端請求並管理到其他伺服器的日誌複製。資料只在一個方向流動:從領導者到其他伺服器。
Raft將一致性問題分解為三個子問題:
-
領導者選舉: 現有領導者失效時,需要選舉新的領導者;
-
日誌複製: 領導者需要通過複製保持所有伺服器的日誌與自己的同步;
-
安全性: 如果其中一個伺服器在特定索引上提交了日誌條目,那麼其他伺服器不能在該索引應用不同的日誌條目。
raft選舉
raft中的幾種狀態
在raft演算法中,在任何時刻,每一個伺服器節點都處於這三個狀態之一:
-
Follower:追隨者,跟隨者都是被動的:他們不會傳送任何請求,只是簡單的響應來自領導者或者候選人的請求;
-
Candidate:候選人,如果跟隨者接收不到訊息,那麼他就會變成候選人併發起一次選舉,獲得叢集中大多數選票的候選人將成為領導者。
-
Leader:領導者,系統中只有一個領導人並且其他的節點全部都是跟隨者,領導人處理所有的客戶端請求(如果一個客戶端和跟隨者聯絡,那麼跟隨者會把請求重定向給領導人)
來看下幾個狀態的關係
任期
Raft將時間劃分為任意長度的任期,每個任期都以一次選舉開始。如果一名候選人贏得選舉,他在剩下的任期時間內仍然是領導者。如果投票出現分歧,那麼這個任期則沒有領導者,及時結束。
任期號單調遞增。每個伺服器儲存當前任期號,並在每次通訊中交換該任期編號。
如果一個伺服器的當前任期號小於其他伺服器,那麼它將把當前任期更新為更大的值。如果候選人或領導者發現其任期已過期,則立即轉化為追隨者狀態。如果伺服器接收到帶有過期任期號的請求,它將拒絕該請求。
leader選舉
領導者定期向跟隨者傳送心跳,來維持自己的leader角色。如果跟隨者在一定的時間內沒有接收到任何的訊息,也就是選舉超時,那麼他就會認為系統中沒有可用的領導者,並且發起選舉以選出新的領導者。
要開始一次選舉過程,跟隨者先要增加自己的當前任期號並且轉換到候選人狀態。然後他會並行的向叢集中的其他伺服器節點傳送請求投票的 RPCs 來給自己投票。
候選人的選舉會有下面三種結果:
1、候選人自己贏得了選舉;
2、其他服務成為了leader;
3、候選人中沒有選出領導者,可能是多個跟隨者同時成為候選人,然後選票被瓜分了,以至於沒有候選人能獲得最大的票數。這種情況下面詳細介紹。
對於選舉過程,對於選票被瓜分的情況,Raft演算法使用隨機候選超時時間的方法來確保很少會發生選票瓜分的情況,就算髮生也能很快的解決。
來看下這個選舉的隨機演算法:
1、為了阻止選票起初就被瓜分,候選超時時間是從一個固定的區間(例如 150-300 毫秒)隨機選擇;
2、這個候選超時時間就是follower要等待成為candidate的時間;
3、每一個候選人在開始一次選舉的時候會重置一個隨機候選的時間,也就是150-300中隨機一個值;
4、這個時間結束之後follower變成candidate開始選舉,不同時候甦醒競爭leader,這樣甦醒早的就有競爭優勢;
5、這樣大大減少了選票被瓜分的情況,如何選票還是被瓜分,就繼續從1開始選舉。
日誌複製
一旦leader被選舉成功,就可以對客戶端提供服務了。客戶端提交每一條命令都會被按順序記錄到leader的日誌中,每一條命令都包含term編號和順序索引,然後向其他節點並行傳送AppendEntries RPC用以複製命令(如果命令丟失會不斷重發),當複製成功也就是大多數節點成功複製後,leader就會提交命令,即執行該命令並且將執行結果返回客戶端,raft保證已經提交的命令最終也會被其他節點成功執行。
來看下是具體的流程:
1、所有的請求都先經過leader,每個請求首先以日誌的形式儲存在leader中,然後這時候日誌的狀態是uncommited狀態;
2、然後leader將這些更改的請求傳送到follower;
3、leader等待大多數的follower確認提交;
4、leader在等待大多數的follower確認提交之後,commit這些更改,然後通知客戶端更新的結果;
5、同時leader會不斷的嘗試通知follower去儲存所有更新的資訊。
日誌由有序編號(log index)的日誌條目組成。每個日誌條目包含它被建立時的任期號(term),和用於狀態機執行的命令。如果一個日誌條目被複制到大多數伺服器上,就被認為可以提交(commit)了。
Raft日誌同步保證如下兩點:
-
如果不同日誌中的兩個條目有著相同的索引和任期號,則它們所儲存的命令是相同的;
-
如果不同日誌中的兩個條目有著相同的索引和任期號,則它們之前的所有條目都是完全一樣的。
第一條特性源於Leader在一個term內在給定的一個log index最多建立一條日誌條目,同時該條目在日誌中的位置也從來不會改變。
第二條特性:Raft演算法在傳送日誌複製請求時會攜帶前置日誌的term和logIndex值(即 prevLogTerm 和 prevLogIndex),只有在 prevLogTerm 和 prevLogIndex 匹配的情況下才能成功響應請求。如果prevLogTerm和prevLogIndex不匹配,則說明當前節點可能是新加入的、或者之前服從於其它Leader,亦或當前節點之前是Leader節點。為了兌現承諾二,Leader節點需要與該Follower節點向前追溯找到term和logIndex匹配的那條日誌,並使用Leader節點的日誌強行覆蓋該Follower此後的日誌資料。
一般情況下,Leader和Followers的日誌保持一致,因此AppendEntries一致性檢查通常不會失敗。然而,Leader崩潰可能會導致日誌不一致:舊的Leader可能沒有完全複製完日誌中的所有條目。一個Follower可能會丟失掉Leader上的一些條目,也有可能包含一些Leader沒有的條目,也有可能兩者都會發生。丟失的或者多出來的條目可能會持續多個任期。
Leader通過強制Followers複製它的日誌來處理日誌的不一致,Followers上的不一致的日誌會被Leader的日誌覆蓋。
Leader為了使Followers的日誌同自己的一致,Leader需要找到Followers同它的日誌一致的地方,然後覆蓋Followers在該位置之後的條目。
Leader會從後往前試,每次AppendEntries失敗後嘗試前一個日誌條目,直到成功找到每個Follower的日誌一致位點,然後向後逐條覆蓋Followers在該位置之後的條目。
安全性
上面我們討論的是理想狀態下的情況,在實際的生產環境中,我們會遇到各種各樣的情況。這裡參考了一位大佬文章理解 Raft 分散式共識演算法
下面來討論幾種常見的問題
leader當機,新的leader未同步前任committed的資料
leader當機了,然後又選出了新的leader,但是新的leader沒有同步前任committed的資料,新leader節點會強行覆蓋叢集中其它節點與自己衝突的日誌資料。
如何避免:
這種情況raft會對參加選舉的節點進行限制,只有包含已經committed日誌的節點才有機會競選成功
-
1、參選節點的term值大於等於投票節點的term值;
-
2、如果 term 值相等,則參選節點的 lastLogIndex 大於等於投票節點的 lastLogIndex 值。
Leader在將日誌複製給Follower節點之前當機
如果在複製之前當機,當然這時候訊息處於uncommitted狀態,新選出的leader一定不包含這些日誌資訊,所以新的leader會強制覆蓋follower中跟他衝突的日誌,也就是剛剛當機的leader,如果變成follower,他未同步的資訊會被新的leader覆蓋掉。
Leader在將日誌複製給Follower節點之間當機
在複製的過程中當機,會有兩種情況:
-
1、只有少數的follower被同步到了;
-
2、大多數的follower被同步到了;
情況1:如果只有少數的follower被同步了,如果新的leader不包含這些資訊,新的leader會覆蓋那些已經同步的節點的資訊,如果新的節點包含這些資料,直接走到下面的情況2;
情況2:Leader在複製的過程中當機,所以肯定訊息是沒有commit的,新的leader需要再次嘗試將其複製給各個Follower節點,並依據自己的複製狀態決定是否提交這些日誌。
Leader在響應客戶端之前當機
這種情況,我們根據上面的同步機制可以知道,訊息肯定是committed狀態的,新的leader肯定包含這個資訊,但是新任Leader可能還未被通知該日誌已經被提交,不過這個資訊在之後一定會被新任Leader標記為committed。
不過對於客戶端可能超時拿不到結果,認為本次訊息失敗了,客戶端需要考慮冪等性。
時間和可用性
Raft 的要求之一就是安全性不能依賴時間:整個系統不能因為某些事件執行的比預期快一點或者慢一點就產生了錯誤的結果。但是,可用性(系統可以及時的響應客戶端)不可避免的要依賴於時間。
領導人選舉是 Raft 中對時間要求最為關鍵的方面。Raft 可以選舉並維持一個穩定的領導人,只要系統滿足下面的時間要求:
廣播時間(broadcastTime) << 候選超時時間(electionTimeout) << 平均故障間隔時間(MTBF)
-
broadcastTime: 廣播時間指的是從一個伺服器並行的傳送 RPCs 給叢集中的其他伺服器並接收響應的平均時間,也就是叢集之間的平均延時時間;
-
electionTimeout: 追隨者設定的候選超時時間;
-
MTBF:平均故障間隔時間就是對於一臺伺服器而言,兩次故障之間的平均時間。
如果一個follower在一個electionTimeout時間內沒有接收到leader的RPC,也沒有接收到其他candidate的voteRequestRPC,他就會甦醒,變成candidate狀態,開始新一輪的投票。所以broadcastTime要小於electionTimeout的時間。
在Leader當機與選舉出新任Leader之間,整個叢集處於無主的狀態,我們應該儘可能縮短此類狀態的持續時間,而控制的引數就是electionTimeout的最小值,所以electionTimeout需要在保證大於broadcastTime的前提下遠小於一個叢集中機器的平均故障間隔時間MTBF。
網路分割槽問題
如果由於網路的隔離,導致原來的Raft叢集分裂成多個小的叢集,各自分割槽中會重新開始選舉形各自形成新的leader
在各自分割槽之內,各自leader會收到不同的client傳送的請求,但是我們叢集的節點還是5,所以兩個節點的分割槽必定選不出leader。
-
如果請求傳送給了3個節點的分割槽,因為叢集包含3個節點,所以提交給該分割槽的指令對應的日誌存在被committed的可能性,此時3個節點均成功複製了日誌。
-
如果請求傳送給了2個節點的分割槽,因為叢集包含2個節點,所以提交給該叢集的指令對應的日誌因不滿足過半數的條件而無法被提交。
當網路恢復的時候,新的leader必定在三個叢集的節點中選取(為什麼呢?可參看上文的安全性),然後新的leader覆蓋未同步的2個節點中的資料。
總結
1、Raft在對應的任期中每次只有一個leader產生,通過候選超時演算法,保證了在大多數只有一個leader被選出的情況;
2、所有的資料都是從leader流向follower中,通過日誌的複製確認機制,保證絕大多數的follower都能同步到訊息;
3、當然,raft對於分散式中出現的各種安全性問題也做了相容;
4、不過真正實現一個生產級別的Raft演算法庫,需要考慮的東西還是很多,這裡主要分析了幾個主要的問題。
參考
【一文搞懂Raft演算法】https://www.cnblogs.com/xybaby/p/10124083.html
【尋找一種易於理解的一致性演算法(擴充套件版)】https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md
【raft演示動畫】https://raft.github.io/raftscope/index.html
【理解 raft 演算法】https://sanyuesha.com/2019/04/18/raft/
【理解Raft一致性演算法—一篇學術論文總結】https://mp.weixin.qq.com/s/RkMeYyUck1WQPjNiGvahKQ
【Raft協議原理詳解】https://zhuanlan.zhihu.com/p/91288179
【Raft演算法詳解】https://zhuanlan.zhihu.com/p/32052223