6.824——實驗二:Raft

1.介紹

這是構建KV存儲系統一系列實驗的第一個實驗。而對於分佈式數據庫而言,首先要解決兩個重大的問題:容錯、一致性。所以這個實驗當中,需要實現Raft協議(一個複製狀態機協議)以解決這兩個首要問題。

一個可靠性服務需要通過存儲其狀態副本(比如數據等)到多個副本服務器上來實現容錯。即使一些服務器節點失敗了,其它副本服務器仍可以運行該服務。但問題的關鍵是,失敗的服務節點會造成數據不一致。
下一個實驗會在基於Raft協議之上構建kv服務,然後可以在多機環境上共享kv服務,以求更高的性能。

2.Raft

爲了解決上面的問題即一致性問題,於是出現了Raft,Raft能夠管理所有服務節點的狀態副本,能夠讓失敗的服務節點恢復到正確的一致性狀態上來,算法的目標就是讓服務器節點按照少數服從多數的方式,最終達成一致意見。正如上面講到的,Raft是一個複製狀態機協議。https://yuerblog.cc/2018/07/28/understand-raft/——這篇博客不錯,raft關鍵在於抽屜理論,二階段提交,選舉約束,一共3個部分。
【下面仔細說一下Raft的原理】
在GFS中我們說過一致性的實現原理——確定一個主實例,串行化所有寫操作,然後在其他實例重放相同的操作序列,以保證多個實例數據的一致性。但是GFS中只有一個主實例master,一旦master崩潰,這會造成很大的影響,所以一般GFS都還會有些備用master,需要在這些備用master上同樣同步保存配置信息和日誌信息。

雖然大體上Raft協議也是確定一個主實例Leader,但不同的是其不需要額外的備份Leader,因爲其他的所有節點都可以成爲Leader。
下面描述一下Raft的大體流程,在一臺Leader服務器上,一致性模塊(這裏就是Raft)接收到客戶端的指令並把指令整理後寫入到日誌中,並與其他Follower服務器上的一致性模塊(Raft實例)通信,以確保每一個日誌最終包含一致的請求序列,即使有某些服務器宕機。一旦這些指令被正確的複製了,每一個服務器的狀態機都會按同樣的順序去執行它們,然後將結果返回給客戶端。另外,如果某些服務器宕機後又重新恢復了,其Raft會主動和Leader服務器通信,及時更新本地log並執行相應的命令序列,保持狀態一致性。
http://thesecretlivesofdata.com/raft/ 這個鏈接超級棒,講得很詳細。總結一下Raft的三個主要子問題:
【1. 日誌複製】
這裏我們首先說一下每一個服務器上日誌記錄的形式:term+command。
在這裏插入圖片描述
對於整個系統而言,所有的改變都要經由Leader來實現。在一臺Leader服務器上,一致性模塊(這裏就是Raft)接收到客戶端的指令並把指令整理後寫入到日誌中,此時,該條日誌記錄還處於未提交狀態,節點並不會執行具體操作;而是在提交之前首先與其他Follower服務器上的一致性模塊(Raft實例)通信,以確保每一個日誌最終包含一致的請求序列,再等待大多數節點寫入log並響應Leader之後,Leader才commit執行具體操作,然後Leader通知其他節點commit。

【2. Leader選舉】

  • 對於集羣中的每個節點而言,有3種狀態——Follower、Candidate、Leader。
  • 初始時都爲follower狀態,如果follower節點在一段時間內沒有收到命令(即當前Leader宕機了),就會變爲Candidate狀態;然後Candidate節點會向其他節點發起投票請求,如果該Candidate節點得到了大多數的vote支持,就會變爲Leader。
  • 超時設定:
    election timeout.——若超過這個時間還沒有接到Leader的命令(心跳檢測或log增加等RPC調用),則代表Leader可能掛掉了,於是自己就可以準備上位了

Raft的關鍵就在於Leader選舉機制,因爲Leader是保持一致性的根本,Leader的log條目必須是要與Client提交過的log條目一致纔行,即要保證Leader的log條目一定是commit過最多的。
而抽屜理論恰好解決了這個問題,抽屜中有5個球,3黑2白,任意抽3個球,必然有一個是黑球。
借鑑這個理論,我們只需要添加幾個約束條件即可:

  1. commit限制:我們在日誌複製時,必須要複製超過半數成功返回給當前Leader,Leader才能commit並向Client承諾
  2. 投票(RequestVote)限制:follower只能向大於自己term的candidate投票,在相等的情況下,選擇記錄了最新log的
  3. 選舉限制:如果當前Leader宕機,開始新一輪選舉,則必須要保證接收到超過半數的選票才能成爲新Leader,因爲前面的commit限制,超過半數意味着其中至少有一個follower是最新狀態(可能not commit,但是一定已經寫在了其本地log中),另外由於投票限制,意味着當前Leader一定具有最新的已commit的log條目

【Term】
在這裏插入圖片描述
首先我們從宏觀上考慮:我們可以將一致性看作是一段時間內的一致性,在這一段時間內必須得保證集羣的一致性,而由於各種原因可能導致出現不同的Leader,於是天然的就將時間劃分爲了不同的任期,在一個Term中只能產生一個Leader;
其中Term的變化流程:
Raft開始時所有Follower的Term爲1,其中一個Follower邏輯時鐘到期後轉換爲Candidate,Term加1這時Term爲2,然後開始選舉,這時候有幾種情況會使Term發生改變:
  1:如果當前Term爲2的任期內沒有選舉出Leader或出現異常,則Term遞增,開始新一任期選舉
  2:當這輪Term爲2的週期選舉出Leader後,過後Leader宕掉了,然後其他Follower轉爲Candidate,Term遞增,開始新一任期選舉
  3:當Leader或Candidate發現自己的Term比別的Follower小時Leader或Candidate將轉爲Follower,Term遞增
  4:當Follower的Term比別的Term小時Follower也將更新Term保持與其他Follower一致;
  
【3. 安全】
對於Raft而言安全的關鍵參數就是狀態安全參數,如果一個服務器已經將某個log索引位置的entry用於其狀態機(也就是commit執行了),則其他服務器不允許在同樣的log索引位置增加不同的entry。commit是十分重要的一個概念,commited log 意味着這些日誌已經被持久化的記錄在了集羣中的大多數服務器節點上,而且由於少數服從多數的原則,這保證了即使有一小部分節點因爲網絡等原因沒有持久化記錄,但是一定會在後面某個網絡順暢的時刻由Leader通知持久化記錄並應用於其狀態機。

2.1 具體實現——Raft算法的事件和狀態轉換

首先說一下,每一個服務器都可以視爲一個Raft實例節點,集羣的一致性就是通過各個服務器的Raft實例實現的。
https://www.cnblogs.com/wangbinquan/p/9061223.html——參考鏈接

2.1.1 【Raft集羣啓動 】

1、可以獲取整個Raft集羣的所有節點連接信息
2、currentTerm初始爲0、votedFor初始爲空
3、初始狀態爲Follower
4、如果是重新啓動則有快照和日誌序列,如果爲新集羣則全部爲空
5、啓動隨機定時器,定時器超時時間在[m, n]範圍內,保證請求傳輸時間 << [m, n] << 平均一個服務器兩次出現宕機的時間間隔

2.1.2【狀態】

1.存儲於所有服務器上的持久化狀態變量:

  • currentTerm——當前節點所能看到的最大的term值,該值單調增加
  • votedFor——當前term裏將票投給的對象,如果尚未投票則爲空
  • log[]——日誌條目;每個條目都包含用於狀態機的命令和Leader收到條目的term期限(第一個term爲1)

2.存儲於所有服務器上的可變狀態變量(每次選舉後都要重新初始化)

  • commitIndex——已知已提交的最後一個log條目索引(初始爲0,單調增加)
  • lastApplied——已知已經執行的最後一個log條目索引(初始爲0,單調增加),如果發現當前機器commitIndex > lastApplied則應該將本機log[]中序號爲(lastApplied, commitIndex]的部分應用到狀態機

3.存儲於Leader上的可變狀態變量

  • nextIndex[]——要發送給其它節點的下一條log條目索引(初始時爲Leader的最後一個log條目索引+1)
  • matchIndex[]——所有其它節點已經複製好的最後一條log條目索引,即和Leader匹配的最大日誌編號(初始爲0,單調增加)

2.1.3【RPC方法】

1.AppendEntries RPC——Leader調用以向其它follower節點實現日誌複製功能,也可以用作心跳檢測
我們以下圖來具體分析
在這裏插入圖片描述
我們知道,持久化存儲的日誌信息只有term+command(當然其log index肯定也知道),所以我們只能以term+log index來作爲log 複製的判斷依據。如上圖,在某輪term下,log出現了上述不一致的情況,該如何實現日誌複製功能呢?下面具體看一下Raft的AppendEntries 設計。起初leader並不知道其它節點的log狀態,所以統一將其它節點nextIndex[]設置爲其log index+1,意爲假設follower和自己保持一致,所以接下來就是發送AppendEntries 進行一致性校驗,若follower的log超過了自己或包含自己不匹配的(c、d、f),則follower刪除不匹配的log(這部分刪除由於前面的leader選舉限制保證了這部分多餘或不匹配的log肯定是前一個Leader沒有commit的,即沒有向用戶承諾的數據,可以安全刪除)返回false,若不足(a、b、e)則返回false,然後leader使其nextIndex[i]–,直到最終在某個點達成一致,然後follower便可以複製leader的新增日誌,實現一致性。

參數:

  • term——leader任期
  • leaderID——不解釋
  • prevLogIndex——當前即將發送的日誌的前面一個日誌的索引(等於nextIndex[]中該節點對應的值-1 )
  • prevLogTerm——當前即將發送的日誌的前面一個日誌的term值 (等於此Leader的log[]中prevLogIndex對應日誌的term值 )
    //prevLogIndex…prevLogTerm就相當於Leader的匹配點,只有follower匹配了Leader的匹配點才表示follower接下來的日誌複製操作才能與Leader保持log一致性
  • entries[]——即節點需要add的log條目(心跳檢測時爲空),如果prevLogIndex+1不是此Leader的log[]中最後一條日誌,則 entries[]取log[]中prevLogIndex之後緊接着的部分日誌
  • leaderCommit——Leader最後一次commit的索引

結果

  • term——接收日誌節點的term值,即告知leader當前follower的任期,如果大於leader的term,則leader需要更新自己的currentTerm,並轉換爲Follower狀態
  • success——如果接收日誌節點的log[]結構中prevLogIndex索引處含有日誌並且該日誌的term等於prevLogTerm則返回true,否則false

任何狀態下的節點接收到AppendEntries的返回結果邏輯:(條件依次判斷,不滿足上一條纔會進入下一條)

  1. 如果發過來的term值小於當前term值,返回false,以及currentTerm——針對場景(d):可能舊Leader(term6)與當前follower由於網絡原因,導致很長時間才接受到AppendEntries,但是在這段網絡堵塞的時間內,已經推選出了另外一個新Leader(term7),而由於Leader選舉算法的原因,這個新Leader總是滿足一致性,所以當前follower的日誌可能已處於最新term期,舊Leader沒有AppendEntries完成的任務已經由新Leader代替完成了,此時term7完成。如果網絡恢復,舊Leader發送過來的AppendEntries信息已經過時,直接拒絕即可。
  2. 如果term > currentTerm則設置currentTerm = term, voteFor = leaderId。轉換當前節點爲Follower狀態,重置隨機定時器,並進入下一步——針對場景(b,e)
  3. 如果日誌在 prevLogIndex 位置處的日誌條目的任期號和 prevLogTerm 不匹配(也包括prevLogIndex 不存在日誌的情況),則返回 false,則設置voteFor = leaderId,並返回false——針對場景(a,b,e):可能舊Leader(Term6)還沒有來得及完成對節點a的全部複製,就fail了或者節點a自己還沒寫完log就fail了,等到term8才恢復過來。
  4. 如果在當前索引位置上存在日誌,但是term不一致,則刪除這一條和之後所有的 (因爲當前不一致,後面的肯定不一致),返回false——針對場景(f):在term2、term3時期,大多數follower都沒有來得及完成log寫入就fail掉了(Leader或Follower都有可能fail),只有一小部分follower寫入了term2、term3時期leader的appendEntries日誌。然後再緊接着這幾輪term,f都一直處於fail狀態,直到term8才恢復。
  5. 到這一步,意味着已經達成了一致,即prevLogIndex與prevLogTerm相匹配了,比如f,會不斷地刪除直到leader對應的nextIndex[f]=4,亦即prevLogIndex=3,prevLogTerm=3(表示Leader的匹配點),然後f發現與自己的匹配點相一致,於是可以執行日誌存儲功能

    日誌存儲:
    1、將prevLogIndex之後的日誌全部刪除,並將entries[]中的日誌依次放入log[]中prevLogIndex之後的位置裏。
    2、如果leaderCommit(參數裏) > commitIndex(每個節點存儲裏)則設置commitIndex = leaderCommit並將(commitIndex,leaderCommit]區間的日誌應用到狀態機上(更新commitIndex後機器會自動應用該操作)。

在Leader狀態下接收到AppendEntries的返回結果邏輯:(條件依次判斷,不滿足上一條纔會進入下一條)
6. 返回消息中的term > currentTerm則設置currentTerm = term,並將自己轉換爲Follower狀態,並重置隨機定時器。——對應場景(d)
7. 返回消息中success爲false則將該節點在nextIndex[]對應的值減1——對應場景(a,b,e,f)
8. 如果接收到大多數Follower的成功反饋,則可以提交該條AppendEntries所同步的所有日誌,和此之前的所有日誌。Leader只允許提交當前term的日誌,不允許提交之前term的日誌,但是可以通過提交當前term的日誌達到間接提交之前term的日誌的目的。
> 因爲Leader不允許提交之前term的日誌,因此在Leader被選舉成功時可以發送一條無意義日誌給其他機器,以更新日誌列表中的最大term編號,當接收到大多數返回時提交該日誌,以達到提交之前已被大多數節點接受的日誌的目的

2.RequestVote RPC——candidate向follower拉票
參數:
在這裏插入圖片描述
返回結果:在這裏插入圖片描述
投票人的投票邏輯實現:

  1. 如果term < currentTerm返回 false ——選舉限制,保證leader始終是commit過的term最新的
  2. 如果 votedFor 爲空或者爲 candidateId,並且候選人的日誌至少和自己一樣新(即candidate的最後一個日誌的term和index要分別大於等於自己節點的最後一個日誌的term和index),那麼就投票給他——在同樣commit的情況下(即1),儘量選舉not commit但是已經寫入log最多的。

2.1.4 【Raft實例規則】

1.所有節點:

  • 如果commitIndex > lastApplied,那麼就 lastApplied 加一,並把log[lastApplied]應用到狀態機中
  • 如果接收到的 RPC 請求或響應中,任期號T > currentTerm,那麼就令 currentTerm 等於 T,並切換到follower狀態

2.Followers:

響應來自候選人和領導者的請求,如果在超過選舉超時時間的情況之前都沒有收到領導人的心跳檢查,或者是候選人的請求投票,就自己變成候選人

3.Candidates:

在轉變成候選人後就立即開始選舉過程:

  • 自增當前的任期號(currentTerm++)
  • 給自己投票 votedFor = me
  • 重置選舉超時計時器
  • 發送請求投票的 RPC 給其他所有服務器
    -如果接收到大多數服務器的選票,那麼就變成領導人
  • 如果接收到來自新的領導人的附加日誌 RPC,轉變成跟隨者
  • 如果選舉過程超時,再次發起一輪選舉

4.Leaders:

  • 一旦成爲領導人:發送空的附加日誌 RPC(心跳)給其他所有的服務器;在一定的空餘時間之後不停的重複發送,以阻止跟隨者超時
  • 如果接收到來自客戶端的請求:附加條目到本地日誌中,在條目被應用到狀態機後響應客戶端
  • 如果對於一個跟隨者,最後日誌條目的索引值大於等於 nextIndex,那麼:發送從 nextIndex 開始的所有日誌條目:
    • 如果成功:更新相應跟隨者的 nextIndex 和 matchIndex
    • 如果因爲日誌不一致而失敗,減少 nextIndex 重試
  • 如果存在一個滿足N > commitIndex的 N,並且大多數的matchIndex[i] ≥ N成立,並且log[N].term == currentTerm成立,那麼令 commitIndex 等於這個 N

2.1.5 超時限定

對於Raft而言,超時限定是十分重要的一個設定,只有合理實現超時限定才能滿足Leader選舉的內在要求,Raft 的要求之一就是安全性不能依賴時間:整個系統不能因爲某些事件運行的比預期快一點或者慢一點就產生了錯誤的結果。但是,可用性(系統可以及時的響應客戶端)不可避免的要依賴於時間。例如,如果消息交換比服務器故障間隔時間長,候選人將沒有足夠長的時間來贏得選舉;沒有一個穩定的領導人,Raft 將無法工作。
所以我們得保證在一個選舉超時時間內,應該對於所有正常的網絡通信而言,必須要來得及持久化狀態存儲以及及時作出迴應。
Raft 可以選舉並維持一個穩定的領導人,只要系統滿足下面的時間要求:

廣播時間(broadcastTime) << 選舉超時時間(electionTimeout) << 平均故障間隔時間(MTBF)

在這個不等式中,廣播時間指的是從一個服務器並行的發送 RPCs 給集羣中的其他服務器並接收響應的平均時間;然後平均故障間隔時間就是對於一臺服務器而言,兩次故障之間的平均時間。廣播時間必須比選舉超時時間小一個量級,這樣領導人才能夠發送穩定的心跳消息來阻止跟隨者開始進入選舉狀態;選舉超時時間應該要比平均故障間隔時間小上幾個數量級,這樣整個系統才能穩定的運行(比如,如果平均故障時間是1天,如果超時時間設爲2天,那肯定不行,這樣的話整個系統都崩潰1天,才能進行下一次Leader選舉)。當領導人崩潰後,整個系統會大約相當於選舉超時的時間裏不可用;我們希望這種情況在整個系統的運行中很少出現。
另外值得注意的一點是,選舉時間必須得是一個隨機時間,這樣才能避免選票的瓜分,否則,所有follower都會在同一時間轉換爲candidate狀態併發起拉票請求,導致各自的票都投給了自己,導致造成無限循環競選Leader。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章