MIT6.824-Lab2A

Lab2A

Lab2A的地址:https://pdos.csail.mit.edu/6.824/labs/lab-raft.htmlhtml

Lab2A須要完成Raft協議中的Leader Election部分。
按照論文Figure2實現以下功能:網絡

  • 初始選舉
  • Candidate發佈RequestVote rpc
  • Leader發佈AppendEntry rpc, 包括心跳
  • server的狀態轉換(Follower, Candidate, Leader)

實驗提示:多線程

  • raft.go添加必須的狀態。
  • 定義log entry的結構。
  • 填充RequestVoteArgsRequestVOteReply結構。
  • 修改Make()以建立後臺goroutine, 必要時這個goroutine會發送RequestVoteRPC。
  • 實現RequestVote()RPC的handler。
  • 定義AppendEntriesRPC結構和它的handler。
  • 處理election timeout和"只投票一次"機制。
  • tester要求heartbeat發送速率不能超過10個/s, 因此要限制HB發送速率。
  • 即便在split vote的狀況下,也必須在5s內選出新的leader, 因此要設置恰當的Election timeout(不能按照論文中的election timeout設置爲150ms~300ms, 必須不大不小)。
  • 切記, 在Go中, 大寫字母開頭的函數和結構(方法和成員)才能被外部訪問!

測試

lab 2A有兩個測試:TestInitialElection()TestReElection(). 前者較爲簡單, 只須要完成初始選舉而且各節點就term達成一致就能夠經過. 後者的測試內容見Bug分析部分。函數

構思

通過不斷重構, 最後的程序結構以下:測試

  • MainBody(), 負責監聽來自rf.Controller的信號並轉換狀態.
  • Timer(), 計時器.
  • Voter(), 負責發送RequestVote RPC和計算選舉結果.
  • APHandler(), 負責發送心跳,而且統計結果.

四個經過channel來通訊(自定義整型信號).
先前的設計是MainBody() 監聽來自若干個channel的信號,後來瞭解到channel一發多收, 信號只能被一個gorouine接受, 可能會有出乎意料的後果.
因此修改MainBody主循環, 使得主循環只監聽來自rf.Controller的信號, 而後向其餘channel中發送信號(MPSC).ui

Bugs分析

2019-10-30

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使得在第三段時, leader1leader2(即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執行少許邏輯判斷後就進入睡眠阻塞,與計算密集絕不相干。

此外檢查執行環境, 確認所有核心都投入使用。

2019-11-08

筆者終於找到了問題的根源, 而且找到了解決辦法.
筆者在今天忽然意識到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過程給筆者最大的教訓是, 不要盲目信任別人的代碼和承諾

本站公眾號
   歡迎關注本站公眾號,獲取更多信息