Raft一致性協議

0. 寫在前面

分佈式存儲系統通過維護多個副本來進行fault-tolerance,提高系統的availability

帶來的代價就是分佈式存儲系統的核心問題之一:維護多個副本的一致性


一致性協議就是用來幹這事的,即使在部分副本宕機的情況下。Raft是一種較容易理解的一致性協議。

一致性協議通常基於replicated state machines,即所有結點都從同一個state出發,都經過同樣的一些操作序列,最後到達同樣的state。


1. Raft 概述

1.1 Raft大概將整個過程分爲三個階段:

  • leader election
  • log replication
  • commit(safety)

1.2 每個server有三個狀態:

  • leader
  • follower
  • candidate

正常情況下,所有server中只有一個是leader,其它的都是follower。server之間通過RPC消息通信。follower不會主動發起RPC消息。leader和candidate(選主的時候)會主動發起RPC消息。


2. Leader election

時間被分爲很多連續的隨機長度的term(一段時間),一個term由一個唯一的id標識。每個term一開始就進行leader election

  1. followers 將自己維護的current_term_id加1;
  2. 然後將自己的狀態轉成candidate
  3. 發送RequestVoteRPC消息(帶上current_term_id) 給其它所有server

2.1 Leader election過程有三種結果:

第一種情況:
自己被選成了 leader。當收到了 majority 的投票後,狀態切成 leader ,並且定期給其它的所有 server 發心跳消息(其實是不帶 log 的 AppendEntriesRPC )以告訴對方自己是 current_term_id 所標識的 term 的 leader 。每個 term 最多隻有一個 leader , term id 作爲 logical clock ,在每個 RPC 消息中都會帶上,用於檢測過期的消息,比如自己是一個過期的 leader(term id 更小的leader)。當一個 server 收到的 RPC 消息中的 rpc_term_id 比本地的 current_term_id 更大時,就更新 current_term_id 爲 rpc_term_id ,並且如果當前 state 爲 leader 或者 candidate 時,將自己的狀態切成 follower 。如果 rpc_term_id 比本地的 current_term_id 更小,則拒絕這個 RPC 消息。

第二種情況:
自己是 follower 。如1所述,當 candidate 在等待投票的過程中,收到了大於或者等於本地的 current_term_id 的聲明對方是 leader 的 AppendEntriesRPC 時,則將自己的 state 切成 follower ,並且更新本地的 current_term_id 。

第三種情況:
沒有選出 leader 。當投票被瓜分,沒有任何一個 candidate 收到了 majority 的 vote 時,沒有 leader 被選出。這種情況下,每個 candidate 等待的投票的過程就超時了,接着 candidates 都會將本地的 current_term_id 再加1,發起 RequestVoteRPC 進行新一輪的 leader election 。

2.2 投票策略:

每個 server 只會給每個 term 投一票,具體的是否同意和後續的 Safety 有關。

2.3 多輪選不出leader情況

當投票被瓜分後,所有的candidate同時超時,然後有可能進入新一輪的票數被瓜分,爲了避免這個問題,Raft採用一種很簡單的方法:每個candidate的election timeout從150ms-300ms之間隨機取,那麼第一個超時的candidate就可以發起新一輪的leader election,帶着最大的term_id給其它所有server發送RequestVoteRPC消息,從而自己成爲leader,然後給他們發送心跳消息以告訴他們自己是 leader。


3. Log Replication

3.1 leader處理客戶端請求

當leader被選出來後,leader就可以接受客戶端發來的請求了,每個請求包含一條需要被replicated state machines執行的命令。leader會把它作爲一個log entry,append到它的日誌中,然後給其它的server發AppendEntriesRPC。當leader確定一個log entry被safely replicated了,就apply這條log entry到狀態機中然後返回結果給客戶端。如果某個follower宕機了或者運行的很慢,或者網絡丟包了,則會一直給這個follower發AppendEntriesRPC直到日誌一致

當一條日誌是commited時,leader才能決定將它apply到狀態機中。Raft 保證一條commited的log entry已經持久化了並且會被所有的server執行。

3.2 follower日誌不一致

當一個新的leader選出來的時候,它的日誌和其它的follower的日誌可能不一樣,這個時候,就需要一個機制來保證日誌是一致的。如下圖所示,一個新leader產生時,集羣狀態可能如下:


這裏寫圖片描述

  • 最上面這個是新leader,a~f 是 follower,每個格子代表一條 log entry,格子內的數字代表這個 log entry 是在哪個 term 上產生的。

  • 新leader產生後,log就以leader上的log爲準。其它的follower要麼少了數據比如b,要麼多了數據,比如f,要麼既少了又多了數據,比如d。


需要有一種機制來讓leader和follower對log達成一致,leader會爲每個follower維護一個nextIndex,表示leader給各個follower發送的下一條log entry在log中的index,初始化爲leader的最後一條log entry的下一個位置。leader給follower發送AppendEntriesRPC消息,帶着(term_id, (nextIndex-1)), term_id即(nextIndex-1)這個槽位的log entry的term_id,follower接收到AppendEntriesRPC後,會從自己的log中找是不是存在這樣的log entry,如果不存在,就給leader回覆拒絕消息,然後leader則將nextIndex減1,再重複,直到AppendEntriesRPC消息被接收。

以leader和b爲例:

初始化,nextIndex爲11,leader給b發送AppendEntriesRPC(6,10),b在自己log的10號槽位中沒有找到term_id爲6的log entry。則給leader迴應一個拒絕消息。接着,leader將nextIndex減一,變成10,然後給b發送AppendEntriesRPC(6, 9),b在自己log的9號槽位中同樣沒有找到term_id爲6的log entry。循環下去,直到leader發送了AppendEntriesRPC(4,4),b在自己log的槽位4中找到了term_id爲4的log entry。接收了消息。隨後,leader就可以從槽位5開始給b推送日誌了


4. Safety

4.1 哪些follower有資格成爲leader ?

  • Raft保證被選爲新leader的server擁有所有的已經committed的log entry,這與ViewStamped Replication不同,後者不需要這個保證,而是通過其他機制從follower拉取自己沒有的commited的log entry。

  • 這個保證是在RequestVoteRPC階段做的,candidate在發送RequestVoteRPC時,會帶上自己的最後一條log entry的term_id和index,server在接收到RequestVoteRPC消息時,如果發現自己的日誌比RPC中的更新,就拒絕投票。日誌比較的原則是,如果本地的最後一條log entry的term id更大,則更新,如果term id一樣大,則日誌更多的更大(index更大)。

4.2 哪些log entry被認爲是commited?


這裏寫圖片描述

A time sequence showing why a leader cannot determine commitment using log entries from older terms. In (a) S1 is leader and partially replicates the log entry at index 2. In (b) S1 crashes; S5 is elected leader for term 3 with votes from S3, S4, and itself, and accepts a different entry at log index 2. In (c) S5 crashes; S1 restarts, is elected leader, and continues replication. At this point, the log entry from term 2 has been replicated on a majority of the servers, but it is not committed. If S1 crashes as in (d), S5 could be elected leader (with votes from S2, S3, and S4) and overwrite the entry with its own entry from term 3. However, if S1 replicates an entry from its current term on a majority of the servers before crashing, as in (e), then this entry is committed (S5 cannot win an election). At this point all preceding entries in the log are committed as well.

關於算法的正確性證明見:Raft implementations


5. Log Compaction

在實際的系統中,不能讓日誌無限增長,否則系統重啓時需要花很長的時間進行回放,從而影響availability。Raft採用對整個系統進行snapshot來處理,snapshot之前的日誌都可以丟棄。

snapshot技術在Chubby和ZooKeeper系統中都有采用。


這裏寫圖片描述


每個server獨立的對自己的系統狀態進行snapshot,並且只能對已經committed log entry(已經apply到了狀態機)進行snapshot,snapshot有一些元數據,包括last_included_index,即snapshot覆蓋的最後一條commited log entry的 log index,和last_included_term,即這條日誌的termid。這兩個值在snapshot之後的第一條log entry的AppendEntriesRPC的consistency check的時候會被用上,之前講過。一旦這個server做完了snapshot,就可以把這條記錄的最後一條log index及其之前的所有的log entry都刪掉。

snapshot的缺點就是不是增量的,即使內存中某個值沒有變,下次做snapshot的時候同樣會被dump到磁盤。

當leader需要發給某個follower的log entry被丟棄了(因爲leader做了snapshot),leader會將snapshot發給落後太多的follower。或者當新加進一臺機器時,也會發送snapshot給它。

發送snapshot使用新的RPC,InstalledSnapshot

做snapshot有一些需要注意的性能點:
1. 不要做太頻繁,否則消耗磁盤帶寬。
2. 不要做的太不頻繁,否則一旦server重啓需要回放大量日誌,影響availability。統推薦當日志達到某個固定的大小做一次snapshot。
3. 做一次snapshot可能耗時過長,會影響正常log entry的replicate。這個可以通過使用copy-on-write的技術來避免snapshot過程影響正常log entry的replicate。


6. Cluster membership changes

Raft將有server加入集羣或者從集羣中刪除也納入一致性協議中考慮,避免由於下線老集羣上線新集羣而引起的不可用。集羣的成員列表重配置也是一條log entry,log內容包含了集羣成員列表。

老集羣配置用Cold 表示,新集羣配置用Cnew 表示。

當集羣成員配置改變時,leader收到人工發出的重配置命令從Cold 切成Cnew ,leader 給其它server複製一條特殊的log entry給其它的server,內容包括ColdCnew ,一旦server收到了這條特殊的配置log entry,其後的log entry會被replicate到ColdCnew 中,一條log entry被認爲是committed的需要滿足這條日誌既被Cold 的majority寫盤,也被Cnew 的majority寫盤。一旦ColdCnew 這條log entry被確認爲committed,leader就會產生一條只包含了Cnew 的log entry,同樣複製給所有server,server收到log後,老集羣的server就可以自動下線了。


7. Performance


這裏寫圖片描述

橫座標代表沒有leader的ms數,每條線代表election timeout的隨機取值區間。

上圖說明只要給個5ms的區間,就能避免反覆的投票被瓜分。超過10s沒有leader的情況都是因爲投票被瓜分的情況。

150-300ms的election timeout區間,沒有leader的時間平均287ms。

系統推薦使用150ms~300ms


8. Implementation

由於Go語言內置RPC,Channel,goroutine等高級編程組件,實現一個相對於其他語言還是容易些,這裏有一個Go的實現 Raft


9. 參考資料:

In Search of an Understandable Consensus Algorithm

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