1 leader選舉
-
1.1 剛開始所有server啓動都是follower狀態
然後等待leader或者candidate的RPC請求、或者超時。
上述3種情況處理如下:
leader的AppendEntries RPC請求:更新term和leader信息,當前follower再重新重置到follower狀態
candidate的RequestVote RPC請求:爲candidate進行投票,如果candidate的term比自己的大,則當前follower再重新重置到follower狀態
超時:轉變爲candidate,開始發起選舉投票
-
1.2 candidate收集投票的過程
candidate會爲此次狀態設置隨機超時時間,一旦出現在當前term中大家都沒有獲取過半投票即split votes,超時時間短的更容易獲得過半投票。
candidate會向所有的server發送RequestVote RPC請求,請求參數見下面的官方圖
上面對參數都說明的很清楚了,我們來重點說說圖中所說的這段話
If votedFor is null or candidateId, and candidate’s log is at least as up-to-date as receiver’s log, grant vote
votedFor是server保存的投票對象,一個server在一個term內只能投一次票。如果此時已經投過票了,即votedFor就不爲空,那麼此時就可以直接拒絕當前的投票(當然還要檢查votedFor是不是就是請求的candidate)。
如果沒有投過票:則對比candidate的log和當前server的log哪個更新,比較方式爲誰的lastLog的term越大誰越新,如果term相同,誰的lastLog的index越大誰越新。
candidate統計投票信息,如果過半同意了則認爲自己當選了leader,轉變成leader狀態,如果沒有過半,則等待是否有新的leader產生,如果有的話,則轉變成follower狀態,如果沒有然後超時的話,則開啓下一次的選舉。
2 log複製
2.1 請求都交給leader
一旦leader選舉成功,所有的client請求最終都會交給leader(如果client連接的是follower則follower轉發給leader)
2.2 處理請求過程
-
2.2.1 client請求到達leader
leader首先將該請求轉化成entry,然後添加到自己的log中,得到該entry的index信息。entry中就包含了當前leader的term信息和在log中的index信息
-
2.2.2 leader複製上述entry到所有follower
來看下官方給出的AppendEntries RPC請求
從上圖可以看出對於每個follower,leader保持2個屬性,一個就是nextIndex即leader要發給該follower的下一個entry的index,另一個就是matchIndex即follower發給leader的確認index。
一個leader在剛開始的時候會初始化:
nextIndex=leader的log的最大index+1 matchIndex=0
然後開始準備AppendEntries RPC請求的參數
prevLogIndex=nextIndex-1 prevLogTerm=從log中得到上述prevLogIndex對應的term
然後開始準備entries數組信息
從leader的log的prevLogIndex+1開始到lastLog,此時是空的
然後把leader的commitIndex作爲參數傳給
leaderCommit=commitIndex
至此,所有參數準備完畢,發送RPC請求到所有的follower,follower再接收到這樣的請求之後,處理如下:
-
重置HeartbeatTimeout
-
檢查傳過來的請求term和當前follower的term
Reply false if term < currentTerm
-
檢查prevLogIndex和prevLogTerm和當前follower的對應index的log是否一致,
Reply false if log doesn’t contain an entry at prevLogIndex whose term matches prevLogTerm
這裏可能就是不一致的,因爲初始prevLogIndex和prevLogTerm是leader上log的lastLog,不一致的話返回false,同時將該follower上log的lastIndex傳送給leader
-
leader接收到上述false之後,會記錄該follower的上述lastIndex
macthIndex=上述lastIndex nextIndex=上述lastIndex+1
然後leader會從新按照上述規則,發送新的prevLogIndex、prevLogTerm、和entries數組
-
follower檢查prevLogIndex和prevLogTerm和對應index的log是否一致(目前一致了)
-
然後follower就開始將entries中的數據全部覆蓋到本地對應的index上,如果沒有則算是添加如果有則算是更新,也就是說和leader的保持一致
-
最後follower將最後複製的index發給leader,同時返回ok,leader會像上述一樣來更新follower的macthIndex
-
-
2.2.3 leader統計過半複製的entries
leader一旦發現有些entries已經被過半的follower複製了,則就將該entry提交,將commitIndex提升至該entry的index。(這裏是按照entry的index先後順序提交的),具體的實現可以通過follower發送過來macthIndex來判定是否過半了
一旦可以提交了,leader就將該entry應用到狀態機中,然後給客戶端回覆OK
然後在下一次heartBeat心跳中,將commitIndex就傳給了所有的follower,對應的follower就可以將commitIndex以及之前的entry應用到各自的狀態機中了
3 安全
3.1 選舉約束
對於上述leader選舉有個重點強調的地方就是
被選舉出來的leader必須要包含所有已經比提交的entries
如leader針對複製過半的entry提交了,但是某些follower可能還沒有這些entry,當leader掛了,該follower如果被選舉成leader的時候,就可能會覆蓋掉了上述的entry了,造成不一致的問題,所以新選出來的leader必須要滿足上述約束
目前對於上述約束的簡單實現就是:
只要當前server的log比半數server的log都新就可以,這裏的新就是上述說的:
誰的lastLog的term越大誰越新,如果term相同,誰的lastLog的index越大誰越新
但是正是這個實現並不能完全實現約束,纔會產生下面的另外一個問題,一會會詳細案例來說明這個問題
3.2 當前term的leader是否能夠直接提交之前term的entries
raft給出的答案是:
當前term的leader不能“直接”提交之前term的entries
也就是可以間接的方式來提交。我們來看下raft給出不能直接提交的案例
最上面一排數字表示的是index,s1-s5表示的是server服務器,a-e表示的是不同的場景,方框裏面的數字表示的是term
詳細解釋如下:
- a場景:s1是leader,此時處於term2,並且將index爲2的entry複製到s2上
- b場景:s1掛了,s5當選爲leader,處於term3,s5在index爲2的位置上接收到了新的entry
-
c場景:s5掛了,s1當選爲leader,處於term4,s1將index爲2,term爲2的entry複製到了s3上,此時已經滿足過半數了
重點就在這裏:此時處於term4,但是之前處於term2的entry達到過半數了,s1是提交該entry呢還是不提交呢?
假如s1提交的話,則index爲2,term爲2的entry就被應用到狀態機中了,是不可改變了,此時s1如果掛了,來到term5,s5是可以被選爲leader的,因爲按照之前的log比對策略來說,s5的最後一個log的term是3比s2 s3 s4的最後一個log的term都大。一旦s5被選舉爲leader,即d場景,s5會複製index爲2,term爲3的entry到上述機器上,這時候就會造成之前s1已經提交的index爲2的位置被重新覆蓋,因此違背了一致性。
假如s1不提交,而是等到term4中有過半的entry了,然後再將之前的term的entry一起提交(這就是所謂的間接提交,即使滿足過半,但是必須要等到當前term中有過半的entry才能跟着一起提交),即處於e場景,s1此時掛的話,s5就不能被選爲leader了,因爲s2 s3的最後一個log的term爲4比s5的3大,所以s5獲取不到投票,進而s5就不可能去覆蓋上述的提交
這裏再對日誌覆蓋問題進行詳細闡述
日誌覆蓋包含2種情況:
-
commitIndex之後的log覆蓋:是允許的,如leader發送AppendEntries RPC請求給follower,follower都會進行覆蓋糾正,以保持和leader一致。
-
commitIndex及其之前的log覆蓋:是禁止的,因爲這些已經被應用到狀態機中了,一旦再覆蓋就出現了不一致性。而上述案例中的覆蓋就是指這種情況的覆蓋。
從這個案例中我們得到的一個新約束就是:
當前term的leader不能“直接”提交之前term的entries
必須要等到當前term有entry過半了,才順便一起將之前term的entries進行提交
所以raft靠着這2個約束來進一步保證一致性問題。
再來仔細分析這個案例,其問題就是出在:上述leader選舉上,s1如果在c場景下將index爲2、term爲2的entry提交了,此時s5也就不包含所有的commitLog了,但是s5按照log最新的比較方法還是能當選leader,那就是說log最新的比較方法並不能保證3.1中的選舉約束即
被選舉出來的leader必須要包含所有已經比提交的entries
所以可以理解爲:正是由於上述選舉約束實現上的缺陷才導致又加了這麼一個不能直接提交之前term的entries的約束。
3.3 安全性論證
Leader Completeness: 如果一個entry被提交了,那麼在之後的leader中,必然存在該entry。
經過上述2個約束,就能得出Leader Completeness結論。
正是由於上述“不能直接提交之前term的entries”的約束,所以任何一個entry的提交必然存在當前term下的entry的提交。那麼此時所有的server中有過半的server都含有當前term(也是當前最大的term)的entry,假設serverA將來會成爲leader,此時serverA的lastlog的term必然是不大於當前term的,它要想成爲leader,即和其他server pk 誰的log最新,必然是需要滿足log的index比他們大的,所以必然含有已提交的entry。
4 其他注意點
4.1 client端
在client看來:
如果client發送一個請求,leader返回ok響應,那麼client認爲這次請求成功執行了,那麼這個請求就需要被真實的落地,不能丟。
如果leader沒有返回ok,那麼client可以認爲這次請求沒有成功執行,之後可以通過重試方式來繼續請求。
所以對leader來說:
一旦你給客戶端回覆OK的話,然後掛了,那麼這個請求對應的entry必須要保證被應用到狀態機,即需要別的leader來繼續完成這個應用到狀態機。
一旦leader在給客戶端答覆之前掛了,那麼這個請求對應的entry就不能被應用到狀態機了,如果被應用到狀態機就造成客戶端認爲執行失敗,但是服務器端缺持久化了這個請求結果,這就有點不一致了。
這個原則同消息隊列也是一致的。再來說說什麼叫消息隊列的消息丟失(很多人還沒真正搞明白這個問題):client向服務器端發送消息,服務器端回覆OK了,之後因爲服務器端自己的內部機制的原因導致該消息丟失了,這種情況才叫消息隊列的消息丟失。如果服務器端沒有給你回覆OK,那麼這種情況就不屬於消息隊列丟失消息的範疇。
再來看看raft是否能滿足這個原則:
leader在某個entry被過半複製了,認爲可以提交了,就應用到狀態機了,然後向客戶端回覆OK,之後leader掛了,是可以保證該entry在之後的leader中是存在的
leader在某個entry被過半複製了,然後就掛了,即沒有向客戶端回覆OK,raft的機制下,後來的leader是可能會包含該entry並提交的,或可能直接就覆蓋掉了該entry。如果是前者,則該entry是被應用到了狀態機中,那麼此時就出現一個問題:client沒有收到OK回覆,但是服務器端竟然可以成功保存了
爲了掩蓋這種情況,就需要在客戶端做一次手腳,即客戶端對那麼沒有回覆OK的都要進行重試,客戶端的請求都帶着一個唯一的請求id,重試的時候也是拿着之前的請求id去重試的
服務器端發現該請求id已經存在提交log中了,那麼直接回復OK,如果不在的話,那麼再執行一次該請求。
4.2 follower掛了
follower掛了,只要leader還滿足過半條件就,一切正常。他們掛了又恢復之後,leader是會不斷進行重試的,該follower仍然是能恢復正常的
follower在接收AppendEntries RPC的時候是冪等操作
4.3 集羣成員調整
目前這一塊,貌似raft paper的那個實現也被廢棄了,各個實現也都是各自的解決辦法,如參考下copycat的實現Membership Changes
最後就再說下raft的java版實現,可以看下copycat