2B中最主要的任務就是進行紀錄檔的複製。Raft是一個強領導人的系統,這意味著所有的紀錄檔新增都是由領導人發起的,與之相類似的,還有很多其他的結論(它們都是比較顯然的),讀者可以自行證明。
我們可以這樣地分解複製紀錄檔的過程
我們首先需要完善Raft結構體的內容。
我們對新加入的內容進行解釋:
commitIndex
:這代表了每一個Raft中最高的已經提交的紀錄檔下標,它表示對於這個下標以內的所有紀錄檔,我們都已經和領導達成共識。可以想象,它的更新是在收到RPC的時候完成的,因為如果領導人提交了某一條指令,且某個追隨者有這條指令,那麼就可以認為是達成共識的了,後續可以進行應用等操作。
lastApplied
:這是一個緊跟commitIndex
的成員,他指代的是每一個Raft中已經應用到狀態機中的紀錄檔下標的最大值。我們會對每個Raft有一個輪詢(至少我是這麼實現的),如果lastApplied<commitIndex
,那麼就把這個區間內的所有指令應用到複製狀態機上。
nextIndex
(對於領導人):這代表的是對於每個成員,下一組要發的指令的下標的最小值,可以想象它代表著領導人和每個成員在哪些紀錄檔上達成了一致,它的更新應該是在AppenEntriesRPC
中實現的。
matchIndex
(對於領導人):這代表的是,對於每個成員,和領導人達成一致的下標的最大值(當然,你可以期待一般matchIndex=nextIndex-1
)。我們通過它來決定要提交到哪一條指令(也即大部分人matchIndex
的下界)。
經過上層伺服器的多次嘗試,我們終於在領導人這裡加了一條紀錄檔:我們需要完成Start()
函數,它的引數為一條指令(一個空的interface),返回的是這條指令在領導人這邊的紀錄檔編號,領導人的任期,和這個人是否是領導人。同時,在Start函數中我們也要顯式地向每一個追隨者發一條心跳(其實就是AppendEntriesRpc
).
類似於MakeElection
函數,我們需要完成launchAppendEnries()
這一函數,並處理回覆資訊。
我們需要完成AppendEntries()
函數,在裡面完成RPCHandler
的功能。
我們需要完成上文中提到的輪詢,對每一個Raft開一個Go程來檢查要不要提交某一些指令,如果是的話,那就加到applyCh
內(這個applyCh
也需要自己完成)。
在Start
中,我們需要:
增加log
改變lastLogIndex, lastLogTerm
開Go程來傳送資訊:go rf.launchAppendEntries(peerId)
我們首先需要傳送AppendEntriesArgs
,然後需要考慮AppendEntriesReply
,具體來說:
需要遵循Rules For All Servers,如果自己的任期變大,不需要處理;如果Reply的任期更高,變成追隨者......(建議參考raft-extended)
如果回覆顯示成功,那麼把nextIndex, matchIndex
都增加,並考察commitIndex
是否也可以增加(看看是不是有一半的matchIndex
)都超過了某一個值
如果回覆顯示失敗,我們就把nextIndex[peerId]
減少一,來向下匹配。(在後文中我們會看到對於這一點的明顯優化)
這是2B中的核心內容,我們在這裡考察論文中的圖示:
這個比較簡單,只要知道「通道」是啥基本就可以了。
在上面這張圖中,Reply false的意思是直接返回。
這裡第三條的conflicts with的實現需要細細思考。如果接收者的紀錄檔中的每一條都是正確的,那麼不需要截斷(我到2C才發現這個)
對於Rules For All Server而言,如果變成Follower,不需要返回。這一點上的處理是和2A一致的。