本文將從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)上時,它被視為已提交。
當日誌條目應用於狀態機時,它被視為已套用。
etcd raft 是一個用 Go 編寫的 Raft 演算法庫,廣泛應用於 etcd、Kubernetes、CockroachDB 等系統。
etcd raft 的首要特點是它只實現了 Raft 演算法的核心部分。使用者必須自行實現網路傳輸、磁碟儲存以及Raft流程中涉及的其他元件(儘管etcd提供了預設實作)。
與 etcd raft 函式庫的互動有些簡單:它告訴您哪些資料需要持久化以及哪些訊息需要傳送到其他節點。您的責任是處理儲存和網路傳輸過程並相應地通知它。它不關心如何實現這些操作的細節;它只是處理您提交的數據,並根據 Raft 演算法告訴您接下來的步驟。
在etcd raft的程式碼實作中,這種互動模型與Go獨特的通道特性無縫結合,使得etcd raft函式庫真正與眾不同。
在etcd raft中,Raft Log的主要實作位於log.go和log_unstable.go檔案中,主要結構是raftLog和unstable。不穩定結構也是 raftLog 中的一個領域。
etcd raft透過協調raftLog和unstable來管理演算法內的日誌。
為了簡化討論,本文將只關注日誌條目的處理邏輯,而不涉及 etcd raft 中的快照處理。
type raftLog struct { storage Storage unstable unstable committed uint64 applying uint64 applied uint64 }
raftLog的核心欄位:
type unstable struct { entries []pb.Entry offset uint64 offsetInProgress uint64 }
unstable 的核心領域:
raftLog 中的核心欄位很簡單,可以輕鬆地與 Raft 論文中的實作相關聯。然而,unstable 中的欄位可能看起來更抽象。以下範例旨在幫助闡明這些概念。
假設我們的 Raft 日誌中已經儲存了 5 個日誌條目。現在,我們在unstable中儲存了3個日誌條目,而這3個日誌條目目前正在持久化。情況如下圖:
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欄位:
type unstable struct { entries []pb.Entry offset uint64 offsetInProgress uint64 }
處理完Ready傳遞過來的日誌、訊息等資料後,我們可以呼叫Node介面中的Advance方法來通知etcd raft我們已經完成了它的指令,讓它可以接收並處理下一個Ready。
etcd raft 提供了 AsyncStorageWrites 選項,可在一定程度上增強節點效能。然而,我們在這裡不考慮這個選項。
在使用者端,重點是處理接收到的 Ready 結構體中的資料。在 etcd raft 方面,重點是確定何時將 Ready 結構傳遞給使用者以及之後要採取的操作。
我在下圖中總結了這個過程中涉及到的主要方法,圖中展示了方法調用的大致順序(注意,這僅代表大概的調用順序):
可以看到整個過程是一個循環。這裡我們先概述一下這些方法的大致功能,在後續的寫流分析中,我們會深入研究這些方法是如何作用於raftLog和unstable等核心欄位的。
這裡有兩點要考慮:
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 日誌開始。
圖中,綠色代表raftLog欄位和儲存在Storage中的日誌條目,而紅色代表不穩定欄位和儲存在entry中的未持久化日誌條目。
由於我們已經提交並套用了三個日誌條目,因此已提交和已套用的日誌條目都設定為 3。 applying 欄位保存前一個應用程式的最高日誌條目的索引,在本例中也是 3。
此時,還沒有發起任何請求,因此unstable.entries為空。 Raft Log 中的下一個日誌索引是 4,偏移量為 4。由於目前沒有日誌被持久化,所以 offsetInProgress 也設定為 4。
現在,我們發起一個請求,將兩個日誌條目追加到 Raft Log 中。
如圖所示,附加的日誌條目儲存在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。
readyWithoutAccept 的目的是建立要回傳給使用者的 Ready 結構體。由於我們有兩個未持久化的日誌條目,readyWithoutAccept 會將這兩個日誌條目包含在傳回的 Ready 的 Entries 欄位中。
acceptReady 在 Ready 結構傳遞給使用者後被呼叫。
acceptReady 將正在持久化的日誌條目的索引更新為 6,這表示 [4, 6) 範圍內的日誌條目現在被標記為正在持久化。
使用者將Entries持久化為Ready後,呼叫Node.Advance來通知etcd raft。然後,etcd raft 就可以執行在acceptReady 中建立的「回呼」了。
這個「回呼」會清除unstable.entries中已經持久化的日誌條目,然後將偏移量設為Storage.LastIndex 1,即6。
我們假設這兩個日誌條目已經被 Raft 叢集中的大多數節點持久化,因此我們可以將這兩個日誌條目標記為已提交。
繼續我們的循環,HasReady 偵測到是否存在已提交但尚未套用的日誌條目,因此傳回 true。
readyWithoutAccept 傳回一個 Ready,其中包含已提交但尚未應用於狀態機的日誌條目 (4, 5)。
這些條目的計算方式為:低、高:= 在左開、右閉區間內應用 1、提交 1。
acceptReady 然後將 Ready 中傳回的日誌條目 [4, 5] 標記為已套用於狀態機。
使用者呼叫 Node.Advance 後,etcd raft 執行「回呼」並將更新應用於 5,表示索引 5 及先前的日誌項目已全部應用於狀態機。
這樣就完成了寫入請求的處理流程。最終狀態如下圖,可以和初始狀態進行比較。
我們首先概述了 Raft Log,了解其基本概念,然後初步了解了 etcd raft 實作。然後我們深入研究了 etcd raft 中 Raft Log 的核心模組,並考慮了重要的問題。最後,我們透過對寫入請求流的完整分析將所有內容連結在一起。
我希望這種方法可以幫助您清楚地了解 etcd Raft 實現,並形成您自己對 Raft Log 的見解。
本文到此結束。如有錯誤或疑問,歡迎私訊或留言。
順便說一句,raft-foiver 是我實現的 etcd raft 的簡化版本,保留了 Raft 的所有核心邏輯,並根據 Raft 論文中的流程進行了優化。以後我會單獨發一篇文章介紹這個函式庫。如果你有興趣,請隨時 Star、Fork 或 PR!
以上是了解 etcd 的 Raft 實作:深入研究 Raft 日誌的詳細內容。更多資訊請關注PHP中文網其他相關文章!