Lab4 記錄

INnoVation-V2發表於2024-09-27

Part A:無快照的KVServers

KVServer整體結構如下

image-20240921171228925

每個KvServer對應一個Raft Server,該Raft Server可能是Leader或Follower

  1. Client向KVServer傳送請求,如果該KVServer對應的Raft Server不是Leader,直接返回Error,Clerk向其他KVServer發起請求
  2. KVServer將命令提交到Raft Leader,如果執行完成,Raft Server會透過applyChan將命令傳送回KVServer
  3. KVServer將接收到的命令執行到本地狀態機
  4. 返回結果到Clerk

第一步

Task

第一個任務是實現一個沒有訊息丟失和伺服器失敗情況下的解決方案。

可以將Lab 2中的客戶端程式碼(kvsrv/client.go)複製到kvraft/client.go 中。你需要新增邏輯,以決定每個請求應該傳送到哪個kvserver。記住,Append()不再返回值。

繼續在server.go中實現Put()、Append()和Get()的處理函式。這些處理函式應該使用Start()將一個操作(Op)加入Raft日誌中;你需要在server.go中填充 Op結構體的定義,使其能夠描述Put/Append/Get操作。每個Server應在Raft 提交操作時(即操作出現在applyCh上時)執行Op命令。RPC處理函式應該注意到 Raft何時提交了它的Op,然後回覆該RPC請求。

當你透過第一個測試 “One client” 時,任務就完成了。

KvRaft中,Client透過Clerk提交Command,

每個KvServer對應一個Raft Server

Clerk將命令提交給KvServer

  1. 如果這個KvServer對應的Raft Server不是Leader

    返回false,並返回LeaderID

    Client根據LeaderID重新發起

  2. KvServer提交Command到Raft Leader

  3. 一旦命令執行完成,Raft會透過ApplyChan將commit的命令傳送過來

    KvServer根據發過來的命令判斷哪個指令執行完成,向對應的呼叫返回結果

第二步

Task

新增程式碼以處理故障和重複的Clerk請求,包括這樣的場景:Clerk在某個任期內向kvserver的Leader傳送請求,因等待回覆超時,在新任期內又將請求傳送給了新的Leader。這個請求只能被執行一次。這裡的說明文件提供了有關重複檢測的指導。你的程式碼需要透過go test -run 4A測試。

提示:

  • 你需要處理這樣的情況:一個Leader呼叫了Start()處理Clerk的RPC請求,但在該請求提交到日誌之前失去了領導權。這種情況下,你應該讓Clerk重新傳送請求到其他伺服器,直到找到新的Leader。可以透過以下方式實現:kvserver檢測Leader是否失去領導權,比如發現Raft的任期發生了變化,或者在Start()返回的索引處出現了不同的請求。如果原來的Leader被網路分割槽隔離,它可能不知道新的Leader出現了;但同樣處於該分割槽內的客戶端也無法聯絡到新Leader,因此在這種情況下,允許伺服器和客戶端無限期等待,直到分割槽恢復。
  • 你可能需要修改Clerk以記錄上一次RPC請求中找到的Leader,並在下一次RPC 時首先將請求傳送給該伺服器。避免浪費時間去尋找Leader,從而更快地透過某些測試。
  • 你應該使用類似於Lab 2的重複請求檢測機制。該機制能夠快速釋放伺服器記憶體,例如透過每個新的RPC可以預設表示客戶端已經接收了上一次RPC的回覆。你可以假設每個客戶端一次只會向Clerk發起一個呼叫。你可能需要根據實際情況,修改 Lab 2中重複檢測表裡儲存的資訊。
  1. Leader失效處理

    所有命令,一旦呼叫了Client,Client必須不斷重試直到執行成功,不能執行失敗

    Client

    1. 傳送Log到KvServer
    2. 如果超時沒有回覆,則可能是Server失效,切換Server重試

    KvServer

    1. 接收Client的Request
    2. 提交Log到Raft,儲存返回的Index,Term,以及Log到Slice中
    3. Leader失效:
      1. 透過rf.getState()獲取Raft狀態,如果Term發生改變,或者已經不是Leader,就返回ErrWrongLeader
      2. 如果返回的CommitLog Msg對應的Index和本地儲存的Log不同,同樣返回ErrWrongLeader,讓Client重試
    4. 收到Raft發回的CommitLog Msg,把對應的Log從Slice中去除
  2. 重複請求處理

    和Lab2一樣的處理思路

  3. 處理分割槽partition

    如果KvServer提交了命令到Raft,而且該Raft是Leader,如果一直不返回,那就持續等待

坑點

  1. 這一節有些問題是因為Raft的問題導致的,最好能透過Raft測試100遍,差不多就沒有大問題了。

  2. 同一Index位出現了不同的Log,以及Term發生改變,上述兩種情況發生時都需要重新提交Log

  3. 但對於Term改變,重新提交Log可能會出現重複日誌的情況,

    比如分割槽的情況,一共五個Raft Server,其中0,1,2一組,3,4一組

    0是Leader,日誌來臨時,0將日誌複製給了2

    之後3回到叢集,2被斷開,因為3,4一直在進行選舉但又不可能選舉成功(Server數小於一半),因此必然Term很大,0給3傳送心跳訊息,就會發現Term大於自身,因為0轉換Follower,準備重新選舉,這是我們的KvServer也發現Term發生了變化,於是重新提交Log

    之後3又被斷開,2回來並贏得選舉,這時新Log提交給了2,2成功將新的日誌項與前一個日誌項複製到所有Server,於是就發生了重複日誌

重複日誌的處理方法

  1. 記住Client一次只會傳送一個Request到KvServer,一個Request沒有成功以前,不會傳送新的Request
  2. 因為網路延遲導致的重複請求,只會傳送給同一個Server
  3. 當Client收到結果後,不會再出現重複請求

結果

程式碼地址:https://github.com/INnoVationv2/6.5840/tree/Lab4/PartA

image-20240924233327075

Part B: 使用快照的KV service

目前的KVServer不會呼叫Raft的Snapshot(),因此重新啟動的伺服器必須重播完整的Raft日誌才能恢復其狀態。現在,使用Lab 3D中的Raft的Snapshot()方法,修改kvserver與Raft配合使用,以節省日誌空間並減少重新啟動時間。

Tester將maxraftstate傳遞給StartKVServer()。maxraftstate表示持久 Raft狀態的最大允許大小(單位為位元組,包括日誌,但不包括快照)。你應該將 maxraftstate與persister.RaftStateSize()進行比較每當你的KVServer檢測到Raft狀態大小接近此閾值時,應透過呼叫Raft的Snapshot來儲存快照。如果maxraftstate為-1,則不必快照。maxraftstate適用於你的Raft作為persister.Save()的第一個引數傳遞的GOB編碼位元組。

修改你的kvserver,使其能夠檢測持久化的Raft狀態是否過大,並將快照交給Raft。當kvserver重新啟動時,它應該從persister讀取快照,並從快照中恢復其狀態。

提示

  • 思考一下kvserver何時應該對其狀態進行快照,以及快照中應包含哪些內容。Raft使用Save()將每個快照以及相應的Raft狀態儲存在persister中。你可以使用ReadSnapshot()讀取最新儲存的快照。
  • kvserver必須能夠在跨越檢查點時檢測到日誌中的重複操作,因此用於檢測這些操作的任何狀態都必須包含在快照中。
  • 將快照中儲存的結構的所有欄位必須以大寫字母開頭
  • 您的Raft庫中可能存在本實驗中暴露的錯誤。如果您對 Raft 實現進行了更改,請確保它繼續透過所有實驗 3 測試。
  • Lab 4 測試的合理時間是400秒實際時間和700秒CPU時間。此外, go test -run TestSnapshotSize應花費少於20秒的實際時間。

思路

  1. Raft中儲存的什麼?

    是所有日誌序列

    Raft Server重啟後,重播所有Log到KvServer,KVServer接收到日誌後,一個一個將日誌應用到本地db,這樣狀態就恢復如初了

  2. 快照儲存的什麼?

    快照儲存的是,某個Commit Index之前所有日誌的最終結果,即KVServer中的KV狀態

  3. 如何進行快照?

    1. 每次接收到Raft Server發來的log時,將persister.RaftStateSize()maxraftstate進行比較,當大小接近時,呼叫Snapshot
    2. 將KVServer的當前狀態透過Snapshot傳送給Raft Server
  4. 傳給Snapshot()的KVServer當前狀態需要哪些欄位?

    1. 當前的KV狀態
    2. LastLogIndex
    3. 防止重複請求的相關欄位
      1. prevCmd
      2. submitCmd
      3. history
      4. matchIndex

Leader:0

其他4個Follower:1,2,3,4

  1. 來了一個新日誌
  2. Leader傳送給了1 2 3,傳送完成後達成共識,回覆Client已經執行完成
  3. Client收到後,應用日誌到自身狀態機,並發現狀態大小已達到MaxRaftState
  4. 呼叫Leader生成了snapshot
  5. Leader接下來傳送日誌給4,因為已經生成snapshot,於是傳送snapshot給0
  6. 此時4剛好收到Raft發來的前一個日誌,並且Raft Server4同樣到達MaxRaftState,於是生成snapshot
  7. 4收到Leader發來的snapshot,儲存下來,並嘗試傳送給KVServer
  8. 但此時,KVServer卡在生成snapshot的lock之前

如果前面Raft實現沒有問題,PartB很簡單,但是很多時候生成Snapshot,接收Snaoshot之間會死鎖,可以不斷列印協程數量,如果發現協程數量急劇攀升,八成就是死鎖了。

這種情況Debug也比較麻煩,建議:檢視Log,如果發現某個Server在某個時間點後再也沒有列印任何日誌,那應該就是這個Server出現了死鎖,詳細看看這個Server最近做了哪些操作,然後仔細分析。

結果

程式碼地址:https://github.com/INnoVationv2/6.5840/tree/Lab4/PartB

image-20240926180910122

全部測試結果

go test -failfast
Test: one client (4A) ...
  ... Passed --  15.1  5 35348 5889
Test: ops complete fast enough (4A) ...
  ... Passed --   1.1  3  4015    0
Test: many clients (4A) ...
  ... Passed --  15.1  5 19773 3676
Test: unreliable net, many clients (4A) ...
  ... Passed --  19.2  5  4770  566
Test: concurrent append to same key, unreliable (4A) ...
  ... Passed --   3.9  3   314   52
Test: progress in majority (4A) ...
  ... Passed --   0.6  5    57    2
Test: no progress in minority (4A) ...
  ... Passed --   1.1  5   129    3
Test: completion after heal (4A) ...
  ... Passed --   1.0  5    56    3
Test: partitions, one client (4A) ...
  ... Passed --  22.5  5 45475 3300
Test: partitions, many clients (4A) ...
  ... Passed --  22.7  5 18715 1760
Test: restarts, one client (4A) ...
  ... Passed --  18.9  5 27998 4693
Test: restarts, many clients (4A) ...
  ... Passed --  19.0  5 10847 2042
Test: unreliable net, restarts, many clients (4A) ...
  ... Passed --  22.7  5  4372  480
Test: restarts, partitions, many clients (4A) ...
  ... Passed --  25.6  5 14712 1506
Test: unreliable net, restarts, partitions, many clients (4A) ...
  ... Passed --  27.6  5  5485  326
Test: unreliable net, restarts, partitions, random keys, many clients (4A) ...
  ... Passed --  32.3  7 13263  672
Test: InstallSnapshot RPC (4B) ...
  ... Passed --   2.5  3   950   63
Test: snapshot size is reasonable (4B) ...
  ... Passed --   0.7  3  3207  800
Test: ops complete fast enough (4B) ...
  ... Passed --   0.7  3  4015    0
Test: restarts, snapshots, one client (4B) ...
  ... Passed --  19.4  5 134165 23573
Test: restarts, snapshots, many clients (4B) ...
  ... Passed --  19.7  5  5871  892
Test: unreliable net, snapshots, many clients (4B) ...
  ... Passed --  17.4  5  3875  468
Test: unreliable net, restarts, snapshots, many clients (4B) ...
  ... Passed --  22.3  5  4154  467
Test: unreliable net, restarts, partitions, snapshots, many clients (4B) ...
  ... Passed --  28.8  5  4461  259
Test: unreliable net, restarts, partitions, snapshots, random keys, many clients (4B) ...
  ... Passed --  35.0  7 11036  447
PASS
ok  	6.5840/kvraft	395.967s