概念入門:http://www.jdon.com/artichect/raft.htmlhtml
原文來自:https://www.cnblogs.com/foxmailed/p/3418143.html算法
分佈式存儲系統一般經過維護多個副原本進行fault-tolerance,提升系統的availability,帶來的代價就是分佈式存儲系統的核心問題之一:維護多個副本的一致性。一致性協議就是用來幹這事的,即便在部分副本宕機的狀況下。Raft是一種較容易理解的一致性協議。一致性協議一般基於replicated state machines,即全部結點都從同一個state出發,都通過一樣的一些操做序列,最後到達一樣的state。網絡
爲了便於理解,Raft大概將整個過程分爲三個階段,leader election,log replication和commit(safety)。app
每一個server處於三個狀態:leader,follower,candidate。正常狀況下,全部server中只有一個是leader,其它的都是follower。server之間經過RPC消息通訊。follower不會主動發起RPC消息。leader和candidate(選主的時候)會主動發起RPC消息。分佈式
Leader election性能
時間被分爲不少連續的隨機長度的term(一段時間),一個term由一個惟一的id標識。每一個term一開始就進行leader election:spa
1. followers將本身維護的current_term_id加1。3d
2. 而後將本身的狀態轉成candidate。日誌
3. 發送RequestVoteRPC消息(帶上current_term_id) 給 其它全部serverorm
這個過程會有三種結果:
1. 本身被選成了主。當收到了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消息。
2. 別人成爲了主。如1所述,當candidate在等待投票的過程當中,收到了大於或者等於本地的current_term_id的聲明對方是leader的AppendEntriesRPC時,則將本身的state切成follower,而且更新本地的current_term_id。
3. 沒有選出主。當投票被瓜分,沒有任何一個candidate收到了majority的vote時,沒有leader被選出。這種狀況下,每一個candidate等待的投票的過程就超時了,接着candidates都會將本地的current_term_id再加1,發起RequestVoteRPC進行新一輪的leader election。
投票策略:
每一個server只會給每一個term投一票,具體的是否贊成和後續的Safety有關。
當投票被瓜分後,全部的candidate同時超時,而後有可能進入新一輪的票數被瓜分,爲了不這個問題,Raft採用一種很簡單的方法:每一個candidate的election timeout從150ms-300ms之間隨機取,那麼第一個超時的candidate就能夠發起新一輪的leader election,帶着最大的term_id給其它全部server發送RequestVoteRPC消息,從而本身成爲leader,而後給他們發送心跳消息以告訴他們本身是主。
Log Replication
當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執行。
當一個新的leader選出來的時候,它的日誌和其它的follower的日誌可能不同,這個時候,就須要一個機制來保證日誌是一致的。以下圖所示,一個新leader產生時,集羣狀態可能以下:
最上面這個是新leader,a~f是follower,每一個格子表明一條log entry,格子內的數字表明這個log entry是在哪一個term上產生的。
新leader產生後,log就以leader上的log爲準。其它的follower要麼少了數據好比b,要麼多了數據,好比d,要麼既少了又多了數據,好比f。
須要有一種機制來讓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推送日誌了。
Safety
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更大)。
2. 哪些log entry被認爲是commited?
兩種狀況:
1. leader正在replicate當前term即term2的log entry給其它follower,一旦leader確認了這條log entry被majority寫盤了,這條log entry就被認爲是committed。如圖a,S1做爲當前term即term2的leader,log index爲2的日誌被majority寫盤了,這條log entry被認爲是commited
2. leader正在replicate更早的term的log entry給其它follower。圖b的狀態是這麼出來的:
S1做爲term2的leader,給S1和S2 replicate完log index=2的日誌後crash,當前狀態爲:
S1 1 2 宕機
S2 1 2
S3 1
S4 1
S5 1
S5被選爲term3的leader(因爲S5的最後一條log entry比S3,S4的最後一條log entry更新或同樣新,接收到S3,S4,S5的投票),本身產生了一條term3的日誌,沒有給任何人複製,就crash了,當前狀態以下:
S1 1 2
S2 1 2
S3 1
S4 1
S5 1 3 宕機
接着S1重啓後,又被選爲term4的leader(接收到S1,S2,S3的投票,文中沒有指出S4?),而後S1給S3複製了log index爲2的log entry,當前狀態以下:
S1 1 2
S2 1 2
S3 1 2
S4 1
S5 1 3 宕機
這個時候S5重啓,被選爲了term5的主(接收了S2,S3,S4,S5的投票),那麼S5會把log index爲2的日誌3複製給其它server,那麼日誌2就被overwrite了。
因此雖然這裏日誌2被majority的server寫盤了,可是並不表明它是commited的。
對commit加一個限制:主的當前term的至少一條log entry被majority寫盤
如:c圖中,就是主的當前term 4的一條log entry被majority寫盤了,假設這個時候S1宕機了,S5是不可能變成主的。由於S2和S3的log entry的term爲4,比S5的3大。
關於算法的正確性證實見:Raft implementations
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。
Cluster membership changes
Raft將有server加入集羣或者從集羣中刪除也歸入一致性協議中考慮,避免因爲下線老集羣上線新集羣而引發的不可用。集羣的成員列表重配置也是一條log entry,log內容包含了集羣成員列表。
老集羣配置用Cold表示,新集羣配置用Cnew表示。
當集羣成員配置改變時,leader收到人工發出的重配置命令從Cold切成Cnew,leader 給其它server複製一條特殊的log entry給其它的server,內容包括Cold∪Cnew,一旦server收到了這條特殊的配置log entry,其後的log entry會被replicate到Cold∪Cnew中,一條log entry被認爲是committed的須要知足這條日誌既被Cold的majority寫盤,也被Cnew的majority寫盤。一旦Cold∪Cnew這條log entry被確認爲committed,leader就會產生一條只包含了Cnew的log entry,一樣複製給全部server,server收到log後,老集羣的server就能夠自動下線了。
Performance
橫座標表明沒有leader的ms數,每條線表明election timeout的隨機取值區間。
上圖說明只要給個5ms的區間,就能避免反覆的投票被瓜分。超過10s沒有leader的狀況都是由於投票被瓜分的狀況。
150-150ms的election timeout區間,沒有主的時間平均287ms。
系統推薦使用150ms~300ms。
參考資料: