首頁 > 後端開發 > Golang > 了解 etcd 的 Raft 實作:深入研究 Raft 日誌

了解 etcd 的 Raft 實作:深入研究 Raft 日誌

Mary-Kate Olsen
發布: 2024-11-23 06:14:17
原創
191 人瀏覽過

介紹

本文將從Raft共識演算法中的日誌開始,並介紹分析etcd的Raft中Raft Log模組的設計與實作。目的是幫助讀者更好地理解etcd的Raft實現,並為實現類似場景提供一個可能的方法。

筏日誌概述

Raft 共識演算法本質上是一個複製狀態機,其目標是在伺服器叢集中以相同的方式複製一系列日誌。這些日誌使叢集中的伺服器能夠達到一致的狀態。

在這種情況下,日誌指的是Raft Log。叢集中的每個節點都有自己的Raft Log,Raft Log由一系列日誌條目組成。日誌條目通常包含三個欄位:

  • 索引:日誌條目的索引
  • 任期:建立日誌條目時領導者的任期
  • 資料:日誌條目中包含的數據,可以是特定的命令等

要注意的是,Raft Log 的索引從 1 開始,只有 Leader 節點才能建立 Raft Log 並將其複製到 Follower 節點。

當日誌條目持久儲存在叢集中的大多數節點(例如 2/3、3/5、4/7)上時,它被視為已提交

當日誌條目應用於狀態機時,它被視為已套用

Understanding etcd

etcd 的 raft 實作概述

etcd raft 是一個用 Go 編寫的 Raft 演算法庫,廣泛應用於 etcd、Kubernetes、CockroachDB 等系統。

etcd raft 的首要特點是它只實現了 Raft 演算法的核心部分。使用者必須自行實現網路傳輸、磁碟儲存以及Raft流程中涉及的其他元件(儘管etcd提供了預設實作)。

與 etcd raft 函式庫的互動有些簡單:它告訴您哪些資料需要持久化以及哪些訊息需要傳送到其他節點。您的責任是處理儲存和網路傳輸過程並相應地通知它。它不關心如何實現這些操作的細節;它只是處理您提交的數據,並根據 Raft 演算法告訴您接下來的步驟。

在etcd raft的程式碼實作中,這種互動模型與Go獨特的通道特性無縫結合,使得etcd raft函式庫真正與眾不同。

如何實現Raft日誌

日誌和 log_unstable

在etcd raft中,Raft Log的主要實作位於log.go和log_unstable.go檔案中,主要結構是raftLog和unstable。不穩定結構也是 raftLog 中的一個領域。

  • raftLog負責Raft Log的主要邏輯。它可以透過提供給使用者的Storage介面存取節點的日誌儲存狀態。
  • 不穩定,顧名思義,包含尚未持久化的日誌條目,即未提交的日誌。

etcd raft透過協調raftLog和unstable來管理演算法內的日誌。

raftLog和unstable的核心字段

為了簡化討論,本文將只關注日誌條目的處理邏輯,而不涉及 etcd raft 中的快照處理。

type raftLog struct {
    storage Storage
    unstable unstable
    committed uint64
    applying uint64
    applied uint64
}
登入後複製
登入後複製
登入後複製

raftLog的核心欄位:

  • storage:使用者實現的儲存接口,用於檢索已經持久化的日誌條目。
  • 不穩定:儲存未持久化的日誌。例如,當 Leader 收到來自客戶端的請求時,它會使用其 Term 建立日誌條目並將其附加到不穩定日誌中。
  • 已提交:在Raft論文中稱為commitIndex,它表示最後一個已知已提交日誌條目的索引。
  • 正在套用:目前正在套用的日誌條目的最高索引。
  • applied:在Raft論文中被稱為lastApplied,它是已經應用到狀態機的日誌條目的最高索引。
type unstable struct {
    entries []pb.Entry
    offset uint64
    offsetInProgress uint64
}
登入後複製
登入後複製

unstable 的核心領域:

  • entries: 未持久化的日誌條目,作為切片儲存在記憶體中。
  • offset: 用於將entries中的日誌條目對應到Raft Log,其中entries[i] = Raft Log[i offset]。
  • offsetInProgress: 表示目前正在持久化的條目。正在進行的條目由條目[:offsetInProgress-offset]表示。

raftLog 中的核心欄位很簡單,可以輕鬆地與 Raft 論文中的實作相關聯。然而,unstable 中的欄位可能看起來更抽象。以下範例旨在幫助闡明這些概念。

假設我們的 Raft 日誌中已經儲存了 5 個日誌條目。現在,我們在unstable中儲存了3個日誌條目,而這3個日誌條目目前正在持久化。情況如下圖:

Understanding etcd

offset=6表示unstable.entries中位置0、1、2的日誌條目分別對應實際Raft Log中的位置6(0 6)、7(1 6)、8(2 6)。當offsetInProgress=9時,我們知道unstable.entries[:9-6],包括位置0、1和2的三個日誌條目,都被持久化了。

在unstable中使用offset和offsetInProgress的原因是unstable並沒有儲存所有的Raft Log條目。

何時互動

由於我們只專注於 Raft 日誌處理邏輯,所以這裡的「何時互動」是指 etcd raft 何時傳遞需要使用者持久化的日誌條目。

使用者端

etcd raft 主要透過 Node 介面中的方法與使用者互動。 Ready 方法傳回一個通道,讓使用者可以從 etcd raft 接收資料或指令。

type raftLog struct {
    storage Storage
    unstable unstable
    committed uint64
    applying uint64
    applied uint64
}
登入後複製
登入後複製
登入後複製

從此通道接收的 Ready 結構包含需要處理的日誌條目、應傳送到其他節點的訊息、節點的目前狀態等等。

對於Raft Log的討論,我們只需要關注Entries和ComfilledEntries欄位:

  • 條目: 需要持久化的日誌條目。一旦這些條目被持久化,就可以使用儲存介面檢索它們。
  • ComfilledEntries: 需要套用於狀態機的日誌項目。
type unstable struct {
    entries []pb.Entry
    offset uint64
    offsetInProgress uint64
}
登入後複製
登入後複製

處理完Ready傳遞過來的日誌、訊息等資料後,我們可以呼叫Node介面中的Advance方法來通知etcd raft我們已經完成了它的指令,讓它可以接收並處理下一個Ready。

etcd raft 提供了 AsyncStorageWrites 選項,可在一定程度上增強節點效能。然而,我們在這裡不考慮這個選項。

etcd 筏側

在使用者端,重點是處理接收到的 Ready 結構體中的資料。在 etcd raft 方面,重點是確定何時將 Ready 結構傳遞給使用者以及之後要採取的操作。

我在下圖中總結了這個過程中涉及到的主要方法,圖中展示了方法調用的大致順序(注意,這僅代表大概的調用順序):

Understanding etcd

可以看到整個過程是一個循環。這裡我們先概述一下這些方法的大致功能,在後續的寫流分析中,我們會深入研究這些方法是如何作用於raftLog和unstable等核心欄位的。

  • HasReady: 顧名思義,它檢查是否有一個 Ready 結構體需要傳遞給使用者。例如,如果unstable中有未持久化的日誌條目目前不在持久化過程中,HasReady將傳回true。
  • readyWithoutAccept: HasReady 傳回 true 後調用,該方法會建立要傳回給使用者的 Ready 結構體,包括需要持久化的日誌條目和標記為已提交的日誌條目。
  • acceptReady: 在etcd raft 將readyWithoutAccept 建立的Ready 結構傳遞給使用者後呼叫。它將Ready傳回的日誌條目標記為正在持久化和應用,並建立一個“回調”,當使用者調用Node.Advance時調用,將日誌條目標記為已持久化和應用。
  • Advance: 在使用者呼叫 Node.Advance 後執行在 AcceptReady 中建立的「回呼」。

如何定義承諾和應用

這裡有兩點要考慮:

1。堅持≠承諾

如最初所定義的,只有當日誌條目被 Raft 叢集中的大多數節點持久化時,該日誌條目才被視為已提交。因此,即使我們透過 Ready 持久化 etcd raft 傳回的條目,這些條目仍然無法被標記為已提交。

但是,當我們呼叫 Advance 方法通知 etcd raft 我們已經完成持久化步驟時,etcd raft 將評估叢集中其他節點的持久化狀態,並將一些日誌條目標記為已提交。然後,這些條目透過 Ready 結構的 CommiedEntries 欄位提供給我們,以便我們可以將它們套用到狀態機。

因此,在使用 etcd raft 時,將條目標記為已提交的時間由內部管理,使用者只需滿足持久性先決條件即可。

內部透過呼叫 raftLog.commitTo 方法實現承諾,該方法會更新 raftLog.comfilled,對應 Raft 論文中的 commitIndex。

2。承諾≠應用

在 etcd raft 中呼叫 raftLog.commitTo 方法後,直到 raft.comfilled 索引的日誌條目都被視為已提交。然而,索引在lastApplied

將條目標記為已應用的時間也在 etcd raft 內部處理;使用者只需要將Ready中提交的條目應用到狀態機即可。

另一個微妙之處是,在 Raft 中,只有 Leader 可以提交條目,但所有節點都可以應用它們。

寫入請求處理流程

在這裡,我們將透過分析 etcd raft 處理寫入請求時的流程來連接先前討論的所有概念。

初始狀態

為了討論更一般的場景,我們將從一個 已經提交並應用了三個日誌條目的 Raft 日誌開始

Understanding etcd

圖中,綠色代表raftLog欄位和儲存在Storage中的日誌條目,而紅色代表不穩定欄位和儲存在entry中的未持久化日誌條目。

由於我們已經提交並套用了三個日誌條目,因此已提交和已套用的日誌條目都設定為 3。 applying 欄位保存前一個應用程式的最高日誌條目的索引,在本例中也是 3。

此時,還沒有發起任何請求,因此unstable.entries為空。 Raft Log 中的下一個日誌索引是 4,偏移量為 4。由於目前沒有日誌被持久化,所以 offsetInProgress 也設定為 4。

發出請求

現在,我們發起一個請求,將兩個日誌條目追加到 Raft Log 中。

Understanding etcd

如圖所示,附加的日誌條目儲存在unstable.entries中。在此階段,核心欄位中記錄的索引值不會發生任何變更。

已準備好

還記得HasReady方法嗎? HasReady 檢查是否有未持久化的日誌條目,如果有,則傳回 true。

判斷是否存在未持久化日誌條目的邏輯是基於unstable.entries[offsetInProgress-offset:]的長度是否大於0。顯然,在我們的例子中:

type raftLog struct {
    storage Storage
    unstable unstable
    committed uint64
    applying uint64
    applied uint64
}
登入後複製
登入後複製
登入後複製

表示有兩個未持久化的日誌條目,因此 HasReady 傳回 true。

Understanding etcd

準備好但不接受

readyWithoutAccept 的目的是建立要回傳給使用者的 Ready 結構體。由於我們有兩個未持久化的日誌條目,readyWithoutAccept 會將這兩個日誌條目包含在傳回的 Ready 的 Entries 欄位中。

Understanding etcd

接受準備

acceptReady 在 Ready 結構傳遞給使用者後被呼叫。

Understanding etcd

acceptReady 將正在持久化的日誌條目的索引更新為 6,這表示 [4, 6) 範圍內的日誌條目現在被標記為正在持久化。

進步

使用者將Entries持久化為Ready後,呼叫Node.Advance來通知etcd raft。然後,etcd raft 就可以執行在acceptReady 中建立的「回呼」了。

Understanding etcd

這個「回呼」會清除unstable.entries中已經持久化的日誌條目,然後將偏移量設為Storage.LastIndex 1,即6。

提交日誌條目

我們假設這兩個日誌條目已經被 Raft 叢集中的大多數節點持久化,因此我們可以將這兩個日誌條目標記為已提交。

Understanding etcd

已準備好

繼續我們的循環,HasReady 偵測到是否存在已提交但尚未套用的日誌條目,因此傳回 true。

Understanding etcd

準備好但不接受

readyWithoutAccept 傳回一個 Ready,其中包含已提交但尚未應用於狀態機的日誌條目 (4, 5)。

這些條目的計算方式為:低、高:= 在左開、右閉區間內應用 1、提交 1。

Understanding etcd

接受準備

acceptReady 然後將 Ready 中傳回的日誌條目 [4, 5] 標記為已套用於狀態機。

Understanding etcd

進步

使用者呼叫 Node.Advance 後,etcd raft 執行「回呼」並將更新應用於 5,表示索引 5 及先前的日誌項目已全部應用於狀態機。

Understanding etcd

最終狀態

這樣就完成了寫入請求的處理流程。最終狀態如下圖,可以和初始狀態進行比較。

Understanding etcd

概括

我們首先概述了 Raft Log,了解其基本概念,然後初步了解了 etcd raft 實作。然後我們深入研究了 etcd raft 中 Raft Log 的核心模組,並考慮了重要的問題。最後,我們透過對寫入請求流的完整分析將所有內容連結在一起。

我希望這種方法可以幫助您清楚地了解 etcd Raft 實現,並形成您自己對 Raft Log 的見解。

本文到此結束。如有錯誤或疑問,歡迎私訊或留言。

順便說一句,raft-foiver 是我實現的 etcd raft 的簡化版本,保留了 Raft 的所有核心邏輯,並根據 Raft 論文中的流程進行了優化。以後我會單獨發一篇文章介紹這個函式庫。如果你有興趣,請隨時 Star、Fork 或 PR!

參考

  • https://github.com/B1NARY-GR0UP/raft
  • https://github.com/etcd-io/raft

以上是了解 etcd 的 Raft 實作:深入研究 Raft 日誌的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板