螞蟻金服生產級 Raft 算法庫存儲模塊剖析 | SOFAJRaft 實現原理

前言

SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景算法

SOFAJRaft 存儲模塊分爲:緩存

  1. Log 存儲記錄 Raft 配置變動和用戶提交任務日誌;
  2. Meta 存儲即元信息存儲記錄 Raft 實現的內部狀態;
  3. Snapshot 存儲用於存放用戶的狀態機 Snapshot 及元信息。

本文將圍繞日誌存儲,元信息存儲以及快照存儲等方面剖析 SOFAJRaft 存儲模塊原理,闡述如何解決 Raft 協議存儲問題以及存儲模塊實現:安全

  • Raft 配置變動和用戶提交任務日誌如何存儲?如何調用管理日誌存儲?
  • SOFAJRaft Server 節點 Node 是如何存儲 Raft 內部配置?
  • Raft 狀態機快照 Snapshot 機制如何實現?如何存儲安裝鏡像?

日誌存儲

Log 存儲,記錄 Raft 配置變動和用戶提交任務的日誌,把日誌從 Leader 複製到其餘節點上面。網絡

  • LogStorage 是日誌存儲實現,默認實現基於 RocksDB 存儲,經過 LogStorage 接口擴展自定義日誌存儲實現;
  • LogManager 負責調用底層日誌存儲 LogStorage,針對日誌存儲調用進行緩存、批量提交、必要的檢查和優化。

LogStorage 存儲實現

LogStorage 日誌存儲實現,定義 Raft 分組節點 Node 的 Log 存儲模塊核心 API 接口包括:數據結構

  • 返回日誌裏的首/末個日誌索引;
  • 按照日誌索引獲取 Log Entry 及其任期;
  • 把單個/批量 Log Entry 添加到日誌存儲;
  • 從 Log 存儲頭部/末尾刪除日誌;
  • 刪除全部現有日誌,重置下任日誌索引。

Log Index 提交到 Raft Group 中的任務序列化爲日誌存儲,每條日誌一個編號,在整個 Raft Group 內單調遞增並複製到每一個 Raft 節點。LogStorage 日誌存儲實現接口定義入口:併發

com.alipay.sofa.jraft.storage.LogStorage

RocksDBLogStorage 基於 RocksDB 實現

Log Structured Merge Tree 簡稱 LSM ,把一顆大樹拆分紅 N 棵小樹,數據首先寫入內存,內存裏構建一顆有序小樹,隨着小樹愈來愈大,內存的小樹 Flush 到磁盤,磁盤中的樹按期作合併操做合併成一棵大樹以優化讀性能,經過把磁盤的隨機寫轉化爲順序寫提升寫性能,RocksDB 就是基於 LSM-Tree 數據結構使用 C++ 編寫的嵌入式 KV 存儲引擎,其鍵值均容許使用二進制流。RocksDB 按順序組織全部數據,通用操做包括 get(key), put(key), delete(Key) 以及 newIterator()。RocksDB 有三種基本的數據結構:memtable,sstfile 以及 logfile。memtable 是一種內存數據結構--全部寫入請求都會進入 memtable,而後選擇性進入 logfile。logfile 是一種有序寫存儲結構,當 memtable 被填滿的時候被刷到 sstfile 文件並存儲起來,而後相關的 logfile 在以後被安全地刪除。sstfile 內的數據都是排序好的,以便於根據 key 快速搜索。app

LogStorage 默認實現 RocksDBLogStorage 是基於 RocksDB 存儲日誌,初始化日誌存儲 StorageFactory 根據 Raft節點日誌存儲路徑和 Raft 內部實現是否調用 fsync 配置默認建立 RocksDBLogStorage 日誌存儲。基於 RocksDB 存儲實現 RocksDBLogStorage 核心操做包括:性能

  • init():建立 RocksDB 配置選項調用 RocksDB#open() 方法構建 RocksDB 實例,添加 default 默認列族及其配置選項獲取列族處理器,經過 newIterator() 生成 RocksDB 迭代器遍歷 KeyValue 數據檢查 Value 類型加載 Raft 配置變動到配置管理器 ConfigurationManager。RocksDB 引入列族 ColumnFamily 概念,所謂列族是指一系列 KeyValue 組成的數據集,RocksDB 讀寫操做須要指定列族,建立 RocksDB 默認構建命名爲default 的列族。
  • shutdown():首先關閉列族處理器以及 RocksDB 實例,其次遍歷列族配置選項執行關閉操做,接着關閉RocksDB 配置選項,最後清除強引用以達到 Help GC 垃圾回收 RocksDB 實例及其配置選項對象。
  • getFirstLogIndex():基於處理器 defaultHandle 和讀選項 totalOrderReadOptions 方法構建 RocksDB 迭代器 RocksIterator,檢查是否加載過日誌裏第一個日誌索引,未加載需調用 seekToFirst() 方法獲取緩存 RocksDB 存儲日誌數據的第一個日誌索引。
  • getLastLogIndex():基於處理器 defaultHandle 和讀選項 totalOrderReadOptions 構建 RocksDB 迭代器 RocksIterator,調用 seekToLast() 方法返回 RocksDB 存儲日誌記錄的最後一個日誌索引。
  • getEntry(index):基於處理器 defaultHandle 和指定日誌索引調用 RocksDB#get() 操做返回 RocksDB 索引位置日誌 LogEntry。
  • getTerm(index):基於處理器 defaultHandle 和指定日誌索引調用 RocksDB#get() 操做獲取 RocksDB 索引位置日誌而且返回其 LogEntry 的任期。
  • appendEntry(entry):檢查日誌 LogEntry 類型是否爲配置變動,配置變動類型調用 RocksDB#write() 方法執行批量寫入,用戶提交任務的日誌基於處理器 defaultHandle 和 LogEntry 對象調用 RocksDB#put() 方法存儲。
  • appendEntries(entries):調用 RocksDB#write() 方法把 Raft 配置變動或者用戶提交任務的日誌同步刷盤批量寫入 RocksDB 存儲,經過 Batch Write 手段合併 IO 寫入請求減小方法調用和上下文切換。
  • truncatePrefix(firstIndexKept):獲取第一個日誌索引,後臺啓動一個線程基於默認處理器 defaultHandle 和配置處理器 confHandle 執行 RocksDB#deleteRange() 操做刪除從 Log 頭部以第一個日誌索引到指定索引位置範圍的 RocksDB 日誌數據。
  • truncateSuffix(lastIndexKept):獲取最後一個日誌索引,基於默認處理器 defaultHandle 和配置處理器 confHandle 執行 RocksDB#deleteRange() 操做清理從 Log 末尾以指定索引位置到最後一個索引範疇的 RocksDB 未提交日誌。
  • reset(nextLogIndex):獲取 nextLogIndex 索引對應的 LogEntry,執行 RocksDB#close() 方法關閉 RocksDB實例,調用 RocksDB#destroyDB() 操做銷燬 RocksDB 實例清理 RocksDB 全部數據,從新初始化加載 RocksDB 實例而且重置下一個日誌索引位置。

RocksDBLogStorage 基於 RocksDB 存儲日誌實現核心入口:優化

com.alipay.sofa.jraft.storage.RocksDBLogStorage

LogManager 存儲調用

日誌管理器 LogManager 負責調用 Log 日誌存儲 LogStorage,對 LogStorage 調用進行緩存管理、批量提交、檢查優化。Raft 分組節點 Node 初始化/啓動時初始化日誌存儲 StorageFactory 構建日誌管理器 LogManager,基於日誌存儲 LogStorage、配置管理器 ConfigurationManager、有限狀態機調用者 FSMCaller、節點性能監控 NodeMetrics 等 LogManagerOptions 配置選項實例化 LogManager。根據 Raft 節點 Disruptor Buffer 大小配置生成穩定狀態回調 StableClosure 事件 Disruptor 隊列,設置穩定狀態回調 StableClosure 事件處理器 StableClosureEventHandler 處理隊列事件,其中 StableClosureEventHandler 處理器事件觸發的時候判斷任務回調 StableClosure 的 Log Entries 是否爲空,若是任務回調的 Log Entries 爲非空需積攢日誌條目批量 Flush,空則檢查 StableClosureEvent 事件類型而且調用底層存儲 LogStorage#appendEntries(entries) 批量提交日誌寫入 RocksDB,當事件類型爲SHUTDOWN、RESET、TRUNCATE_PREFIX、TRUNCATE_SUFFIX、LAST_LOG_ID 時調用底層日誌存儲 LogStorage 進行指定事件回調 ResetClosure、TruncatePrefixClosure、TruncateSuffixClosure、LastLogIdClosure 處理。ui

當 Client 向 SOFAJRaft 發送命令以後,Raft 分組節點 Node 的日誌管理器 LogManager 首先將命令以 Log 的形式存儲到本地,調用 appendEntries(entries, done) 方法檢查 Node 節點當前爲 Leader 而且 Entries 來源於用戶未知分配到的正確日誌索引時須要分配索引給添加的日誌 Entries ,而當前爲 Follower 時而且 Entries 來源於 Leader 必須檢查以及解決本地日誌和  Entries 之間的衝突。接着遍歷日誌條目 Log Entries 檢查類型是否爲配置變動,配置管理器 ConfigurationManager 緩存配置變動 Entry,將現有日誌條目 Entries 添加到 logsInMemory 進行緩存,穩定狀態回調 StableClosure 設置須要存儲的日誌,發佈 OTHER 類型事件到穩定狀態回調 StableClosure 事件隊列,觸發穩定狀態回調 StableClosure 事件處理器 StableClosureEventHandler 處理該事件,處理器獲取任務回調的 Log Entries 把日誌條目積累到內存中以便後續統一批量 Flush,經過 appendToStorage(toAppend) 操做調用底層LogStorage 存儲日誌 Entries。同時 Replicator 把此條 Log 複製給其餘的 Node 實現併發的日誌複製,當 Node 接收集羣中半數以上的 Node 返回的「複製成功」的響應將這條 Log 以及以前的 Log 有序的發送至狀態機裏面執行。

LogManager 調用日誌存儲 LogStorage 實現邏輯:

元信息存儲

Metadata 存儲即元信息存儲,用來存儲記錄 Raft 實現的內部狀態,譬如當前任期 Term、投票給哪一個 PeerId 節點等信息。

RaftMetaStorage 存儲實現

RaftMetaStorage 元信息存儲實現,定義 Raft 元數據的 Metadata 存儲模塊核心 API 接口包括:

  • 設置/獲取 Raft 元數據的當前任期 Term;
  • 分配/查詢 Raft 元信息的 PeerId 節點投票。

Raft 內部狀態任期 Term 是在整個 Raft Group 裏單調遞增的 long 數字,用來表示一輪投票的編號,其中成功選舉出來的 Leader 對應的 Term 稱爲 Leader Term,Leader 沒有發生變動期間提交的日誌都有相同的 Term 編號。PeerId 表示 Raft 協議的參與者(Leader/Follower/Candidate etc.), 由三元素組成: ip:port:index,其中 ip 是節點的 IP, port 是端口, index 表示同一個端口的序列號。RaftMetaStorage 元信息存儲實現接口定義入口:

com.alipay.sofa.jraft.storage.RaftMetaStorage

LocalRaftMetaStorage 基於 ProtoBuf 實現

Protocol Buffers 是一種輕便高效的結構化數據存儲格式,用於結構化數據串行化或者說序列化,適合作數據存儲或 RPC 數據交換格式,用於通信協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。用戶在 .proto 文件定義 Protocol Buffer 的 Message 類型指定須要序列化的數據結構,每個 Message  都是一個小的信息邏輯單元包含一系列的鍵值對,每種類型的 Message 涵蓋一個或者多個惟一編碼字段,每一個字段由名稱和值類型組成,容許 Message 定義可選字段 Optional Fields、必須字段 Required Fields、可重複字段 Repeated Fields。

RaftMetaStorage 默認實現 LocalRaftMetaStorage 是基於 ProtoBuf  Message 本地存儲 Raft 元數據,初始化元信息存儲 StorageFactory 根據 Raft 元信息存儲路徑、 Raft 內部配置以及 Node 節點監控默認建立 LocalRaftMetaStorage 元信息存儲。基於 ProtoBuf 存儲實現 LocalRaftMetaStorage 主要操做包括:

  • init():獲取 Raft 元信息存儲配置 RaftMetaStorageOptions 節點 Node,讀取命名爲 raft_meta 的 ProtoBufFile 文件加載 StablePBMeta 消息,根據 StablePBMeta ProtoBuf 元數據緩存 Raft 當前任期 Term 和 PeerId 節點投票信息。
  • shutdown():獲取內存裏 Raft 當前任期 Term 和 PeerId 節點投票構建 StablePBMeta 消息,按照 Raft 內部是否同步元數據配置寫入 ProtoBufFile 文件。
  • setTerm(term):檢查 LocalRaftMetaStorage 初始化狀態,緩存設置的當前任期 Term,按照 Raft 是否同步元數據配置把當前任期 Term 做爲 ProtoBuf 消息保存到 ProtoBufFile 文件。
  • getTerm():檢查 LocalRaftMetaStorage 初始化狀態,返回緩存的當前任期 Term。
  • setVotedFor(peerId):檢查 LocalRaftMetaStorage 初始化狀態,緩存投票的 PeerId 節點,按照 Raft 是否同步元數據配置把投票 PeerId 節點做爲 ProtoBuf 消息保存到 ProtoBufFile 文件。
  • getVotedFor():檢查 LocalRaftMetaStorage 初始化狀態,返回緩存的投票 PeerId 節點。

LocalRaftMetaStorage 基於 ProtoBuf 本地存儲 Raft 元信息實現入口:

com.alipay.sofa.jraft.storage.impl.LocalRaftMetaStorage

快照存儲

當 Raft 節點 Node 重啓時,內存中狀態機的狀態數據丟失,觸發啓動過程從新存放日誌存儲 LogStorage 的全部日誌重建整個狀態機實例,此種場景會致使兩個問題:

  • 若是任務提交比較頻繁,例如消息中間件場景緻使整個重建過程很長啓動緩慢;
  • 若是日誌很是多而且節點須要存儲全部的日誌,對存儲來講是資源佔用不可持續;
  • 若是增長 Node 節點,新節點須要從 Leader 獲取全部的日誌從新存放至狀態機,對於 Leader 和網絡帶寬都是不小的負擔。 

所以經過引入 Snapshot 機制來解決此三個問題,所謂快照 Snapshot 即對數據當前值的記錄,是爲當前狀態機的最新狀態構建"鏡像"單獨保存,保存成功刪除此時刻以前的日誌減小日誌存儲佔用;啓動的時候直接加載最新的 Snapshot 鏡像,而後重放在此以後的日誌便可,若是 Snapshot 間隔合理,整個重放到狀態機過程較快,加速啓動過程。最後新節點的加入先從 Leader 拷貝最新的 Snapshot 安裝到本地狀態機,而後只要拷貝後續的日誌便可,可以快速跟上整個 Raft Group 的進度。Leader 生成快照有幾個做用:

  • 當有新的節點 Node 加入集羣不用只靠日誌複製、回放機制和 Leader 保持數據一致,經過安裝 Leader 的快照方式跳過早期大量日誌的回放;
  • Leader 用快照替代 Log 複製減小網絡端的數據量;
  • 用快照替代早期的 Log 節省存儲佔用空間。

Snapshot 存儲,用於存儲用戶的狀態機 Snapshot 及元信息:

  • SnapshotStorage 用於 Snapshot 存儲實現;
  • SnapshotExecutor 用於管理 Snapshot 存儲、遠程安裝、複製。

SnapshotStorage 存儲實現

SnapshotStorage 快照存儲實現,定義 Raft 狀態機的 Snapshot 存儲模塊核心 API 接口包括:

  • 設置 filterBeforeCopyRemote 設置爲 true 複製到遠程以前過濾數據;
  • 建立快照編寫器;
  • 打開快照閱讀器;
  • 從遠程 Uri 複製數據;
  • 啓動從遠程 Uri 複製數據的複製任務;
  • 配置 SnapshotThrottle,SnapshotThrottle 用於重盤讀/寫場景限流的,好比磁盤讀寫、網絡帶寬。

LocalSnapshotStorage 基於本地文件實現

SnapshotStorage 默認實現 LocalSnapshotStorage 是基於本地文件存儲 Raft 狀態機鏡像,初始化元快照存儲 StorageFactory 根據 Raft 鏡像快照存儲路徑和 Raft 配置信息默認建立 LocalSnapshotStorage 快照存儲。基於本地文件存儲實現 LocalSnapshotStorage 主要方法包括:

  • init():刪除文件命名爲 temp 的臨時鏡像 Snapshot,銷燬文件前綴爲 snapshot_ 的舊快照 Snapshot,獲取快照最後一個索引 lastSnapshotIndex。
  • close():按照快照最後一個索引 lastSnapshotIndex 和鏡像編寫器 LocalSnapshotWriter 快照索引重命名臨時鏡像 Snapshot 文件,銷燬編寫器 LocalSnapshotWriter 存儲路徑快照。
  • create():銷燬文件命名爲 temp 的臨時快照 Snapshot,基於臨時鏡像存儲路徑建立初始化快照編寫器 LocalSnapshotWriter,加載文件命名爲 __raft_snapshot_meta 的 Raft 快照元數據至內存。
  • open():根據快照最後一個索引 lastSnapshotIndex 獲取文件前綴爲 snapshot_ 快照存儲路徑,基於快照存儲路徑建立初始化快照閱讀器 LocalSnapshotReader,加載文件命名爲 __raft_snapshot_meta 的 Raft 鏡像元數據至內存。
  • startToCopyFrom(uri, opts):建立初始化狀態機快照複製器 LocalSnapshotCopier,生成遠程文件複製器 RemoteFileCopier,基於遠程服務地址 Endpoint 獲取 Raft 客戶端 RPC 服務鏈接指定 Uri,啓動後臺線程複製 Snapshot 鏡像數據,加載 Raft 快照元數據獲取遠程快照 Snapshot 鏡像文件,讀取遠程指定快照存儲路徑數據拷貝到 BoltSession,快照複製器 LocalSnapshotCopier 同步 Raft 快照元數據。

SnapshotExecutor 存儲管理

快照執行器 SnapshotExecutor 負責 Raft 狀態機 Snapshot 存儲、Leader 遠程安裝快照、複製鏡像 Snapshot 文件,包括兩大核心操做:狀態機快照 doSnapshot(done) 和安裝快照 installSnapshot(request, response, done)。StateMachine 快照 doSnapshot(done) 獲取基於臨時鏡像 temp 文件路徑的 Snapshot 存儲快照編寫器 LocalSnapshotWriter,加載 __raft_snapshot_meta 快照元數據文件初始化編寫器;構建保存鏡像回調SaveSnapshotDone 提供 FSMCaller 調用 StateMachine 的狀態轉換髮布 SNAPSHOT_SAVE 類型任務事件到 Disruptor 隊列,經過 Ring Buffer 方式觸發申請任務處理器 ApplyTaskHandler 運行快照保存任務,調用 onSnapshotSave() 方法存儲各類類型狀態機快照。遠程安裝快照 installSnapshot(request, response, done) 按照安裝鏡像請求響應以及快照原信息建立而且註冊快照下載做業 DownloadingSnapshot,加載快照下載 DownloadingSnapshot 獲取當前快照拷貝器的閱讀器 SnapshotReader,構建安裝鏡像回調 InstallSnapshotDone 分配 FSMCaller 調用 StateMachine 的狀態轉換髮布 SNAPSHOT_LOAD 類型任務事件到 Disruptor 隊列,也是經過 Ring Buffer 觸發申請任務處理器 ApplyTaskHandler 執行快照安裝任務,調用 onSnapshotLoad() 操做加載各類類型狀態機快照。

SnapshotExecutor 狀態機快照和遠程安裝鏡像實現邏輯:

總結

本文從 Log 日誌存儲 LogStorage、Meta 元信息存儲 RaftMetaStorage 以及 Snapshot 快照存儲 SnapshotStorage 三個方面詳述 SOFAJRaft 存儲模塊實現細節,直觀刻畫 SOFAJRaft Server 節點 Node 之間存儲日誌、Raft 配置和鏡像流程。


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索