分布式系統(tǒng)-實(shí)驗(yàn)-kvraft

介紹

這一次我們要在 實(shí)驗(yàn)二:Raft 的基礎(chǔ)上創(chuàng)建一個可以容錯的鍵值存儲服務(wù)。具體來說就是用幾個 Raft 實(shí)例組成一個可以容錯的狀態(tài)機(jī),而不同客戶端操作通過 Raft log 保證一致性。同時(shí)當(dāng)有半數(shù)以上的 Raft 實(shí)例正常運(yùn)行且能相互通信時(shí),我們的服務(wù)就能正確運(yùn)行。

在 kvraft 包中,我們要實(shí)現(xiàn) Key/Value 的服務(wù)器和客戶端,當(dāng)然服務(wù)器同時(shí)是一個 Raft 實(shí)例??蛻舳丝上蚍?wù)器發(fā)送 Put(), Append() 和 Get() RPC,服務(wù)器會將這些命令放到 log 中,然后按順序執(zhí)行??蛻舳丝梢韵蛉魏我粋€服務(wù)器發(fā)送請求,但是如果訪問的服務(wù)器不是 leader 或者沒有響應(yīng),則需要換一個服務(wù)器試試。如果一個客戶端指令確定寫入 log 中了,則需要給客戶端說一聲,要是此時(shí)服務(wù)器宕機(jī)或者網(wǎng)絡(luò)故障了,客戶端應(yīng)該將同樣的指令發(fā)給另外一個服務(wù)器。

我們的實(shí)驗(yàn)分兩步來做,第一步我們簡單的將每個請求的內(nèi)容追加到 log 列表中,而不用考慮他的空間占用。第二步,我們將實(shí)現(xiàn)論文中第7章的內(nèi)容,用 snapshot 回收無用的 log。

雖然這個實(shí)驗(yàn)不要寫很多代碼,但是可能得花很長時(shí)間去搞明白你的實(shí)現(xiàn)為啥不行。其實(shí)比實(shí)驗(yàn)二更難 debug,因?yàn)橛懈嗟哪K是異步的工作方式。

本實(shí)驗(yàn)的代碼應(yīng)在 src/kvraft.go 中實(shí)現(xiàn)。需要通過所有測試:

$ setup ggo_v1.5
$ cd ~/6.824
$ git pull
...
$ cd src/kvraft
$ GOPATH=~/6.824
$ export GOPATH
$ go test
...
$

Step 1: 初步 Key/Value 實(shí)現(xiàn)

首先明確一下我們的服務(wù)對外的接口:

  1. Put(key, value), 為 key 設(shè)置新的值 value
  2. Get(key), 獲取 key 的值
  3. Append(key, arg), 為 key 的值追加 arg

我們服務(wù)由幾個 Raft 實(shí)例組成一個備份狀態(tài)集群,客戶端應(yīng)該嘗試跟不同的服務(wù)器通訊。只要連上了一個 leader,并且此時(shí)有半數(shù)以上的機(jī)器正常運(yùn)行,則客戶端的命令應(yīng)該被執(zhí)行。

當(dāng)然我們先從最簡單的場景實(shí)現(xiàn),即沒有機(jī)器故障。但是我們得保證服務(wù)響應(yīng)的順序一致性。即客戶端的命令記錄在每個 Raft 實(shí)例上都按相同的順序執(zhí)行,且每個命令最多執(zhí)行一次。Get(key) 應(yīng)該讀取最新的數(shù)據(jù)。

先看看 Op struct 里面還缺少啥屬性,然后實(shí)現(xiàn) PutAppend() 和 Get() 的 handler,它們應(yīng)該調(diào)用 Raft 的 Start() 方法將 Op 放入 log 中,然后等待 Raft 向 applyCh 發(fā)送數(shù)據(jù),以表示剛才的命令執(zhí)行成功,在此之前不應(yīng)該再向 Start() 新的指令。收到成功消息后,向客戶端反饋。

完成之后需要能通過 "One client" 測試。

由于我們發(fā)送 Start() 和獲得結(jié)果是異步的,所以得考慮如何處理先后關(guān)系。

當(dāng)一個 leader 接收到 Start() 的命令,但是在 commit 之前失去了 leader 身份時(shí),客戶端應(yīng)該尋找新的 leader 重新發(fā)送這個命令。當(dāng)客戶端連上的 leader 被網(wǎng)絡(luò)隔離了,此時(shí)這個 leader 還是認(rèn)為自己是 leader,只是無法讓系統(tǒng)執(zhí)行命令,而且客戶端也不知道有新的 leader,這種情況下只能讓客戶端無限等待。

最好記住上次的 leader 的 index,這樣可以先試試它還是不是 leader,以減少尋找 leader 的時(shí)間。

因?yàn)槲覀兗僭O(shè)網(wǎng)絡(luò)和 Raft 實(shí)例都不是始終穩(wěn)定,所以我們需要處理一個請求發(fā)送多次的情況,保證同一請求只處理一次。所以我們需要一些額外數(shù)據(jù)去標(biāo)示不同的請求。

處理同一請求重發(fā)的情況: 一個客戶端在 term1 向 leader1 發(fā)送請求,然后等待響應(yīng)超時(shí),然后將這個請求發(fā)給了 leader2,此時(shí)在 term2??蛻舳说恼埱髴?yīng)該只被執(zhí)行一次。完成 Step 1 后,需要能通過 TestPersistPartitionUnreliable() 前面所有測試。

提到一個請求只能被服務(wù)器執(zhí)行一次,這取決于客戶端如何識別不同的請求,可以為每個請求加上標(biāo)示位,當(dāng)然也可以規(guī)定每個客戶端同時(shí)只能處理一個請求。

Step 2: 壓縮 log 減少冗余

為了讓 Raft 的 log 無限制增長,我們要用快照(snapshot,論文第7章)機(jī)制可以定期刪除無用的 log。

StartKVServer() 傳入的參數(shù) maxraftstate 用于表明每個 Raft 實(shí)例用于保存 log 的空間大小。每當(dāng)空間不足時(shí)需要生成一個 snapshot,然后告訴 Raft 當(dāng)前的 log 已經(jīng)被快照存入磁盤(persister.SaveSnapshot())了,可以將內(nèi)存的 log 扔掉了。

要實(shí)現(xiàn) snapshot 機(jī)制,我們需要修改 raft 包的代碼,當(dāng) follower 需要的同步的 log 已經(jīng)被快照存入磁盤了,則 leader 需要給 follower 發(fā)送一個 InstallSnapshot RPC,follower 接收到這種 RPC 后,需要向 kvraft 發(fā)送對應(yīng)的 snapshot。

snapshot 需要保存的不僅僅是 log 列表,還需要相關(guān)的信息比如請求序列號等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容