歡迎光臨
每天分享高質量文章

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

SOFAStack 

Scalable Open Financial Architecture Stack 

是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景里錘煉出來的最佳實踐。

本文為《剖析 | SOFAJRaft 實現原理》第一篇,本篇作者米麒麟,來自陸金所。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和原始碼愛好者們出品,專案代號:,文章尾部有參與方式,歡迎同樣對原始碼熱情的你加入。

SOFAJRaft https://github.com/alipay/sofa-jraft

 

前言 

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 日誌儲存實現接口定義入口:

  1.  

    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 快速搜索。

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 儲存日誌實現核心入口:

  1.  

    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、TRUNCATEPREFIX、TRUNCATESUFFIX、LASTLOGID 時呼叫底層日誌儲存 LogStorage 進行指定事件回呼 ResetClosure、TruncatePrefixClosure、TruncateSuffixClosure、LastLogIdClosure 處理。

當 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 元信息儲存實現接口定義入口:

  1.  

    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 元信息實現入口:

  1.  

    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,加載檔案命名為 _raftsnapshot_meta 的 Raft 快照元資料至記憶體。

  • open()

    根據快照最後一個索引 lastSnapshotIndex 獲取檔案前綴為 snapshot_ 快照儲存路徑,基於快照儲存路徑創建初始化快照閱讀器 LocalSnapshotReader,加載檔案命名為 _raftsnapshot_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,加載 _raftsnapshotmeta 快照元資料檔案初始化編寫器;構建儲存鏡像回呼SaveSnapshotDone 提供 FSMCaller 呼叫 StateMachine 的狀態轉換髮布 SNAPSHOTSAVE 型別任務事件到 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 配置和鏡像流程。

    已同步到看一看
    赞(0)

    分享創造快樂