MIT 6.824 Lab3 RaftKV
實驗內容
在lab2的Raft函式庫之上,搭建一個能夠容錯的key/value儲存服務,需要提供一致性保證。
強一致性的解釋如下:對於單個請求,整個服務需要表現得像個單機服務,並且對狀態機的修改基於之前所有的請求。對於併發的請求,返回的值和最終的狀態必須相同,就好像所有請求都是序列的一樣。即使有些請求發生在了同一時間,那麼也應當一個一個響應。此外,在一個請求被執行之前,這之前的請求都必須已經被完成(在技術上我們也叫著線性化(linearizability))。
整體架構
Part A - 不需要日誌壓縮的key/value服務
Clerk客戶端實現
需要解決的問題:客戶端傳送請求,服務端同步成功並且提交,接著apply後返回給客戶端執行結果,但返回客戶端時候rpc丟失,客戶端只能進行重試直到明確地寫入成功或失敗為止,但該操作可能已經在服務端應用過了,違背了線性一致性。
clientId和commandId來唯一的標識一個客戶端,從而保證線性一致性。RAFT原文介紹需要保證日誌僅被執行一次,即它可以被 commit 多次,但一定只能 apply 一次。
可以看一下:
連結:https://www.zhihu.com/question/278551592/answer/400962941
在Raft論文的6.3節,這個問題有詳細討論。
用普通後臺術語就是冪等。Raft作者把這歸為實現linearizable semantics所需要處理的一部分。Raft論文裡,也給出了具體的通用解決辦法。基本思路是:
- 每個要做proposal的client需要一個唯一的identifier,它的每個不同
proposal需要有一個順序遞增的序列號,client id和這個序列號由此可以唯一確定一個不同的proposal,從而使得各個raft節點可以記錄儲存各proposal應用以後的結果。- 當一個proposal超時,client不提高proposal的序列號,使用原proposal序列號重試。
- 當一個proposal被成功提交併應用且被成功回覆給client以後,client順序提高proposal的序列號,並記錄下收到的成功回覆的proposal的序列號。raft節點收到一個proposal請求以後,得到請求中夾帶的這個最大成功回覆的proposal的序列號,它和它之前所有的應用結果都可以刪去。proposal序列號和client id可用於判斷這個proposal是否應用過,如果已經應用過,則不再再次應用,直接返回已儲存的結果。等於是每個不同的proposal可以被commit多次,在log中出現多次,但永遠只會被apply一次。
- 系統維護一定數量允許的client數量,比如可以用LRU策略淘汰。請求過來了,而client已經被LRU淘汰掉了,則讓client直接fail掉。
- 這些已經註冊的client資訊,包括和這些client配套的上述proposal結果、各序列號等等,需要在raft組內一致的維護。也就是說,上述各raft端資料結構和它們的操作實際是state machine的一部分。在做snapshotting的時候,它們同樣需要被儲存與恢復。
可能感覺讓這樣重試的request被commit多次有奇怪。其實不奇怪,實際操作中,它們對狀態機而言是個NO-OP。原文中的原話也清楚列明這些log entry會在raft log中重複出現,由狀態機來負責過濾掉,狀態機能看到接觸到的自然是commit以後的log entry。
The Raft log provides a serial order in which commands are applied on every server. Commands take effect instantaneously and exactly once according to their first appearance in the Raft log, since any subsequent appearances are filtered out by the state machines as described above.
不是所有的應用都需要這樣的功能。最直接的例子就是membership change本身。membership change的時候,比如有一個node的id是XYZ,因為超時你試圖去再次提交一個membership remove操作,再次去刪除這個id為XYZ的節點,它並不帶來實際損害(很多raft庫不允許一個已經被刪除的節點再次以相同node id加入回來)。
還需要注意的是:請求服務端超時或請求的服務端不為主節點時,能嘗試連線其他服務端。