-
算法基礎
-
選舉和日誌複製
-
安全性
-
節點變更
第一篇:《解讀Raft(一 算法基礎)》
什麼是RAFT
分佈式系統除了提升整個體統的性能外還有一個重要特徵就是提高系統的可靠性。
提供可靠性可以理解爲系統中一臺或多臺的機器故障不會使系統不可用(或者丟失數據)。
保證系統可靠性的關鍵就是多副本(即數據需要有備份),一旦有多副本,那麼久面臨多副本之間的一致性問題。
一致性算法正是用於解決分佈式環境下多副本之間數據一致性的問題的。
業界最著名的一致性算法就是大名鼎鼎的Paxos(Chubby的作者曾說過:世上只有一種一致性算法,就是Paxos)。但Paxos是出了名的難懂,而Raft正是爲了探索一種更易於理解的一致性算法而產生的。
Raft is a consensus algorithm for managing a replicated log.
Raft是一種管理複製日誌的一致性算法。
它的首要設計目的就是易於理解,所以在選主的衝突處理等方式上它都選擇了非常簡單明瞭的解決方案。
Raft將一致性拆分爲幾個關鍵元素:
-
Leader選舉
-
日誌複製
-
安全性
Raft算法
所有一致性算法都會涉及到狀態機,而狀態機保證系統從一個一致的狀態開始,以相同的順序執行一些列指令最終會達到另一個一致的狀態。
以上是狀態機的示意圖。所有的節點以相同的順序處理日誌,那麼最終x、y、z的值在多個節點中都是一致的。
算法基礎
角色
Raft通過選舉Leader並由Leader節點負責管理日誌複製來實現多副本的一致性。
在Raft中,節點有三種角色:
-
Leader:負責接收客戶端的請求,將日誌複製到其他節點並告知其他節點何時應用這些日誌是安全的
-
Candidate:用於選舉Leader的一種角色
-
Follower:負責響應來自Leader或者Candidate的請求
角色轉換如下圖所示:
-
所有節點初始狀態都是Follower角色
-
超時時間內沒有收到Leader的請求則轉換爲Candidate進行選舉
-
Candidate收到大多數節點的選票則轉換爲Leader;發現Leader或者收到更高任期的請求則轉換爲Follower
-
Leader在收到更高任期的請求後轉換爲Follower
任期
Raft把時間切割爲任意長度的任期,每個任期都有一個任期號,採用連續的整數。
每個任期都由一次選舉開始,若選舉失敗則這個任期內沒有Leader;如果選舉出了Leader則這個任期內有Leader負責集羣狀態管理。
算法
狀態
狀態 | 所有節點上持久化的狀態(在響應RPC請求之前變更且持久化的狀態) |
---|---|
currentTerm | 服務器的任期,初始爲0,遞增 |
votedFor | 在當前獲得選票的候選人的 Id |
log[] | 日誌條目集;每一個條目包含一個用戶狀態機執行的指令,和收到時的任期號 |
狀態 | 所有節點上非持久化的狀態 |
---|---|
commitIndex | 最大的已經被commit的日誌的index |
lastApplied | 最大的已經被應用到狀態機的index |
狀態 | Leader節點上非持久化的狀態(選舉後重新初始化) |
---|---|
nextIndex[] | 每個節點下一次應該接收的日誌的index(初始化爲Leader節點最後一個日誌的Index + 1) |
matchIndex[] | 每個節點已經複製的日誌的最大的索引(初始化爲0,之後遞增) |
AppendEntries RPC
用於Leader節點複製日誌給其他節點,也作爲心跳。
參數 | 解釋 |
---|---|
term | Leader節點的任期 |
leaderId | Leader節點的ID |
prevLogIndex | 此次追加請求的上一個日誌的索引 |
prevLogTerm | 此次追加請求的上一個日誌的任期 |
entries[] | 追加的日誌(空則爲心跳請求) |
leaderCommit | Leader上已經Commit的Index |
prevLogIndex和prevLogTerm表示上一次發送的日誌的索引和任期,用於保證收到的日誌是連續的。
返回值 | 解釋 |
---|---|
term | 當前任期號,用於Leader節點更新自己的任期(應該說是如果這個返回值比Leader自身的任期大,那麼Leader需要更新自己的任期) |
success | 如何Follower節點匹配prevLogIndex和prevLogTerm,返回true |
接收者實現邏輯
-
返回false,如果收到的任期比當前任期小
-
返回false,如果不包含之前的日誌條目(沒有匹配prevLogIndex和prevLogTerm)
-
如果存在index相同但是term不相同的日誌,刪除從該位置開始所有的日誌
-
追加所有不存在的日誌
-
如果leaderCommit>commitIndex,將commitIndex設置爲commitIndex = min(leaderCommit, index of last new entry)
RequestVote RPC
用於Candidate獲取選票。
參數 | 解釋 |
---|---|
term | Candidate的任期 |
candidateId | Candidate的ID |
lastLogIndex | Candidate最後一條日誌的索引 |
lastLogTerm | Candidate最後一條日誌的任期 |
參數 | 解釋 |
---|---|
term | 當前任期,用於Candidate更新自己的任期 |
voteGranted | true表示給Candidate投票 |
接收者的實現邏輯
-
返回false,如果收到的任期比當前任期小
-
如果本地狀態中votedFor爲null或者candidateId,且candidate的日誌等於或多餘(按照index判斷)接收者的日誌,則接收者投票給candidate,即返回true
節點的執行規則
所有節點
-
如果commitIndex > lastApplied,應用log[lastApplied]到狀態機,增加lastApplied
-
如果RPC請求或者響應包含的任期T > currentTerm,將currentTerm設置爲T並轉換爲Follower
Followers
-
響應來自Leader和Candidate的RPC請求
-
如果在選舉超時週期內沒有收到AppendEntries的請求或者給Candidate投票,轉換爲Candidate角色
Candidates
-
轉換爲candidate角色,開始選舉:
-
遞增currentTerm
-
給自己投票
-
重置選舉時間
-
發送RequestVote給其他所有節點
-
如果收到了大多數節點的選票,轉換爲Leader節點
-
如果收到Leader節點的AppendEntries請求,轉換爲Follower節點
-
如果選舉超時,重新開始新一輪的選舉
Leaders
-
一旦選舉完成:發送心跳給所有節點;在空閒的週期內不斷髮送心跳保持Leader身份
-
如果收到客戶端的請求,將日誌追加到本地log,在日誌被應用到狀態機後響應給客戶端
-
如果對於一個跟隨者,最後日誌條目的索引值大於等於 nextIndex,那麼:發送從 nextIndex 開始的所有日誌條目:
-
如果成功:更新相應跟隨者的 nextIndex 和 matchIndex
-
如果因爲日誌不一致而失敗,減少 nextIndex 重試
-
如果存在一個滿足N > commitIndex的 N,並且大多數的matchIndex[i] ≥ N成立,並且log[N].term == currentTerm成立,那麼令commitIndex等於這個N
解讀Raft(二 選舉和日誌複製)
Leader election
Raft採用心跳機制來觸發Leader選舉。Leader週期性的發送心跳(如果有正常的RPC的請求情況下可以不發心跳)包保持自己Leader的角色(避免集羣中其他節點認爲沒有Leader而開始選舉)。
Follower在收到Leader或者Candidate的RPC請求的情況下一直保持Follower狀態。而當一段時間內(election timeout)沒有收到請求則認爲沒有Leader節點而出發選舉流程。
選舉流程如下:
- Follower遞增自己的任期並設置爲Candidate角色
- 投票給自己並且併發的給所有節點發送投票請求
- 保持Candidate狀態直到:
- 同一個任期內獲得大多數選票,成爲Leader(一個節點在一個任期內只能給一個Candidate投票,任期相同則選票先到先得)並給其他節點發送心跳來保持自己的角色
- 收到其他節點的RPC請求,如果請求中的任期大於等於Candidate當前的任期,認爲其他節點成爲了Leader,自身轉換爲Follower;如果其他節點的任期小於自身的任期,拒絕RPC請求並保持Candidate角色
- 一段時間後仍舊沒有Leader(可能是出現了平票的情況),則在選舉超時後重新發起一輪選舉(遞增任期、發送投票請求)
爲了避免平票的問題,同時在出現平票的情況後能快速解決,Raft的選舉超時時間是在一個區間內隨機選擇的(150~300ms)。這樣儘量把服務器選舉時間分散到不同的時間,保證大多數情況下只有一個節點會發起選舉。在平票的情況下,每個節點也會在一個隨機時間後開始新一輪選舉,避免可能出現的一直處於平票的情況。
Log replication
一旦Leader被選舉出來後,Leader就開始爲集羣服務:處理所有的客戶端請求並將數據複製到所有節點。
一旦日誌被“安全”的複製,那麼Leader將這個日誌應用到自己的狀態機並響應客戶端。
如果有節點異常或網絡異常,Leader會一直重試直到所有日誌都會正確複製到所有節點(日誌不允許有空洞,所以每個節點上的日誌都是連續的,不能有因爲失敗引起的空洞)。
日誌組織形式如上圖,每個日誌條目中包含可執行的指令、和日誌被創建時的任期號,日誌條目也包含了自己在日誌中的位置,即index。一旦一個日誌條目存在於大多數節點,那麼該日誌條目是committed的。
Raft算法保證所有committed的日誌都是持久化的(日誌需要在大多數節點上持久化之後再響應給客戶端,這意味着每個Follower節點收到AppendEntry請求後需要持久化到日誌之後再響應給Leader),且最終會被所有的狀態機執行。
Raft算法保證了以下特性:
- 如果兩個日誌條目有相同的index和term,那麼他們存儲了相同的指令(即index和term相同,那麼可定是同一條指令,就是同一個日誌條目)
- 如果不同的日誌中有兩個日誌條目,他們的index和term相同,那麼這個條目之前的所有日誌都相同
兩條規則合併起來的含義:兩個日誌LogA、LogB,如果LogA[i].index=Log[i]B.index且LogA[i].term=Log[i].term,那麼LogA[i]=Log[i]B,且對於任何n < i的日誌條目,LogA[n]=LogB[n]都成立。(這個結論顯而易見的可以從日誌複製規則中推導出來)
一個新Leader被選舉出來時,Follower可能是上圖中的任何一種情況。
- (a)(b)可能還沒複製到日誌
- (c)(d)可能曾經是Leader,所有包含了多餘的日誌(這些日誌可能被提交了,也可能沒提交)
- (e)可能是成爲Leader之後增加了一些日誌,但是在Commit之前又編程了Follower角色,且還沒有更新日誌條目
- (f)可能是在任期2稱爲了Leader並追加了日誌但是還沒提交就Crash了,恢復之後在任期3又成了Leader並且又追加了日誌
在Raft中,通過使用Leader的日誌覆蓋Follower的日誌的方式來解決出現像上圖的情況(強Leader)。Leader會找到Follower和自己想通的最後一個日誌條目,將該條目之後的日誌全部刪除並複製Leader上的日誌。詳細過程如下:
- Leader維護了每個Follower節點下一次要接收的日誌的索引,即nextIndex
- Leader選舉成功後將所有Follower的nextIndex設置爲自己的最後一個日誌條目+1
- Leader將數據推送給Follower,如果Follower驗證失敗(nextIndex不匹配),則在下一次推送日誌時縮小nextIndex,直到nextIndex驗證通過
上面的方式顯然可以通過一些方法進行優化來減少重試的次數,但是在Raft論文中對是否有必要進行優化提出了質疑,因爲這種異常的情況很少出現。
解讀Raft(三 安全性)
前言
之前的兩篇文章更多的是在描述Raft算法的正常流程,沒有過多的去討論異常場景。
而實際在分佈式系統中,我們更多的都是在應對網絡不可用、機器故障等異常場景,所以本篇來討論一下Raft協議的安全性,即在異常場景下是否會導致數據丟失、數據不一致等情況。
選舉限制
在Raft協議中,所有的日誌條目都只會從Leader節點往Follower節點寫入,且Leader節點上的日誌只會增加,絕對不會刪除或者覆蓋。
這意味着Leader節點必須包含所有已經提交的日誌,即能被選舉爲Leader的節點一定需要包含所有的已經提交的日誌。因爲日誌只會從Leader向Follower傳輸,所以如果被選舉出的Leader缺少已經Commit的日誌,那麼這些已經提交的日誌就會丟失,顯然這是不符合要求的。
這就是Leader選舉的限制:能被選舉成爲Leader的節點,一定包含了所有已經提交的日誌條目。
回看算法基礎中的RequestVote RPC:
參數 | 解釋 |
---|---|
term | Candidate的任期 |
candidateId | Candidate的ID |
lastLogIndex | Candidate最後一條日誌的索引 |
lastLogTerm | Candidate最後一條日誌的任期 |
參數 | 解釋 |
---|---|
term | 當前任期,用於Candidate更新自己的任期 |
voteGranted | true表示給Candidate投票 |
請求中的lastLogIndex和lastLogTerm即用於保證Follower投票選出的Leader一定包含了已經被提交的所有日誌條目。
- Candidate需要收到超過版本的節點的選票來成爲Leader
- 已經提交的日誌條目至少存在於超過半數的節點上
- 那麼這兩個集合一定存在交集(至少一個節點),且Follower只會投票給日誌條目比自己的“新”的Candidate,那麼被選出的節點的日誌一定包含了交集中的節點已經Commit的日誌
日誌比較規則(即上面“新”的含義):Raft 通過比較兩份日誌中最後一條日誌條目的索引值和任期號定義誰的日誌比較新。如果兩份日誌最後的條目的任期號不同,那麼任期號大的日誌更加新。如果兩份日誌最後的條目任期號相同,那麼日誌比較長的那個就更加新。
日誌提交限制
上圖按時間序列展示了Leader在提交日誌時可能會遇到的問題。
- 在 (a) 中,S1 是領導者,部分的複製了索引位置 2 的日誌條目。
- 在 (b) 中,S1 崩潰了,然後 S5 在任期 3 裏通過 S3、S4 和自己的選票贏得選舉,然後從客戶端接收了一條不一樣的日誌條目放在了索引 2 處。
- 然後到 (c),S5 又崩潰了;S1 重新啓動,選舉成功,開始複製日誌。在這時,來自任期 2 的那條日誌已經被複制到了集羣中的大多數機器上,但是還沒有被提交。
- 如果 S1 在 (d) 中又崩潰了,S5 可以重新被選舉成功(通過來自 S2,S3 和 S4 的選票),然後覆蓋了他們在索引 2 處的日誌。反之,如果在崩潰之前,S1 把自己主導的新任期裏產生的日誌條目複製到了大多數機器上,就如 (e) 中那樣,那麼在後面任期裏面這些新的日誌條目就會被提交(因爲S5 就不可能選舉成功)。 這樣在同一時刻就同時保證了,之前的所有老的日誌條目就會被提交。
任期2內產生的日誌可能在(d)的情況下被覆蓋,所以在出現(c)的狀態下,Leader節點是不能commit任期2的日誌條目的,即不能更新commitIndex。
在上圖最終狀態是(e)的情況下,commitIndex的變化應該是1->3,即在(c)的情況下,任期4在索引3的位置commit了一條消息,commitIndex直接被修改成3。
而任期2的那條日誌會通過Log Matching Property最終被複制到大多數節點企且被應用。
Raft算法保證了以下特性:
- 如果兩個日誌條目有相同的index和term,那麼他們存儲了相同的指令(即index和term相同,那麼可定是同一條指令,就是同一個日誌條目)
- 如果不同的日誌中有兩個日誌條目,他們的index和term相同,那麼這個條目之前的所有日誌都相同
兩條規則合併起來的含義:兩個日誌LogA、LogB,如果LogA[i].index=Log[i]B.index且LogA[i].term=Log[i].term,那麼LogA[i]=Log[i]B,且對於任何n < i的日誌條目,LogA[n]=LogB[n]都成立。(這個結論顯而易見的可以從日誌複製規則中推導出來)
解讀Raft(四 成員變更)
將成員變更納入到算法中是Raft易於應用到實踐中的關鍵,相對於Paxos,它給出了明確的變更過程(實踐的基礎,任何現實的系統中都會遇到因爲硬件故障等原因引起的節點變更的操作)。
顯然,我們可以通過shutdown集羣,然後變更配置後重啓集羣的方式達到成員變更的目的。但是這種操作會損失系統的可用性,同時會帶來操作失誤引起的風險。支持自動化配置,即配置可以在集羣運行期間進行動態的變更(不影響可用性)顯示是一個非常重要的特性。
Raft成員變更機制
在成員變更時,因爲無法做到在同一個時刻使所有的節點從舊配置轉換到新配置,那麼直接從就配置向新配置切換就可能存在一個節點同時滿足新舊配置的“超過半數”原則。
如下圖,原集羣由Server1、Server2、Server3,現在對集羣做變更,增加Server4、Server5。如果採用直接從舊配置到新配置的切換,那麼有一段時間存在兩個不想交的“超過半數的集羣”。
上圖,中在中間位置Server1可以通過自身和Server2的選票成爲Leader(滿足舊配置下收到大多數選票的原則);Server3可以通過自身和Server4、Server5的選票成爲Leader(滿足新配置線,即集羣有5個節點的情況下的收到大多數選票的原則);此時整個集羣可能在同一任期中出現了兩個Leader,這和協議是違背的。
爲了保證安全性,Raft採用了一種兩階段的方式。
第一階段稱爲joint consensus,當joint consensus被提交後切換到新的配置下。
joint consensus狀態下:
- 日誌被提交給新老配置下所有的節點
- 新舊配置中所有機器都可能稱爲Leader
- 達成一致(選舉和提交)要在兩種配置上獲得超過半數的支持
具體的切換過程如下:
- Leader收到C-old到C-new的配置變更請求時,創建C-old-new的日誌並開始複製給其他節點(和普通日誌複製沒有區別)
- Follower以最新的配置做決定(收到C-old-new後就以C-old-new來決定),Leader需要以已經提交的配置來做決定(即只有C-old-new複製到大多數節點後Leader才以這個配置做決定);這個時候處於一個共同決定的過程
- 之後提交C-new到所有節點,一旦C-new被提交,舊的配置就無所謂了
從上圖可以看出,不存在一個階段C-old和C-new可以同時根據自己的配置做出決定,所以不會出現本文開頭描述的情況。
Review成員變更
如果當前的Leader不在C-new的配置中會怎麼樣(即當前的Leader是一個要被下線的節點)?
在C-old-new的狀態下,Leader依舊可用;在C-new被commit之後Leader實際已經從集羣中脫離,此時可以對Leader節點進行下線操作,而新集羣則會在C-new的配置下重新選舉出一個Leader。
如果在配置分發過程中Leader Crash了會怎麼樣?
這個問題要分爲多種情況:1. C-new已經分發到超過半數節點、2. C-new還沒分發到超過半數的節點
情況1:C-new已經分發到超過半數節點
集羣開始重新選舉,此時在C-new的規則下,舊節點(不存在新配置中的節點)不會贏得選舉(因爲他們要在C-old-new的情況下決定,但是拿不到C-new的選票),只有拿到C-new的節點可能成爲Leader並繼續下發C-new配置,流程恢復。
情況2:C-new還沒分發到超過半數的節點
這種情況下,C-old-new和C-new的節點都可以成爲Leader,但是無所謂,因爲無論誰成爲Leader,都能根據當前的配置繼續完成後續流程(如果是C-new那麼相當與完成了最終的配置,不在C-new的節點會因爲沒有心跳數據而失效)
舊節點下線造成的問題:舊節點收不到心跳觸發選舉,發送請求給C-old-new中的節點,是否會影響集羣正常運行
Raft的處理方式:當節點確信有Leader存在時,不會進行投票(在Leader超時之前收到新的投票請求時不會提升term和投票)。且開始選舉之前等待一個選舉超時時間,這樣在新Leader正常工作的情況下,不會受到舊節點的影響。
舊節點在發起選舉前需要等待一段時間,那麼這段時間新Leader可以發送心跳,這樣就減少了影響。 對正常流程的影響不大。(Leader失效後要等一段時間,沒有及時觸發,然而本身這裏就有一個判斷失效的時間,好像影響不大;比如原先超時時間是10s,那麼如果設置成5s,原策略下10s超時就是10s後開始選舉,新策略下5s超時就是超時後再等5s再開始選舉,影響就是超時時間變短)
新的服務器沒有任何數據,加入進來進來怎麼保證系統的可用性(這個時候新日誌沒辦法Commit就沒辦法響應給客戶端)?
新加入的節點需要時間複製數據,在這個過程完成之前,Raft採用以下機制來保證可用性: 新加入節點沒有投票權(Leader複製日誌給他們,但是不將他們考慮在機器數量裏面——即在判斷是否超過半數時不把這些節點考慮在內),直到這些節點的日誌追上其他節點。