Lab 4: Fault-tolerant Key/Value Service

INnoVation-V2發表於2024-09-27

Lab 4: Fault-tolerant Key/Value Service

一、介紹

本次Lab將使用Lab3的Raft庫構建容錯鍵/值儲存服務。您的KV服務將是一個複製狀態機,由多個Server組成,每個Server維護一個KV資料庫,就像Lab2一樣,使用Raft保證Server狀態一致。只要大多數伺服器處於活動狀態並且可以通訊,你的鍵/值服務就應該繼續處理客戶端請求,而不管是否有故障或網路分割槽。Lab4中將實現Raft 互動圖中的所有部分(Clerk、Service 和 Raft)。

客戶端將用和Lab2相同的方式與伺服器進行互動。三種RPC:

  • Put(key, value):替換資料庫中Key的值
  • Append(key, arg):將arg追加到到key對應的的值後面(如果key不存在,則將現有值視為空字串)
  • Get(key):獲取Key對應的Value(若Key不存在,返回空字串)

鍵和值都是字串,PutAppend不返回值。客戶端使用 Put/Append/Get方法,透過ClerkServer進行通訊。Clerk管理與伺服器的RPC互動

Server必須確保Get/Put/Append方法的呼叫是線性化的。如果每次呼叫都是順序進行的,Get/Put/Append方法應該表現得好像系統只有一個狀態副本,並且每次呼叫都應能觀察到前面一系列呼叫對狀態的修改。對於併發呼叫,返回值和最終狀態必須相同,就像操作按某種順序依次執行一樣。如果兩個呼叫在時間上重疊,則認為它們是併發的:例如,客戶端X呼叫Clerk.Put(),客戶端Y呼叫Clerk.Append(),然後客戶端X的呼叫返回。在一個呼叫開始時,它必須能觀察到所有在其之前完成的呼叫的效果。

對單個伺服器提供線性化相對容易。如果服務是複製的,就比較困難,因為所有伺服器必須使用相同的執行順序來執行併發請求,必須避免使用非最新狀態回覆客戶端,並且在故障恢復時保留所有已提交的資料。

本Lab分兩部分。

  • A: 使用Lab 3的Raft實現構建一個複製的鍵值服務,但不使用快照。
  • B: 使用Lab 3D中的快照實現,這將允許Raft丟棄舊的日誌條目。

建議複習Raft論文,特別是7,8節。

為了獲得更廣泛的視角,可以參考Chubby、Paxos Made Live、Spanner、Zookeeper、Harp、Viewstamped Replication 和 Bolosky 等人的研究。

入門

src/kvraft提供了基礎程式碼和相關測試。你需要修改kvraft/client.gokvraft/server.go,也許還有kvraft/common.go

執行以下命令啟動並執行。不要忘記使用git pull來獲取最新軟體。

$ cd ~/6.5840 
$ git pull 
... 
$ cd src/kvraft 
$ go test 
... 
$

Part A:無快照的KVServers(中等/困難)

你的每個鍵值伺服器都會有一個關聯的Raft節點。客戶端(Clerks)傳送Put()Append(),Get()請求到Raft Leader所在kvserver。kvserver將Put/Append/Get操作提交給Raft,Raft日誌將這些操作記錄下來。之後kvservers順序執行日誌中的操作,將操作應用到各自的鍵值資料庫;從而讓所有伺服器保持相同的鍵值資料庫副本。

有時Clerk不知道哪個kvserver是Raft Leader。如果Clerk將請求傳送到了錯誤的kvserver,或者無法聯絡到kvserver,Clerk應嘗試向其他kvserver傳送請求。如果鍵值服務將操作提交到了它的Raft日誌(並因此將該操作應用到鍵值狀態機),領導者將透過響應RPC報告結果給客戶端。如果操作未能提交(例如,Leader被替換),伺服器將報告錯誤,客戶端將嘗試向另一個伺服器傳送請求。

kvserver之間只能透過Raft進行互動。

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” 時,任務就完成了。

提示:

  • 呼叫Start()後,你的kvservers需要等待Raft完成一致性協商。已達成一致的命令會到達applyCh。你的程式碼需要不斷讀取applyCh。同時Put()Append()Get()處理函式使用Start()向Raft日誌提交命令。注意 kvserver與Raft庫之間的死鎖。

  • 如果kvserver不屬於多數派,它不應該完成Get()RPC(以避擴音供過時的資料)。一個簡單的解決方案是將Get()也記錄在Raft日誌中。不需要實現第8節中描述的只讀操作的最佳化。

  • 你不需要向Raft的ApplyMsg或Raft RPC(例如AppendEntries)新增任何欄位,但你可以這麼做。

  • 最好從一開始就新增鎖機制,因為避免死鎖的需求有時會影響整體程式碼設計。透過執行 go test -race 檢查你的程式碼是否是無競態的。

接下來修改你的程式碼,使其能夠在網路和伺服器故障的情況下正常執行。將面臨的問題是,Clerk可能需要多次傳送RPC請求,直到找到正常執行的kvserver。如果Raft Leader剛提交一條日誌到Raft Log後就失效了,Clerk可能無法收到回覆,因此需要重新傳送請求到另一個Leader。所有命令都只能執行一次,你需要確保伺服器不會重複執行重新傳送的請求。

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中重複檢測表裡儲存的資訊。

你的程式碼應該透過Lab 4A測試,像這樣

$ go test -run 4A
Test: one client (4A) ...
  ... Passed --  15.5  5  4576  903
Test: ops complete fast enough (4A) ...
  ... Passed --  15.7  3  3022    0
Test: many clients (4A) ...
  ... Passed --  15.9  5  5884 1160
Test: unreliable net, many clients (4A) ...
  ... Passed --  19.2  5  3083  441
Test: concurrent append to same key, unreliable (4A) ...
  ... Passed --   2.5  3   218   52
Test: progress in majority (4A) ...
  ... Passed --   1.7  5   103    2
Test: no progress in minority (4A) ...
  ... Passed --   1.0  5   102    3
Test: completion after heal (4A) ...
  ... Passed --   1.2  5    70    3
  
  
Test: partitions, one client (4A) ...
  ... Passed --  23.8  5  4501  765
Test: partitions, many clients (4A) ...
  ... Passed --  23.5  5  5692  974
Test: restarts, one client (4A) ...
  ... Passed --  22.2  5  4721  908
Test: restarts, many clients (4A) ...
  ... Passed --  22.5  5  5490 1033
Test: unreliable net, restarts, many clients (4A) ...
  ... Passed --  26.5  5  3532  474
Test: restarts, partitions, many clients (4A) ...
  ... Passed --  29.7  5  6122 1060
Test: unreliable net, restarts, partitions, many clients (4A) ...
  ... Passed --  32.9  5  2967  317
Test: unreliable net, restarts, partitions, random keys, many clients (4A) ...
  ... Passed --  35.0  7  8249  746
PASS
ok  	6.5840/kvraft	290.184s

Passed後面的數字含義:

實際執行時間(以秒為單位)、節點數量、傳送的RPC數量(包括客戶端的 RPC),以及執行的鍵/值運算元量(Clerk的Get/Put/Append呼叫)。

Part B: Key/value service with snapshots (hard)

目前的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秒的實際時間。

你的程式碼應該透過4B、4A、Lab 3測試

$ go test -run 4B
Test: InstallSnapshot RPC (4B) ...
  ... Passed --   4.0  3   289   63
Test: snapshot size is reasonable (4B) ...
  ... Passed --   2.6  3  2418  800
Test: ops complete fast enough (4B) ...
  ... Passed --   3.2  3  3025    0
Test: restarts, snapshots, one client (4B) ...
  ... Passed --  21.9  5 29266 5820
Test: restarts, snapshots, many clients (4B) ...
  ... Passed --  21.5  5 33115 6420
Test: unreliable net, snapshots, many clients (4B) ...
  ... Passed --  17.4  5  3233  482
Test: unreliable net, restarts, snapshots, many clients (4B) ...
  ... Passed --  22.7  5  3337  471
Test: unreliable net, restarts, partitions, snapshots, many clients (4B) ...
  ... Passed --  30.4  5  2725  274
Test: unreliable net, restarts, partitions, snapshots, random keys, many clients (4B) ...
  ... Passed --  37.7  7  8378  681
PASS
ok  	6.5840/kvraft	161.538s

相關文章