系列文章java
1.1 剛開始全部server啓動都是follower狀態git
而後等待leader或者candidate的RPC請求、或者超時。github
上述3種狀況處理以下:算法
leader的AppendEntries RPC請求:更新term和leader信息,當前follower再從新重置到follower狀態數組
candidate的RequestVote RPC請求:爲candidate進行投票,若是candidate的term比本身的大,則當前follower再從新重置到follower狀態安全
超時:轉變爲candidate,開始發起選舉投票服務器
1.2 candidate收集投票的過程微信
candidate會爲這次狀態設置隨機超時時間,一旦出如今當前term中你們都沒有獲取過半投票即split votes,超時時間短的更容易得到過半投票。atom
candidate會向全部的server發送RequestVote RPC請求,請求參數見下面的官方圖.net
上面對參數都說明的很清楚了,咱們來重點說說圖中所說的這段話
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狀態,若是沒有而後超時的話,則開啓下一次的選舉。
一旦leader選舉成功,全部的client請求最終都會交給leader(若是client鏈接的是follower則follower轉發給leader)
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應用到各自的狀態機中了
對於上述leader選舉有個重點強調的地方就是
被選舉出來的leader必需要包含全部已經比提交的entries
如leader針對複製過半的entry提交了,可是某些follower可能尚未這些entry,當leader掛了,該follower若是被選舉成leader的時候,就可能會覆蓋掉了上述的entry了,形成不一致的問題,因此新選出來的leader必需要知足上述約束
目前對於上述約束的簡單實現就是:
只要當前server的log比半數server的log都新就能夠,這裏的新就是上述說的: 誰的lastLog的term越大誰越新,若是term相同,誰的lastLog的index越大誰越新
可是正是這個實現並不能徹底實現約束,纔會產生下面的另一個問題,一會會詳細案例來講明這個問題
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爲二、term爲2的entry提交了,此時s5也就不包含全部的commitLog了,可是s5按照log最新的比較方法仍是能當選leader,那就是說log最新的比較方法並不能保證3.1中的選舉約束即
被選舉出來的leader必需要包含全部已經比提交的entries
因此能夠理解爲:正是因爲上述選舉約束實現上的缺陷才致使又加了這麼一個不能直接提交以前term的entries的約束。
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。
在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,若是不在的話,那麼再執行一次該請求。
follower掛了,只要leader還知足過半條件就,一切正常。他們掛了又恢復以後,leader是會不斷進行重試的,該follower仍然是能恢復正常的
follower在接收AppendEntries RPC的時候是冪等操做
目前這一塊稍後再說
最後就再說下raft的java版實現,能夠看下copycat
#5 後續計劃
後續會再詳細分析下ZooKeeper中的ZAB協議實現,而後對比下raft,有哪些設計上的不一樣點。
歡迎關注微信公衆號:乒乓狂魔