前面一篇文章講了Paxos協議,這篇文章講它的姊妹篇Raft協議,相對於Paxos協議,Raft協議更爲簡單,也更容易工程實現。有關Raft協議和工程實現能夠參考這個連接https://raft.github.io/,裏面包含了大量的論文,視屏已經動畫演示,很是有助於理解協議。
概念與術語
leader:領導者,提供客戶提供服務(生成寫日誌)的節點,任什麼時候候raft系統中只能有一個leader。
follower:跟隨者,被動接受請求的節點,不會發送任何請求,只會響應來自leader或者candidate的請求。若是接受到客戶請求,會轉發給leader。
candidate:候選人,選舉過程當中產生,follower在超時時間內沒有收到leader的心跳或者日誌,則切換到candidate狀態,進入選舉流程。
termId:任期號,時間被劃分紅一個個任期,每次選舉後都會產生一個新的termId,一個任期內只有一個leader。termId至關於paxos的proposalId。
RequestVote:請求投票,candidate在選舉過程當中發起,收到quorum(多數派)響應後,成爲leader。
AppendEntries:附加日誌,leader發送日誌和心跳的機制
election timeout:選舉超時,若是follower在一段時間內沒有收到任何消息(追加日誌或者心跳),就是選舉超時。
Raft協議主要包括三部分,leader選舉,日誌複製和成員變動。git
Raft協議的原則和特色
a.系統中有一個leader,全部的請求都交由leader處理,leader發起同步請求,當多數派響應後才返回客戶端。
b.leader歷來不修改自身的日誌,只作追加操做
c.日誌只從leader流向follower,leader中包含了全部已經提交的日誌
d.若是日誌在某個term中達成了多數派,則之後的任期中日誌必定會存在
e.若是某個節點在某個(term,index)應用了日誌,則在相同的位置,其它節點必定會應用相同的日誌。
f.不依賴各個節點物理時序保證一致性,經過邏輯遞增的term-id和log-id保證。
e.可用性:只要有大多數機器可運行並可相互通訊,就能夠保證可用,好比5節點的系統能夠容忍2節點失效。
f.容易理解:相對於Paxos協議實現邏輯清晰容易理解,而且有不少工程實現,而Paxos則難以理解,也沒有工程實現。
g.主要實現包括3部分:leader選舉,日誌複製,複製快照和成員變動;日誌類型包括:選舉投票,追加日誌(心跳),複製快照github
leader選舉流程
關鍵詞:隨機超時,FIFO
服務器啓動時初始狀態都是follower,若是在超時時間內沒有收到leader發送的心跳包,則進入candidate狀態進行選舉,服務器啓動時和leader掛掉時處理同樣。爲了不選票瓜分的狀況,好比5個節點ABCDE,leader A 掛掉後,還剩4個節點,raft協議約定,每一個服務器在一個term只能投一張票,假設B,D分別有最新的日誌,且同時發起選舉投票,則可能出現B和D分別獲得2張票的狀況,若是這樣都得不到大多數確認,沒法選出leader。爲了不這種狀況發生,raft利用隨機超時機制避免選票瓜分狀況。選舉超時時間從一個固定的區間隨機選擇,因爲每一個服務器的超時時間不一樣,則leader掛掉後,超時時間最短且擁有最多日誌的follower最早開始選主,併成爲leader。一旦candidate成爲leader,就會向其餘服務器發送心跳包阻止新一輪的選舉開始。服務器
發送日誌信息:(term,candidateId,lastLogTerm,lastLogIndex)
candidate流程:
1.在超時時間內沒有收到leader的日誌(包括心跳)
2.將狀態切換爲candidate,自增currentTerm,設置超時時間
3.向全部節點廣播選舉請求,等待響應,可能會有如下三種狀況:
(1).若是收到多數派迴應,則成爲leader
(2).若是收到leader的心跳,且leader的term>=currentTerm,則本身切換爲follower狀態,
不然,保持Candidate身份
(3).若是在超時時間內沒有達成多數派,也沒有收到leader心跳,則極可能選票被瓜分,則會自增currentTerm,進行新一輪的選舉網絡
follower流程:
1.若是term < currentTerm,說明有更新的term,返回給candidate。
2.若是尚未投票,或者candidateId的日誌(lastLogTerm,lastLogIndex)和本地日誌同樣或更新,則投票給它。
注意:一個term週期內,每一個節點最多隻能投一張票,按照先來先到原則動畫
日誌複製流程
關鍵詞:日誌連續一致性,多數派,leader日誌不變動
leader向follower發送日誌時,會順帶鄰近的前一條日誌,follwer接收日誌時,會在相同任期號和索引位置找前一條日誌,若是存在且匹配,則接收日誌;不然拒絕,leader會減小日誌索引位置並進行重試,直到某個位置與follower達成一致。而後follower刪除索引後的全部日誌,並追加leader發送的日誌,一旦日誌追加成功,則follower和leader的全部日誌就保持一致。只有在多數派的follower都響應接受到日誌後,表示事務能夠提交,才能返回客戶端提交成功。
發送日誌信息:(term,leaderId,prevLogIndex,prevLogTerm,leaderCommitIndex)
leader流程:
1.接收到client請求,本地持久化日誌
2.將日誌發往各個節點
3.若是達成多數派,再commit,返回給client。
備註:
(1).若是傳遞給follower的lastLogIndex>=nextIndex,則從nextIndex繼續傳遞
.若是返回成功,則更新follower對應的nextIndex和matchIndex
.若是失敗,則表示follower還差更多的日誌,則遞減nextIndex,重試
(2).若是存在N>commitIndex,且多數派matchIndex[i]>=N, 且log[N].term == currentTerm,
設置commitIndex=N。spa
follower處理流程:
1.比較term號和自身的currentTerm,若是term<currentTerm,則返回false
2.若是(prevLogIndex,prevLogTerm)不存在,說明還差日誌,返回false
3.若是(prevLogIndex,prevLogTerm)與已有的日誌衝突,則以leader爲準,刪除自身的日誌
4.將leader傳過來的日誌追加到末尾
5.若是leaderCommitIndex>commitIndex,說明是新的提交位點,回放日誌,設置commitIndex =
min(leaderCommitIndex, index of last new entry)線程
備註:默認狀況下,若是日誌不匹配,會按logIndex逐條往前推動,直到找到match的位置,有一個簡單的思路是,每次往前推動一個term,這樣能夠減小了網絡交互,儘快早點match的位置,代價是可能傳遞了一些多餘的日誌。設計
快照流程
避免日誌佔滿磁盤空間,須要按期對日誌進行清理,在清理前須要作快照,這樣新加入的節點能夠經過快照+日誌恢復。
快照屬性:
1.最後一個已經提交的日誌(termId,logIndex)
2.新的快照生成後,能夠刪除以前的日誌和之前的快照。
刪日誌不能太快,不然,crash後的機器,原本能夠經過日誌恢復,若是日誌不存在,須要經過快照恢復,比較慢。日誌
leader發送快照流程
傳遞參數(leaderTermId, lastIndex, lastTerm, offset, data[], done_flag)
1.若是發現日誌落後太遠(超過閥值),則觸發發送快照流程
備註:快照不能太頻繁,不然會致使磁盤IO壓力較大;但也須要按期作,清理非必要的日誌,緩解日誌的空間壓力,另外能夠提升follower追趕的速度。blog
follower接收快照流程
1.若是leaderTermId<currentTerm, 則返回
2.若是是第一個塊,建立快照
3.在指定的偏移,將數據寫入快照
4.若是不是最後一塊,等待更多的塊
5.接收完畢後,丟掉之前舊的快照
6.刪除掉不須要的日誌
集羣配置變動
C(old): 舊配置
C(new): 新配置
C(old-new): 過渡配置,須要同時在old和new中達成多數派才行
原則:配置變動過程當中,不會致使出現兩個leader
二階段方案:引入過渡階段C(old-new)
約定:任何一個follower在收到新的配置後,就採用新的配置肯定多數派。
變動流程:
1.leader收到從C(old)切換到C(new)配置的請求
2.建立配置日誌C(old-new),這條日誌須要在C(old)和C(new)中同時達成多數派
3.任何一個follower收到配置後,採用的C(old-new)來肯定日誌是否達成多數派(即便C(old-new)這條日誌還沒達成多數派)
備註:1,2,3階段只有可能C(old)節點成爲leader,由於C(old-new)沒有可能成爲多數派。
4.C(old-new)日誌commit(達成多數派),則不管是C(old)仍是C(new)都沒法單獨達成多數派,即不會存在兩個leader
5.建立配置配置日誌C(new),廣播到全部節點
6.一樣的,任何一個follower收到配置後,採用的C(new)來肯定日誌是否達成多數派
備註:在4,5,6階段,只有可能含有C(old-new)配置的節點成爲leader。
7.C(new)配置日誌commit後,則C(old-new)沒法再達成多數派
8.對於不在C(new)配置的節點,就能夠退出了,變動完成。
備註:在7,8階段,只有可能含有C(new)配置成爲leader。
因此整個過程當中永遠只會有一個leader。對於leader不在C(new)配置的狀況,須要在C(new)日誌提交後,自動關閉。
除了上述的兩階段方案,後續Raft做者又提出了一個相對簡單的一階段方案,每次只添加或者刪除一個節點,這樣設計不用引入過渡狀態,這裏再也不贅述,有興趣的同窗能夠去看他的畢業論文,我會附在後面的參考文檔裏面。
Q&A
1.Raft協議中是否存在「活鎖」,如何解決?
活鎖是相對死鎖而言,所謂死鎖,就是兩個或多個線程相互鎖等待,致使都沒法推動的狀況,而活鎖則是多個工做線程(節點)都在運轉,可是總體系統的狀態沒法推動,好比basic-paxos中某些狀況下投票老是沒有辦法達成多數派。在Raft中,因爲只要一階段提交(只有leader提議),在日誌複製的過程當中不存在活鎖問題。可是選主過程當中,可能出現多個節點同時發起選主的狀況,這樣致使選票瓜分,沒法選出主,在下一輪選舉中依舊如此,致使系統狀態沒法往前推動。Raft經過隨機超時解決這個「活鎖」問題。
2.Raft系統對於各個節點的物理時鐘強一致有要求嗎?
Raft協議對物理是時鐘一致性沒有要求,不須要經過原子鐘NTP來校準時間,可是對於超時時間的設置有要求,具體規則以下:
broadcastTime ≪ electionTimeout ≪ MTBF(Mean Time Between Failure)
首先,廣播時間要遠小於選舉超時時間,leader經過廣播不間斷給follower發送心跳,若是這個時間比超時時間短,就會致使follower誤覺得leader掛了,觸發選主;而後是超時時間要遠小於機器的平均故障時間,若是MTBF比超時時間還短,則永遠會發生選主問題,而在選主完成以前,沒法對外正常提供服務,所以須要保證。通常broadcastTime能夠認爲是一個網絡RTT,同城1ms之內,異地100ms之內,若是是跨國家,可能須要幾百ms;而機器平均故障時間至少是以月爲單位,所以選舉超時時間須要設置1s到5s左右便可。
3.如何保證leader上擁有了全部日誌?
一方面,對於leader不變場景,日誌只能從leader流向follower,而且發生衝突時以leader的日誌爲準;另外一方面,對於leader一直有變換的場景,經過選舉機制來保證,選舉時採用(LogTerm,LogIndex)誰更新的比對方式,而且要獲得多數派的承認,說明新leader的日誌至少是多數派中最新的,另外一方面,提交的日誌必定也是達成了多數派,因此推斷出leader有全部已提交的日誌,不會漏。
4.Raft協議爲何須要日誌連續性,日誌連續性有有什麼優點和劣勢?
由Raft協議的選主過程可知,(termId,logId)必定在多數派中最新纔可能成爲leader,也就是說leader中必定已經包含了全部已經提交的日誌。因此leader不須要從其它follower節點獲取日誌,保證了日誌永遠只從leader流向follower,簡化了邏輯。但缺陷在於,任何一個follower在接受日誌前,都須要接受以前的全部日誌,而且只有追遇上了,纔能有投票權利【不然,複製日誌時,不考慮它們是大多數】,若是日誌差的比較多,就會致使follower須要較長的時間追趕。任何一個沒有追上最新日誌的follower,沒有投票權利,致使網絡比較差的狀況下,不容易達成多數派。而Paxos則容許日誌有「空洞」,對網絡抖動的容忍性更好,但處理「空洞」的邏輯比較複雜。
5.Raft如何保證日誌連續性?
leader向follower發送日誌時,會順帶鄰近的前一條日誌,follwer接受日誌時,會在相同任期號和索引位置找前一條日誌,若是存在且匹配,則接受日誌,不然拒絕接受,leader會減小日誌索引位置並進行重試,直到某個位置與follower達成一致。而後follower刪除索引後的全部日誌,並追加leader發送的日誌,一旦日誌追加成功,則follower和leader的全部日誌就保持一致。而Paxos協議沒有日誌連續性要求,能夠亂序確認。
6.若是TermId小的先達成多數派,TermId大的怎麼辦?可能嗎?
若是TermId小的達成了多數派,則說明TermId大的節點之前是leader,擁有最多的日誌,可是沒有達成多數派,所以它的日誌能夠被覆蓋。但該節點會嘗試繼續投票,新leader發送日誌給該節點,若是leader發現返回的termT>currentTerm,且尚未達成多數派,則從新變爲follower,促使TermId更大的節點成爲leader。但並不保證擁有較大termId的節點必定會成爲leader,由於leader是優先判斷是否達成多數派,若是已經達成多數派了,則繼續爲leader。
7.達成多數派的日誌就必定認爲是提交的?
不必定,必定是在current_term內產生的日誌,而且達成多數派才能認爲是提交的,持久化的,不會變的。Raft中,leader保持原始日誌的termId不變,任何一條日誌,都有termId和logIndex屬性。在leader頻繁變動的狀況下,有可能出現某條日誌在某種狀態下達成了多數派,但日誌最終可能被覆蓋掉,好比下圖:
(a).S1是leader,termId是2,寫了一條日誌到S1和S2,(termId,logIndex)爲(2,2)
(b).S1 crash,S5利用S3,S4,S5當選leader,自增termId爲3,本地寫入一條日誌,(termId,logIndex)爲(3,2)
(c).S5 crash,S1 重啓後從新當選leader,自增termId爲4,將(2,2)從新複製到多數派,提交前crash
(d).S1 crash,S5利用S2,S3,S4當選leader,則將(3,2)的日誌從新複製到多數派,並提交,這樣(2,2)這條日誌曾經雖然達成多數派也會被覆蓋。
(e).假設S1在第一個任期內,將(2,2)達成多數派,則後面S3不會成爲leader,也就不會出現覆蓋的狀況。
參考文檔
https://raft.github.io/raft.pdf
https://ramcloud.stanford.edu/~ongaro/thesis.pdf
https://ramcloud.stanford.edu/~ongaro/userstudy/paxos.pdf