1. 程式人生 > 實用技巧 >Raft 論文筆記

Raft 論文筆記

Raft

一致性演算法。

整體結構

Raft 的作用是讓多臺主機保持一致。fault-tolerant virtual machine 論文中提到過兩種方法,一種是複製所有的狀態到別的主機上,包括 CPU,記憶體,IO 裝置。另一種方法是對主機進行狀態機建模,通過複製主機的日誌,執行相同的日誌內容來保持不同主機的狀態一致。Raft 使用的是第二種。

Raft 中的主機數量是固定的,每臺主機都知道其他主機的位置以便通訊。當啟動叢集的使用,Raft 首先進行的是選出一個 Leader,Leader 負責接收客戶端的請求,將請求寫入到日誌中,傳送給其他主機。當一條日誌為大多數主機所擁有的時候,Commit 一條日誌。當 Leader 寫入日誌的時候,並不執行日誌,當日志被 Committed 了之後,執行這條日誌。

Raft 分成五個部分:

  1. Leader Election
  2. Log Replication
  3. Cluster Membership changes
  4. Log Compaction
  5. Client Interaction

Raft 基礎

叢集中的每臺主機,屬於某一種狀態。一開始所有的主機是 follower 狀態,當選舉計時器超時,轉化為 candidate 狀態。當 candidate 得到了大多數主機的選票的時候,candidate 轉化為 leader 狀態。

raft 中引入了任期 (Term) 的概念。每個任期從選舉開始,選舉完成後,leader 執行其他操作,直到任期結束。在 Lab2 中,任期是無限長的,除非發生了故障,比如 followers 無法接收到 leader 的心跳包,此時 follower 會開始進行選舉,從而產生新的任期中的 leader。下圖中,t3 只有選舉,因為有可能發生這樣一種狀況,沒有一個 candidate 得到足夠的票數,成為 leader。candidate 的選舉計時器超時之後,開始新的任期,重新拉票。為了防止 t3 這種情況頻繁出現,Raft 使用隨機的選舉計時器。lab2 中,每次選舉計時器超時之後,重新在 400 ~ 700 毫秒之間隨機選擇一個數作為下一次的選舉計時。

演算法總結

在進行 Lab2 的時候,嚴格按照圖 2 的表述去實現即可。

Leader Election

每臺主機上設定一個選舉計時器,當這個計時器超時,發出拉票請求。當收到了大多數主機的投票,這臺主機宣稱自己是 Leader,並定時向其他主機發送心跳包。主機上的選舉計時器,在接收到合法的心跳包之後,會重新計時。主機給另一臺主機投票的時候,也會重置選舉計時器。

選舉的時候,需要選取出擁有全部已經提交的日誌的主機。因此當一臺主機接收到拉票請求的時候,它需要判斷是否給出選票。首先,判斷請求的主機任期是否比自己的新,如果是,那麼需要轉化為 follower 狀態,並且更新任期,重置選票。接下來,判斷請求的主機擁有的日誌是否比自己的更新 (more up-to-date)。更新的含義是,最後一個日誌的任期是否更新,如果請求的主機更新,那麼投票。如果最後一個日誌任期一致,判斷長度是否更長,請求的主機更長,那麼投票。

lab2 中的實現。

if args.Term >= rf.currentTerm {
    if args.Term > rf.currentTerm {
        rf.state = follower
        rf.currentTerm = args.Term
        rf.votedFor = -1
        ...
    }
    if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
        lastEntry := rf.log[len(rf.log)-1]

        // outdate case 1
        if lastEntry.Term > args.LastLogTerm {
            return
        }

        // outdate case 2
        if lastEntry.Term == args.LastLogTerm && lastEntry.Index > args.LastLogIndex {
            return
        }

        ...
    }
}

Log Replication

Log Replication 分為兩個部分:append 和 commit。

append,leader 強制將 follower 的日誌改為和自己的一致。不管 follower 中原來的日誌是什麼樣子的,leader 被選舉出來之後,就讓 follower 的日誌和自己的完全一致。leader 對每個 follower 維護一個 nextIndex 陣列,這個陣列的值,指向了要發給 follower 的下一個日誌條目 (log entry)。在傳送條目的 RPC 中,需要額外攜帶前一個日誌的 index、term 兩個資訊。這兩個資訊用來判斷,是否可以 append。如果 follower 在對應的 index 上沒有對應的 term,那麼拒絕 append,返回失敗。leader 接收到失敗,將對應的 nextIndex 減 1,重新發送。當 leader 接收到了成功,將對應的 nextIndex 加 1。實現的時候,在發心跳包的時候,檢查 nextIndex 是否比 log 的日誌大,如果大,那麼傳送多出來的日誌。

commit,當 leader 成功將一條日誌 append 到大多數主機上之後,這條日誌就可以 commit 了。commit 的意思是讓主機執行這條日誌。為了保證主機 commit 了之後,所有的主機都會 commit 同一條日誌,引入了一個約束,主機僅 commit 當前任期的日誌。下圖展示了一種情況,如果 leader 中某個日誌在大多數主機上 append 了之後,就提交,那麼這會導致一個 commit 被覆蓋的問題,導致了主機的不一致。a 中,S1 被選為 leader,將日誌複製到 S2,此時日誌條目 2 還沒有被提交。b 中,S1 離線了,S5 選舉計時器超時並開始拉票,成功拉到了 S3、S4,成為了 leader,於是接收請求,在日誌中寫入條目 3。c 中,S5 斷線了,S1 上線並選為了 leader。此時將條目 2 複製到 S3,條目 2 在大多數主機上了,於是提交了這個日誌,在 S1、S2、S3 上執行了這個條目。d 中,S1 故障,S5 重新被選為了 leader,因為它擁有的條目比 S2、S3、S4 新,所以可以被選為 leader。當它選為了 leader 之後,強制其他主機和自己的日誌一致,於是覆蓋了其他主機,甚至覆蓋了已經執行的條目 2。因此錯誤就發生了。

為了避免這種錯誤,引入約束:主機僅 commit 當前任期的日誌,不 commit 大多數但非當前任期的日誌。commit 的方式是,設定一個 commitIndex,這樣可以間接 commit 前面的日誌。

Cluster Membership changes

叢集的配置是固定的,如果叢集要增加主機或者移除主機該如何做呢?下圖的方法展示了直接將更新主機的配置。這種方法的問題是,不可能一次性更新全部主機的配置,這樣會導致部分主機先更新為新配置,從而有可能使到同一個任期,兩個 leader。

可以有兩種方法解決這個問題,一種是關掉叢集,更新配置後重新啟動叢集。這樣的話,在關閉期間是不可用的。另一種方法是,使用兩階段的方法 (two-phase approach)。第一階段,讓主機同時擁有兩種配置。第二階段,讓主機轉到新的配置上。Raft 中使用一個特殊的 log entry 來讓主機進行狀態轉化,當主機要轉換狀態的時候,在 log 中加入一個條目,leader 將這個條目複製到其他主機上。當主機見到這個條目的時候,就進行轉換,無需等到提交。

兩階段方法,log replication 會在兩種配置中都進行,不管一個主機是否出現在新配置中,都要進行 log replication。leader 可以在新舊配置中產生,不管一個 leader 是否最終會在新配置中。達成一致需要在新舊配置中都達到大多數的狀態。

在更改配置的過程中會出現三個問題:

  1. 新加入的主機需要時間來複制 log。可以考慮額外的階段,加入叢集中,不影響任何決定,但是要 leader 發 log 給他們。
  2. leader 不是最終配置的一部分,step down。
  3. 新配置中移除的主機,它的選舉計時器會超時。解決辦法是,給加一個下界,在這個範圍內接收到的拉票請求,直接忽略。

Log Compaction

為了防止日誌無限制增長,可以使用快照,將當前系統的狀態儲存下來,丟棄快照之前的所有日誌。每個主機獨立地進行快照,我覺得這個應該是有限制的,比如對於 commit 了的日誌,才能進行快照,否則快照下來的日誌後續需要被覆蓋,但是又被丟棄了,找不到那個條目了。如果一個 follower 落後太多了,並且 leader 快照後,將日誌丟棄了,那麼這個落後的 follower 該如何趕上呢?方法是,使用一個 RPC,leader 快照後,可以檢查 follower 的 nextIndex,如果發現落後太多,那麼可以呼叫 InstallSnapshot 來讓 follower 安裝快照,重做日誌內容。

Client Interaction

找到 leader:client 隨機發送請求給一個主機,如果主機不是 leader,那麼傳送足夠的資訊給 client,讓它可以找到 leader。如果主機崩潰了,那麼重新這個過程。

同一請求可能被多次執行:一個 leader 在 commit 一條日誌後,在回覆 client 之前,崩潰了。那麼 client 會重新請求,導致了同一請求多次執行。解決辦法是加一個序列號,如果 leader 接收到了一個已經執行了的請求,那麼立即返回,可以根據序列號判斷是否執行。

讀取過時資料:只讀操作可以不寫日誌。這樣可能導致,在一箇舊的 leader 上,它不知道已經有新的 leader,然後讀取操作返回了過時資料。使用心跳包來確定是否還是 leader。(exchange heartbeat messages with a majority of the cluster)。文字中有這樣一段:

a leader must have the lastest information on which entries are committed.

這有點奇怪,因為按照圖 2 去實現,leader 應該直到這個資訊的。可能這裡是沒有這個資訊的,所以需要提交一個 no-op 來確定提交的最新日誌。