Lab2A的地址:https://pdos.csail.mit.edu/6.824/labs/lab-raft.htmlhtml
Lab2A須要完成Raft協議中的Leader Election
部分。
按照論文Figure2實現以下功能:網絡
RequestVote rpc
AppendEntry rpc
, 包括心跳實驗提示:多線程
raft.go
添加必須的狀態。log entry
的結構。RequestVoteArgs
和RequestVOteReply
結構。Make()
以建立後臺goroutine, 必要時這個goroutine會發送RequestVote
RPC。RequestVote()
RPC的handler。AppendEntries
RPC結構和它的handler。tester
要求heartbeat發送速率不能超過10個/s, 因此要限制HB發送速率。split vote
的狀況下,也必須在5s內選出新的leader, 因此要設置恰當的Election timeout
(不能按照論文中的election timeout設置爲150ms~300ms, 必須不大不小)。lab 2A有兩個測試:TestInitialElection()
和TestReElection()
. 前者較爲簡單, 只須要完成初始選舉而且各節點就term達成一致就能夠經過. 後者的測試內容見Bug分析部分。函數
通過不斷重構, 最後的程序結構以下:測試
rf.Controller
的信號並轉換狀態.RequestVote RPC
和計算選舉結果.四個經過channel來通訊(自定義整型信號).
先前的設計是MainBody()
監聽來自若干個channel的信號,後來瞭解到channel一發多收, 信號只能被一個gorouine接受, 可能會有出乎意料的後果.
因此修改MainBody主循環, 使得主循環只監聽來自rf.Controller
的信號, 而後向其餘channel中發送信號(MPSC).ui
TestInitElection()
能成功經過, 可是TestReElection()
偶爾會失敗, 因而筆者
給TestReElection()
添加一些輸出語句, 將其分爲多個階段, 方便debug。每一個階段,測試函數都會檢查該階段內是否不存在leader, 或者僅有一個leader。.net
選出leader1 # 1 ------------------------------ leader1 下線 選出新leader # 2 ------------------------------ leader1 上線, 變爲follower # 3 ------------------------------ leader2 和 (leader2 + 1 ) % 3 下線 等待2s, 沒有新leader產生 # 4 ------------------------------ (leader2 + 1 ) % 3 產生 選出新leader # 5 ------------------------------ leader2 上線 # 6 ------------------------------
這裏說明一下, 測試函數TestReElection()
使用disconnect(s)
把節點s從網絡中下線。節點自己沒有crash, 仍然正常工做。使用connect(s)
將會恢復節點與網絡的通訊。線程
測試在第3-4階段時偶爾會失敗. 分析發現: 此時系統中不該該存在leader, 可是仍有某個節點聲稱本身是leader.debug
筆者進行屢次測試發現以下規律:設計
test leader1 leader2 (leader2 + 1) % 3 success 0 2 0 failed 0 1 2 success 2 1 2 success 1 0 1 failed 0 1 2 ..
只有當 (leader2 + 1) % 3 == leader1
時才測試成功。爲何呢?
在3-4階段時,節點leader2
與(leader2 + 1)%3
都被下線。
條件l1 == (l2 + 1) % 3
使得在第三段時, leader1
和leader2
(即1-3階段產生的兩個leader)都下線, 因此此時網絡中沒有leader了.
若是不知足這個條件, 那麼惟一一個在網絡中的節點就是leader1
了。leader1
遲遲不變爲Follower, 會致使測試失敗.
經過分析日誌發現: leader1
從新加入網絡時, 沒有收到leader2
的心跳(若是收到,會使leader1變爲follower), 本身也沒有發送心跳, 也沒有收到candidate
的RequestVote RPC. 看起來是被阻塞了。
因而筆者修改TestReElection()
函數, 在2-3和4-5階段讓測試函數睡眠若干個election timeout:
fmt.Println("2-------------- ") ... time.Sleep(time.Duration(2 * ElectionTimeout) * time.Millsecond) // ... fmt.Println("3-------------- ") // ... time.Sleep(time.Duration(2 * ElectionTimeout) * time.Millsecond)
筆者發現, 通過更長時間(12s), old leader最終都得知了存在更大的Term從而變爲follower. 在修改後的測試中, 每次測試均正常經過.
筆者在此階段作的工做有:
筆者發現問題不是在死鎖,因而認爲問題是線程飢餓所致。、
查閱資料發現, 有人提到當某些goroutine是計算密集的, 會致使飢餓。但是筆者的實現中,goroutine執行少許邏輯判斷後就進入睡眠阻塞,與計算密集絕不相干。
此外檢查執行環境, 確認所有核心都投入使用。
筆者終於找到了問題的根源, 而且找到了解決辦法.
筆者在今天忽然意識到rpc調用自己的問題:
rf.peers[server_index].Call("Raft.AppendEntry", &args, &replys[server_index])
此處的Call()
, 是實驗代碼中自帶的、用來模擬執行RPC的函數, 若是RPC正常返回, 則該函數返回true
, 若是超時則返回false
.
筆者在Lab1
中也用過相似的函數, 因此並無懷疑此函數.
然而筆者忽然意識到,該函數的代碼註釋並無說明等待多久纔算超時!
極可能Call的超時是致使測試失敗的關鍵. 因而筆者翻閱源碼,發現Call
這個函數自身並無定時器,它依賴於Endpoint
(模擬端點設備的結構)中的channel!也就是說channel什麼時候返回結果,它就何時返回。
筆者繼續翻看代碼,忽然一個7000
一閃而過,筆者定晴一看,不由背後一涼,心中大驚:
// src/raft/labrpc/labrpc.go func (rn *Network) ProcessReq(req reqMsg) { enabled, servername, server, reliable, longreordering := rn.ReadEndnameInfo(req.endname) if enabled && servername != nil && server != nil { //... 這裏也是設置超時參數, 非重點 } else { //... if rn.longDelays { ms = (rand.Int() % 7000) // <--------------------- Lab2A下, 最多會等待7s! } else { ms = (rand.Int() % 100) } // 等待一段時間而後向channel發送信息。 time.AfterFunc(time.Duration(ms)*time.Millisecond, func() { req.replyCh <- replyMsg{false, nil} }) } }
爲何Lab2A
下的超時設置是7000ms呢? 會不會跳到其餘分支?不會!
首先測試開始時會配置測試環境, 其中就有這樣一句:
// ...config.go: line: 84 cfg.net.LongDelays(true)
這裏設置了長延時參數。
其次, 在測試函數TestReElection()
中, .disconnect(s)
會下線節點s
, 使得s
的網絡請求老是超時. 在代碼層面, 就是把enabled
這個布爾值設爲false
。
綜合上面兩點, .disconnect(s)
後s
的超時設置將跳轉到LongDelays
這個分支, 從而rpc等待時間最高會達到7000ms, 這將會致使old leader長時間阻塞在rpc調用上, 不能及時處理到來的RPC, 也就沒辦法及時變爲follower, 最終致使測試失敗。
那該怎麼辦? 知道了緣由就好辦了: 在APHandler處設置超時監控, 超時或者rpc按時返回則中止等待.
修復以後,再也沒見過test fail了, 真開心。
筆者雖然按照論文來實現,但在實現過程當中,屢屢遺漏某些細節,不得不花大量時間測試(好比rpc中攜帶比本身還大的Term,自身就要馬上變爲follower)。
此外,對於多線程+定時器這樣的場景,基本不能用斷點調試的辦法。打日誌,分析日誌幾乎是惟一路徑。分析日誌也挺考驗思惟邏輯的。
這個Debug過程給筆者最大的教訓是, 不要盲目信任別人的代碼和承諾。