SOFAStack(Scalable Open Financial Architecture Stack)是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。git
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。github
本文爲《剖析 | SOFAJRaft 實現原理》最後一篇,本篇做者胡宗棠,來自中國移動。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:SOFA:JRaftLab/,文末包含往期系列文章。算法
SOFAJRaft:https://gitee.com/sofastack/sofa-jraft瀏覽器
本文主要介紹 SOFAJRaft 在日誌複製和管理中所採用的快照機制。考慮到單獨介紹 SOFAJRaft 中的快照機制原理和實現或許有一些唐突,我會先經過一個讀者都可以看得明白的例子做爲切入點,讓你們對快照這個概念、它能夠解決的主要問題,先有一個比較深入的理解。服務器
SOFAJRaft 是對 Raft 共識算法的 Java 實現。既然是共識算法,就不可避免的要對須要達成共識的內容,在多個服務器節點之間進行傳輸,通常將這些共識的內容稱之爲日誌塊(LogEntry)。若是讀過《剖析 | SOFAJRaft 實現原理》系列前面幾篇文章的同窗,應該瞭解到在 SOFAJRaft 中,能夠經過「節點之間併發複製日誌」、「批量化複製日誌」和「複製日誌pipeline機制」等優化手段來保證服務器節點之間日誌複製效率達到最大化。網絡
但若是遇到下面的兩個場景,僅依靠上面的優化方法並不能有效地根本解決問題:架構
帶着上面兩個疑問,咱們能夠先來看一個你們平常生活中都會遇到的場景—從新安裝操做系統,而後再通俗易懂地爲你們介紹快照的概念與特色。併發
有一天,你的筆記本電腦的 Windows 操做系統由於某一些緣由出現啓動後屢次崩潰問題,無論經過任何方式都沒辦法解決。這時候,咱們想到解決問題的第一個方案就是爲這臺電腦從新安裝操做系統。若是,咱們平時偶爾爲本身電腦的操做系統作過鏡像,直接用以前的鏡像文件便可快速還原系統至以前的某一時間點的狀態,而無需從零開始安裝 Windows 操做系統後,再花大量時間來從新安裝一些本身所須要的系統軟件(好比 Chrome 瀏覽器、印象筆記和 FoxMail 郵件客戶端等)。框架
在上面的例子中,電腦操做系統的鏡像就是系統某一時刻的「快照」,由於它包含了這一時刻,系統當前狀態機的值(對於用戶來講,就是安裝了哪些的應用軟件)。在須要從新安裝操做系統時候,經過鏡像這一「快照」,能夠很高效地完成還原電腦操做系統這個任務,而無需從零開始安裝系統和相應的應用軟件。因此,咱們這裏能夠爲「快照」下一個簡單的定義:一種經過某種數據格式文件來保存系統當前的狀態值的一個副本。異步
「快照」的特色,就如同它字面意思同樣,能夠分爲「快」和「照」:
讀到這裏,再去回顧第一節內容開頭提出的兩個問題,你們應該能夠想到解決問題的方法就是經過引入快照機制。
在 SOFAJRaft 中,Snapshot 爲當前 Raft 節點狀態機的最新狀態打了一個「鏡像」單獨保存,保存成功後在這個時刻以前的日誌便可刪除,減小了日誌文件在磁盤中的佔用空間。而在 Raft 節點啓動時,能夠直接加載最新的 Snapshot 鏡像,直接重放在此以後的日誌文件便可。若是設置保存 Snapshot 的時間間隔比較合理,那麼節點加載鏡像後重放的日誌文件較少,啓動速度也會比較快。對於新 Raft 節點加入某個 SOFAJRaft Group 集羣的場景,新節點可先從 Leader 節點上拷貝最新的 Snapshot 安裝到本地狀態機,而後拷貝後續的日誌數據便可,這樣能夠在快速跟上整個 SOFAJRaft Group 集羣進度的同時,又不會佔用 Leader 節點較大的網絡帶寬資源。
在一個正常運行的 SOFAJRaft Group 集羣中,當其中某一個 Raft 節點出現故障了(假設該故障的緣由不是由磁盤損壞等不可逆因素致使的),該 Raft 節點修復故障從新啓動時,若是節點禁用 Snapshot 快照機制,那麼會重放全部本地的日誌到狀態機以跟上最新的日誌,這樣節點啓動和達到日誌備份完整的耗時均會比較長。可是,若是此時節點開啓了 Snapshot 快照機制,那麼一切就會變得很是高效,節點只須要加載最新的 Snapshot 至狀態機,而後以 Snapshot 數據的日誌爲起點開始繼續回放日誌至狀態機,直到使得狀態機達到最新狀態。
圖1 在 Snapshot 禁用狀況下集羣節點擴容
圖2 在 Snapshot 啓用狀況下集羣節點擴容
從上面兩張 SOFAJRaft 集羣的結構圖上,能夠很明顯地看出在開啓和禁用 Snapshot 時,擴容的新 Raft 節點須要從 Leader 節點傳輸過來不一樣的日誌數量。在禁用 Snapshot 狀況下,新 Raft 節點須要把 Leade 節點內從起始的 T1 時刻至當前 T3 時刻這一時間範圍內的全部日誌都從新傳至本地後提交給狀態機。而在開啓 Snapshot 狀況下,新 Raft 節點則無需像 圖1 中那麼逐條複製 T1~T3 時刻內的全部日誌,而只需先從 Leader 節點加載最新的鏡像文件 Snapshot_Index_File 至本地,而後僅複製 T3 時刻之後的日誌至本地並提交狀態機便可。
在這裏可能有同窗會有疑問:「在 圖 1 中,從 Leader 節點傳給新擴容的 Raft 節點的數據是 T1~T3 的日誌,而 圖2 中取而代之的是 Snapshot_Index_File 快照鏡像文件,彷佛仍是不可避免額外的數據傳輸麼?」仔細看下圖 2,會發現其中 Snapshot_Index_File 快照鏡像文件是對 T1~T3 時刻內日誌數據指令的合併(包括數集合[Add 1,Add 6,Add 4,Sub 3,Sub 4,Add 3]),也即爲最終的數據狀態值。
若是用戶需開啓 SOFAJRaft 的 Snapshot 機制,則須要在其客戶端中設置配置參數類 NodeOptions 的「snapshotUri」屬性(即爲:Snapshot 文件的存儲路徑),配置該屬性後,默認會啓動一個定時器任務(「JRaft-SnapshotTimer」)自動去完成 Snapshot 操做,間隔時間經過配置類 NodeOptions 的「snapshotIntervalSecs」屬性指定,默認 3600 秒。定時任務啓動代碼以下:
從上面源碼中能夠看出,除了依靠定時任務觸發之外,SOFAJRaft 也支持用戶實現自定義的 Closure 類的回調方法,經過 Node 接口主動觸發 Snapshot,並將結果經過 Closure 回調。示例代碼以下:
同時,用戶在繼承並實現業務狀態機類「StateMachineAdapter」(該類爲抽象類)時候須要,一併實現其中的 onSnapshotSave()/onSnapshotLoad()
方法:
這裏須要注意的是,上面的 onSnapshotSave()
和 onSnapshotLoad()
方法均會阻塞 Raft 節點自己的狀態機,應該儘可能經過異步或其餘方式進行優化,避免出現阻塞的狀況。對於 onSnapshotSave()
方法,須要在保存快照文件後調用傳入的參數 closure.run(status)
通知調用者保存成功或者失敗;具體的應用實踐示例,能夠參考 github 上的 Counter 計數器示例。
Counter 計數器示例:https://www.sofastack.tech/projects/sofa-jraft/counter-example/
上一節 handleSnapshotTimeout
方法的關鍵代碼爲最後一行 doSnapshot(null)
方法,深刻代碼後發現,最終調用的是 Snapshot 執行器(SnapshotExecutor)的 doSnapshot(final Closure done)
方法。順着這條源碼線路,接下來看最爲核心的 SnapshotExecutor 快照執行器實現類:SnapshotExecutorImpl,並推出 Raft 節點生成快照、安裝快照和加載快照的總體的框架結構圖。
SOFAJRaft 中 Snapshot 機制的核心類是 SnapshotExecutorImpl。這個 SnapshotExecutor 快照執行器的核心方法是 doSnapshot(...)
和 installSnapshot(...)
:
doSnapshot(...)
方法:該方法用於生成 Raft 節點的快照文件。在該方法中,要先完成如下幾個前置狀態的校驗和檢查:
在完成上面的狀態校驗和檢查後,SOFAJRaft 調用了業務狀態機實現的 onSnapshotSave()
方法,這裏調用者能夠經過參數傳入的參數 closure.run(status)
通知本身保存 Snapshot 文件成功或者失敗。該方法具體的源代碼以下:
installSnapshot(...)
方法:該方法主要適用於 SOFAJRaft 集羣中的 Follower 角色節點,在收到從 Leader 節點發送過來的安裝 Snapshot 的 RPC 請求後,先會對當前節點的狀態作一些前置狀態的校驗(這一點跟上面的 doSnapshot(...)
方法同樣):
在完成上面的狀態校驗和檢查後,SOFAJRaft 在 loadDownloadingSnapshot()
中,調用了業務狀態機實現的 onSnapshotLoad()
方法。該方法具體的源代碼以下:
結合上文對 SnapshotExecutor 快照執行器兩個核心方法的解讀,能夠推出 Raft 節點生成快照、安裝快照和加載快照的總體的框架結構圖:
圖3 生成快照/安裝快照/加載快照框架圖
從上面的總體流程框架圖中能夠看到,在新擴容的 Raft 節點啓動後(它爲 Follower 角色),它獲取到 Leader 節點發送的安裝 Snapshot 的 RPC 請求(InstallSnapshotRequest)後,會在 T1 時刻先調用 SnapshotExecutor 執行器的 installSnapshot()
方法,本地生成如上圖所示的「snapshot_1」數據文件。
而後,該 Follower 節點從 T2 時刻開始繼續執行 SOFAJRaft 的日誌複製流程,從 Leader 節點接收到後續的 LogEntry 日誌文件(如上圖所示的 [Add 5,Sub 2,Add 1] 日誌數據集合)。
最後,在 T3 時刻,該 Follower 節點,調用 SnapshotExecutor 執行器的 doSnapshot()
方法,合併日誌數據集合並生成如上圖所示的「snapshot_2」文件,同時會對以前的日誌進行一個裁剪。具體的作法是,本地清理刪除上圖中從「snapshot_1」文件最後的 index+1 位置前的日誌。
有讀者朋友可能會問裁剪日誌時,爲何不刪除從「snapshot_2」文件最後的 index+1 位置前的日誌?這裏考慮到的主要緣由是,在Raft集羣中, Leader 和 Follower 節點間作日誌複製時,極可能會存在有部分 Follower 節點沒有徹底跟上 Leader 節點的狀況,若是此時 Leader 節點裁剪了從「snapshot_2」文件最後的 index+1 位置前的日誌,那剩餘未完成日誌複製的 Follower 節點就沒法從 Leader 節點同步日誌,而只能經過 Leader 發送過來的 installSnapshotRequest 來完成同步最新的狀態了(感興趣的同窗能夠參考着研究下 SOFAJRaft 源碼 LogManagerImpl 類的 setSnapshot()
方法實現)。
本文圍繞 Snapshot 機制的概念、特色和原理,結合 SOFAJRaft 的 Snapshot 機制的實現細節詳細闡述了 SOFAJRaft-Snapshot 基本流程,介紹了 Snapshot 的實踐應用,並剖析用戶的業務系統如何使用 SOFAJRaft-Snapshot 機制解決 Raft 日誌體積增長佔用磁盤空間和節點重啓時重放全部日誌過多佔用網絡帶寬資源的問題。
本篇是《剖析 | SOFAJRaft 實現原理》系列的最後一篇,感謝 SOFAStack 社區的核心貢獻者們的編寫,也歡迎更多感興趣的技術同窗加入,項目地址:SOFAJRaft:https://gitee.com/sofastack/sofa-jraft
歡迎閱讀原理解析系列,系統學習 SOFAJRaft 並讓它幫助到你的項目: