原文地址: https://qeesung.github.io/202...
Raft 論文地址:https://ramcloud.atlassian.ne...html
Raft論文中分爲三塊:git
本文中主要介紹日誌複製
github
領導人必須從客戶端接收日誌而後複製到集羣中的其餘節點,而且強制要求其餘節點的日誌保持和本身相同。數組
鑑於日誌複製這一塊比較複雜,能夠結合下面兩個網頁來理解:安全
複製狀態機一般都是基於複製日誌實現的,每個服務器存儲一個包含一系列指令的日誌,而且按照日誌的順序進行執行。以下圖所示:服務器
PUT KEY VALUE
狀態機是按照同步到服務器上的指令的順序,一個一個的去Apply
指令,因此指令的順序很重要,若是指令Apply
的順序不一致,或者丟失部分指令,那麼最終狀態機的狀態也會不一致。網絡
而咱們知道網絡是不穩定的,好比延遲,分區,丟包,榮譽和亂序等錯誤。若是不保證狀態機Apply
指令徹底如出一轍,那麼將會致使不一致的結果。而Raft的日誌複製機制則能保證在發生上述網絡問題的時候,全部的服務器都能同步到徹底如出一轍的日誌,也就是說能保證每個服務器的狀態機Apply
到徹底如出一轍的指令。併發
全部服務器上都維持的狀態:優化
commitIndex
:最大的已經被提交的日誌索引lastApplied
:最後被應用到狀態的日誌條目索引lastApplied
應該小於等於commitIndex
,由於只有在日誌被提交之後才能被Apply
到狀態機中spa
領導人常常改變的:
nextIndex[]
:對於每個服務器,須要發送給他的下一個日誌條目的索引值,初始化爲當前領導人的最後的日誌索引值加一matchIndex[]
:對於每個服務器,已經複製給他的日誌的最高索引值nextIndex[]
和 matchIndex[]
的長度等於整個集羣中的服務器數量。
附加日誌RPC的請求結構:
term
:附加日誌的領導人任期號leaderId
:當前領導人的IdpreLogIndex
:當前要附加的日誌entries
的上一條的日誌索引preLogTerm
:當前要附加的日誌entries
的上一條的日誌任期號entries[]
:須要附加的日誌條目(心跳時爲空)leaderCommit
:當前領導人已經提交的最大的日誌索引值preLogIndex
和preLogTerm
主要是用於跟隨者檢測當前領導人要附加的日誌是否和跟隨者當前的日誌匹配,若是不匹配的話,那麼就須要繼續向前搜尋和領導人匹配的日誌(下面章節會介紹)
而leaderCommit
的話用於告訴跟隨者當前提交到什麼位置了(由於收到附加日誌還不能立刻提交,不然可能存在日誌丟失的狀況),以便跟隨者將已經提交的日誌Apply
到狀態機中
附加日誌RPC的響應結構:
term
:跟隨者的當前的任務號success
:跟隨者是否接收了當前的日誌,在preLogIndex
和preLogTerm
匹配的狀況下爲true
,不然返回false
每個日誌條目存儲一條狀態機指令和從領導人收到這條指令時的任期號。日誌中的任期號用來檢查是否出現不一致的狀況,每一條日誌條目同時也都有一個整數索引值來代表它在日誌中的位置。
一旦成爲領導人,那麼領導人就會在固定在必定時間內發送空的附加日誌,也就是心跳,以組織更隨着超時。
若是領導人收到來本身客戶端的請求,那麼首先將請求的指令附加到本身的日誌隊列中,而後領導人會將新附加的日誌條目經過附加日誌的RPC發送到全部的跟隨者。
領導人中維護了兩個常常變更的屬性nextIndex[]
和matchIndex[]
,用於記錄要發什麼日誌和跟隨者收到了什麼日誌。
其中nextIndex[]
記錄了須要發送給每個服務器的日誌的下一個索引值,這個數組會在領導人被選舉出來的時候初始化爲領導人最後的索引加一。注意這個nextIndex[]
只是記錄了要發給下一次附加日誌要發給服務器的索引值,這個索引值可能並不必定準確,好比在跟隨者和領導人的日誌不一致的狀況下,nextIndex[]
的值就須要進行遞減以找到和跟隨者最大的匹配的日誌,具體流程後文中會解釋。
而matchIndex[]
主要是用來記錄跟隨者收到了那些日誌,以方便領導人確認是否當前的日誌能夠進行提交,由於必需要至少N/2+1
的集羣成員(包括領導人本身)都確認收到了日誌才能夠進行的日誌的提交。
咱們知道領導人在附加日誌的時候是併發的向全部跟隨者發起的,而且是在固定週期內定時發送的,因此可能在上一個RPC沒有收到響應的狀況下,發出下一個RPC請求,或者是收到過時的日誌追加請求等等。
比對響應的Term
和當前的Term
以確認本身是否過時:
Term
大於當前的Term
,那麼說明當前的領導人已通過期,立刻將本身切換爲跟隨者Term
小於當前的Term
,那麼說明當前的收到了過時的響應(可能網路延遲致使),那麼忽略判斷附加RPC時的preLogIndex
和當前的nextIndex[peer]-1
是否相等,用於判斷是不是過時的響應,或者是nextIndex[peer]
是否被更改了:
判斷響應的success
是否爲真:
preLogIndex
和preLogTerm
和跟隨者的日誌不匹配,進行步驟5附加日誌成功之後,須要更新matchIndex[peer]
中的值爲已經追加日誌的索引值,表示追加日誌成功,判斷整個matchIndex[]
數組中的是否有一半都大於等於追加日誌的索引:
commitIndex
,進行日誌的提交nextIndex[peer]
來實現。跟隨者收到附加日誌的請求,不能簡單的將日誌追加到本身的日誌後面,由於跟隨者的日誌可能和領導人有衝突,或者跟隨者缺失更多的日誌,入下圖所示。
那麼必定要確保本次附加日誌的以前的全部日誌都相同,也就是說附加當前的日誌以前,缺日誌就要把缺失的日誌補上,日誌衝突了,就要把衝突的日誌覆蓋(領導人能夠強行覆蓋跟隨者的日誌)
判斷附加日誌任期Term
和當前的Term
是否相同:
Term
小於當前的Term
,那麼說明收到了來自過時的領導人的附加日誌請求,那麼拒接處理。Term
大於當前的Term
,那麼更新當前的Term
爲請求的Term
,進行步驟2Term
和當前的Term
相等,那麼說明請求合法,進行步驟2判斷preLogIndex
是否大於當前的日誌長度或者preLogIndex
位置處的任期是否和preLogTerm
相等以檢測要附加的日誌以前的日誌是否匹配:
preLogIndex
的長度大於當前的日誌的長度,那麼說明跟隨者缺失日誌,那麼拒絕附加日誌,返回false
preLogIndex
處的任期和preLogTerm
不相等,那麼說明日誌有衝突,拒絕附加日誌,返回false
preLogIndex
以後的日誌是否匹配。逐一比對要附加的日誌entries[]
是否和本身preLogIndex
以後的日誌是否有衝突:
entries[]
中的任何一位置的日誌發生衝突,那麼須要將以後的日誌進行截斷,並追加爲entries[]
中以後的日誌若是沒有發生衝突,那麼存在兩種狀況:
entries[]
的日誌所有匹配,這種狀況多是重複的附加日誌RPC,那麼這種狀況只會簡單的校驗一遍全部日誌entries[]
的日誌匹配當前的全部日誌,那麼將沒有匹配的日誌全都追加到當前的日誌後面。上述的步驟3是很重要的,若是不對現有的日誌進行比對,並且簡單的進行截斷追加日誌,那麼是很危險,由於可能收到延時的重複日誌附加請求而致使日誌沒必要要的截斷,從而致使已經提交的日誌丟失。
另外還有一個優化點,若是存在大量的衝突日誌,那麼若是經過遞減nextIndex[peer]
將會很慢,因此能夠經過批量跳過沖突日誌方式來作到,能夠再響應中添加conflictIndex
和conflictTerm
來作到,這裏不展開詳細討論。
全部的服務器都有兩個常常變更的值commitIndex
和lastApplied
,commitIndex
表示已經提交的日誌索引,而lastApplied
表示最後Apply
到狀態機中的日誌索引。commtiIndex
會在日誌成功附加到集羣中的N/2+1
的節點上更新。
通常應用日誌到狀態機中是經過一個獨立的線程來作到的,經過監控是否有新提交的日誌,若是有新提交的日誌,那麼就將日誌Apply
到狀態機中,而且更新lastApplied
。因此通常commitIndex >= lastApplied
在實際的應用中,通常將Raft單獨做爲獨立的一層共識模塊,上層模塊將須要達到共識的指令下發給Raft共識模塊,在Raft模塊達到共識之後,就將達成共識的指令Apply
到上層模塊中。好比Etcd
,TiKV
等等