Raft探索歷程--Part2

聲明:本系列文章面向的讀者需要看過Raft論文或者對Raft有一定的瞭解,如果沒有看過論文或者不瞭解Raft,建議先去學習後再來看,否則會比較難懂。

緊接着上一篇的內容,繼續探索Raft的leader選舉、日誌複製、安全性等等實現細節。

Raft基礎

一個Raft集羣通常包含多個機器,比較普遍的Raft集羣組成是2F+1,F代表的是可以發生失敗的機器數量。比如集羣有5臺機器,那麼Raft只能容忍兩臺服務失敗,如果三臺服務不能工作了,那麼整個集羣也就失敗了。大多數Raft集羣的機器數量都是5個。

每臺機器都有三個狀態:leaderfollowercandidate,如下圖所示,就是三種狀態之間的轉換圖。

  • leader接收所有的請求,如果client請求到了follower,那麼follower會將請求轉發到leader

  • follower只是接收來自leader和candidate的請求,不會主動發起請求。

    如果follower沒有收到任何通信或信號,轉變爲candidate,然後重新進行一輪新的選舉

  • candidate,是在選舉新leader時出現的狀態,如果candidate收到來自大多數機器的投票請求(RequestVote RPC,以下稱爲RequestVote請求),就會轉變爲leader

Raft把時間按照term劃分,每個term以一次選舉開始,如果某一個candidate成爲新的leader後,就進入正常運行階段,如果沒有選舉出新的leader,那麼就再次進行一次新的選舉,這時候又是一個新的term。簡單地說,在Raft中,是以term做時間單位。如下圖所示:

每一個服務器都會保存當前的term序號,當前的term序號會在服務器通信之間傳遞。如果服務器收到的請求包含了舊的term,服務器會拒絕該請求。

leader選舉

再來看看Raft是如何進行leader選舉的,Raft使用心跳機制來觸發leader選舉。當服務器啓動的時候,初始狀態是follower,leader發送心跳的方式是發起一個不包含日誌條目的AppendEntries RPC(以下稱爲Append請求)到所有的follower,follower收到來自leader的心跳請求包,說明leader還在”存活”着,如果長時間沒有收到leader的心跳,那麼follower就會認爲當前沒有leader,轉爲candidate,然後發起一次新的leader選舉。

當leader選舉開始時,follower會將當前的term自增,隨後馬上進入candidate狀態,併發起一次RequestVote請求到所有機器,請求其他機器對它進行投票。Raft集羣中的服務器在一個term裏,除了投給自己之外,只能投票給一個candidate。

如果有以下三種情況發生,candidate的狀態就會發生轉變:
1、candidate獲得大多數服務器的投票,成爲leader,此時leader會馬上發出心跳消息包,通知其他機器它成爲了leader
2、candidate收到來自其他機器的心跳消息包,且該心跳包含的term大於當前candidate的term,說明已經有新的leader產生,candidate轉爲follwoer。(如果心跳包含的term小於當前candidate的term,candidate會直接忽略該心跳)。
3、所有的candidate在一段時間內都沒能成功獲得大多數投票,該次選舉被認爲超時,candidate會重新進行一次選舉。但是如果沒有其他的限制,多個follower同時成爲candidate,candidate就會同時發出RequestVote請求,投票就會被分散到多個candidate上,因此沒有candidate贏得選舉,選舉超時,再發起一次選舉又是相同的結果,就會進入一個超時導致沒有leader的死循環。

爲了解決第三種情況,Raft用了隨機超時時間來確保投票分散的情況不會發生。隨機超時時間的意思就是,Raft的超時時間是在一個區間(如150-300ms)裏面選擇,而不是固定的一個時間單位。這樣一來,大部分情況下就只有一個服務器會超時。另外,在超時之後,Raft使用隨機時間來啓動一次新的選舉,這樣就不會出現所有的candidate同時開始重新選舉而導致進入選舉超時的死循環。

這個隨機超時時間機制簡單且有效地解決這個死循環的問題。

日誌複製

Raft是保證分佈式系統數據一致性的協議,主要的工作就是接收客戶端的數據,並同步數據,保證所有節點的數據一致。

日誌複製流程

論文裏面多次提到的log是RAFT系統中要同步的數據,稱爲日誌條目,日誌的格式如下圖所示,每個日誌包含要執行的指令、term和日誌的下標,保存term的目的是爲了及時發現日誌之間的不一致性。

leader選舉出來後,leader就開始正常工作,接受來自客戶端的請求,客戶端的請求包含了需要執行的指令,leader收到客戶端的指令後,把指令作爲日誌條目(log entry)保存起來,同時發送Append請求到所有的follower,通知它們同步該日誌條目,同步完成後,請求返回成功。如果大多數服務器都完成了日誌的同步,leader認爲這個指令是可以commit的,commit時會檢查所有的日誌,會將上一個term產生的可提交的日誌一併提交(下面會提到),然後leader就會將可提交的日誌裏面的指令執行到狀態機,並把返回結果給客戶端。

如果follower同步過程比較緩慢(比如超時、網絡緩慢、響應丟失),leader會一直重試Append請求,直到所有的follower成功保存了所有的日誌條目。

這裏要注意的是,即使leader已經給客戶端響應了,leader還是會重試,這個是合理的,因爲只要leader給客戶端響應了,說明該日誌條目是可提交的,剩下的工作就是保證日誌同步到所有的follower。對於follower而言,一個日誌條目被複制了,還是需要被執行,只有在follower意識到該條目是可提交的,纔會將它真正執行到follower的狀態機。

上面描述的日誌複製流程如下圖所示:

1、client發起指令

2、leader收到指令,發起Append請求到所有follower

3、follower同步日誌到本地

4、follower操作完成,返回給leader

5、如果大多數的follower完成同步,leader返回響應給client

日誌匹配特性

關於日誌,Raft實現並維護了以下兩個特性:

1、如果兩個日誌條目有相同的下標和term,那麼它們保存的指令是一樣的

2、如果兩個日誌條目有相同的下標和term,那麼在它們之前的日誌都是一樣的

這兩個特性共同構成了Raft的日誌匹配特性。如果兩個日誌條目有相同的下標和term,那麼這些日誌都是相同的。在Raft中,leader在某個term的某個下標最多隻會創建一個日誌條目,且它的下標不會發生變化。每一次Append請求時都會做一致性檢查(follower在收到Append請求時,如果發現前一個日誌條目的下標和term與請求包含prevLogIndex與prevLogTerm的不一致,follower會拒絕該請求)。Raft通過以上操作來維護日誌匹配特性,從而保證了日誌條目的一致性。

日誌的一致性

正常情況下,leader與follower的數據是保持一致的,但是如果leader突然掛了,可能會導致數據出現不一致。

在Raft裏面,leader通過強制follower直接複製leader的日誌條目來解決數據不一致的問題,如果follower與leader的日誌條目不一樣,那麼follower只認leader的數據,本地的數據會被leader的數據覆蓋。

leader爲每個follower維護一個nextIndex(leader即將發給follower的下一個日誌條目的下標),在Append請求執行時,如果發現follower的log與leader的不一致,follower會拒絕該請求,leader收到拒絕的響應,會將拒絕的follower的nextIndex減一,然後再發起一次Append請求,如果Append請求成功,leader和follower在該下標位置上的日誌條目就一致了,且在接下來的任期裏,這個日誌會一直保持。

有了日誌複製的功能,leader不需要其他任何操作就能恢復數據的一致性,leader不會刪除或覆蓋自己的日誌條目,只需要正常的執行,在Append請求失敗時會自動檢查一致性,最終達到一致。

日誌複製的機制也展示了Raft的特性:只要大多數機器都是正常運行的情況下,Raft會接收、複製和執行新的日誌條目;正常情況下,經過一次RPC後新的日誌條目都可以被複制到大多數機器;單個機器超時不會影響整體的性能。

安全性

前面介紹了leader的選舉和日誌的複製,但僅靠這兩個機制還不能有效地保證每一臺狀態機使用相同的順序執行相同的命令。比如在僅有這兩種機制的前提下,遇到一些異常的情況,日誌就會亂序或者被其他當選的leader覆蓋,因此還需要一些其他的機制來保障數據的一致性。

選舉限制

在Raft裏的第一個限制:限制日誌的流向只能從leader到follower,且leader不會覆蓋已經存在的日誌條目。

第二個限制,是選舉leader時候的投票限制,candidate包含了所有已提交的日誌條目才能被選上leader。

每一次選舉leader,candidate都會向其他機器發起請求,獲得每個機器最新的日誌條目信息,如果candidate的日誌比大多數機器的版本還新,那麼它就能被選上leader。

日誌版本是最新的定義爲:如果兩條日誌最大下標的term不相同,那麼term較大者勝出;如果兩條日誌最大下標相同,那麼日誌長度較大者勝出。可以用以下邏輯描述:

有機器s1,s2,定義機器包含的日誌log,log裏面最新的日誌下標爲lastIndex,log的長度爲log.length,通過以下的邏輯可以判斷日誌是否最新:

log1 := s1.log
log2 := s2.log


if log1[lastIndex].term != log2.[lastIndex].term && log1.term > log2.term ||
    log1[lastIndex].term == log2.[lastIndex].term && log1.length > log2.length 
then
    s1 win
else
    s2 win

提交之前term的日誌條目

前面提到,如果一個日誌被複制到大多數機器,那麼leader就認爲該日誌是可提交的,但是,如果leader提交該日誌前崩潰了,就會出現意想不到的現象,舉個例子:

如圖所示的場景:
a、S1被選中leader,在index=2處複製日誌進行到一半(只在S2進行了)
b、S1掛了,通過S3、S4和自己的投票,S5被選中leader,term=3,在index=2的地方接收了一個與S1不同的日誌
c、S5掛了,S1重新啓動,被選中爲leader,term=4,但是會繼續它上一次的複製,term=2,index=2的日誌複製到S3,此時這條日誌已經被複制到大多數機器,被認爲可以提交的,此時準備進行提交

以下兩種是假設的情景:
如果
d、S1掛了,S5可以被選爲leader,複製步驟b的數據,此時index=2的數據就被S5覆蓋了

如果
e、如果S1在掛了之前複製了當前term收到的日誌數據(index=3,term=4)到大多數機器,而根據投票的限制,S5不能被選爲leader,因爲log5[3].term < log3[3].term。複製到大多數機器後,這條數據可以被提交,提交時它之前的日誌條目也會被一併提交

上面的d、e是假設的場景,爲了避免上面描述的問題,Raft有另一個限制,leader不會提交前一個term被認爲可以提交的日誌,只能提交當前term認爲可以被提交的日誌。由於日誌匹配特性,Raft會在提交當前term的日誌時,把之前term可提交的日誌間接地一併提交(如果兩個日誌條目有相同的下標和term,那麼在它們之前的日誌都是一樣的,所以leader必須把上一個可提交的日誌提交了,否則就會出現leader或follower之間的數據不一致)。

leader提交日誌時的操作是:

for entry in GetEntries(lastCommited, newCommited):
   entry.Commit();

通過選舉條件限制和leader只能提交當前term的日誌的限制,就不會出現上面圖中描述的問題。

根據Raft算法的基礎,可以論證leader完整性特性,可以進一步證明狀態機,詳細的論證過程就不翻譯了,如果有興趣的話可以翻閱論文。

follower和candidate崩潰

follower和candidate崩潰處理方式是一樣的,如果follower/candidate崩潰了,那麼後續的Append請求或者RequestVote請求就會失敗。Raft的實現是不斷重試這些請求,直到機器重新啓動。(Raft的RPC請求是冪等的,所以重複的RPC不會影響系統。)

時間和可用性

爲了保證Raft系統的高可用,Raft要求安全性不會受執行時間的影響,即,系統不會由於機器的響應時間出現異常的結果。因爲需要一個穩定的leader來保證Raft系統的正常運行。

Raft有三個時間屬性

  • broadcastTime: 服務器發出rpc到收到響應的時間,通常是0.5~20ms,因爲rpc需要將數據持久化到本地

  • electTimeout: 選舉leader超時時間,10~500ms

  • MTBF:

    機器平均故障時間,通用以月爲單位

通過以下的時間表達式,Raft可以保證leader的穩定性:

broadcastTime<=electionTimeout<=MTBF

1、因爲Raft需要依賴心跳包來維持一個leader,所以broadcastTime<=electionTimeout

2、爲了保持系統穩定,electionTimeout<=MTBF

解答Q&A

上一次留下的一個問題:

Q1:在leader選舉過程中,candidate是怎麼決定要投票給發起RequestVote Rpc的機器?是不是接收到請求就要投票?成爲leader有沒有什麼要求?

這個問題在選舉限制部分講到,一次選舉leader,candidate都會向其他機器發起請求,獲得每個機器最新的日誌條目信息,如果candidate的日誌比大多數機器的版本還新,那麼它就能被選上leader。

日誌版本是最新的定義爲:如果兩條日誌最大下標的term不相同,那麼term較大者勝出;如果兩條日誌最大下標相同,那麼日誌長度較大者勝出。即:

    log1 := s1.log
    log2 := s2.log    if log1[lastIndex].term != log2.[lastIndex].term && log1.term > log2.term ||
        log1[lastIndex].term == log2.[lastIndex].term && log1.length > log2.length
    then
        s1 win    else
        s2 win

Q2:日誌匹配特性與leader提交時要提交上一個term可提交的日誌有什麼關聯?

回顧一下日誌匹配特性:

1、如果兩個日誌條目有相同的下標和term,那麼它們保存的指令是一樣的

2、如果兩個日誌條目有相同的下標和term,那麼在它們之前的日誌都是一樣的

如果leader不提交了上一個term的日誌,會出現數據不一致,無法維持日誌匹配特性第二部分。

舉個例子,如圖所示:

假設有5臺機器,如果在term2時,S1是leader,大多數機器都複製了下標2的日誌,此時S4、S5並沒有數據,提交之前S1掛了,然後term3時S3被選爲leader,大多數機器都複製了下標3的日誌,此時leader(S3)提交了下標3的日誌,但是不提交下標2的數據。那麼S4只能收到index=3,term=3的數據,那麼就會出現S1、S4的index=3,term=3之前的日誌數據不一致了。

如果term3時把下標2到下標3之間的數據都提交了,leader在複製時發現日誌不一致,就會強制使用leader的數據,就不會出現這個問題。就符合日誌匹配特性了。

總結

到這裏爲止,本次探索Raft的歷程告一段落,這一次的探索,收穫頗深,日誌複製和leader應該算是Raft中最難的部分,反覆閱讀論文和上網查閱資料,終於搞懂了遇到的難題。

Raft通過選舉限制確保了成爲leader的機器必須是擁有最新數據的,避免了數據被覆蓋的情況;通過提交時提交之前term的日誌滿足日誌匹配特性;通過數據的一致性檢查保證了leader與follower之間的數據一致性。總的來說,Raft通過限制和規定,保證了系統的穩定運行和數據一致性,這也是協議的功能,使用該協議的系統必須遵從這些約定才能正常的運作。

下一次再繼續探索集羣關係變化、日誌壓縮處理等等話題。

參考鏈接:

Question about Committing entries from previous terms

https://groups.google.com/forum/#!topic/raft-dev/d-3XQbyAg2Y

Raft算法詳解

https://zhuanlan.zhihu.com/p/32052223

一文搞懂Raft算法

https://www.cnblogs.com/xybaby/p/10124083.html

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,麻煩順手點個贊吧,謝謝

更多精彩內容,請關注個人公衆號。

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