1. 程式人生 > >SOFAJRaft Snapshot 原理剖析 | SOFAJRaft 實現原理

SOFAJRaft Snapshot 原理剖析 | SOFAJRaft 實現原理

SOFAStackScalable Open Financial Architecture Stack)是螞蟻金服自主研發的金融級分散式架構,包含了構建金融級雲原生架構所需的各個元件,是在金融場景裡錘鍊出來的最佳實踐。

SOFAJRaft Snapshot 原理剖析

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

本文為《剖析 | SOFAJRaft 實現原理》最後一篇,本篇作者胡宗棠,來自中國移動。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和原始碼愛好者們出品,專案代號:SOFA:JRaftLab/,文末包含往期系列文章。

SOFAJRaft:https://gitee.com/sofastack/sofa-jraft

導讀

本文主要介紹 SOFAJRaft 在日誌複製和管理中所採用的快照機制。考慮到單獨介紹 SOFAJRaft 中的快照機制原理和實現或許有一些唐突,我會先通過一個讀者都能夠看得明白的例子作為切入點,讓大家對快照這個概念、它可以解決的主要問題,先有一個比較深刻的理解。

一、快照的概念與特點

SOFAJRaft 是對 Raft 共識演算法的 Java 實現。既然是共識演算法,就不可避免的要對需要達成共識的內容,在多個伺服器節點之間進行傳輸,一般將這些共識的內容稱之為日誌塊(LogEntry)。如果讀過《剖析 | SOFAJRaft 實現原理》系列前面幾篇文章的同學,應該瞭解到在 SOFAJRaft 中,可以通過“節點之間併發複製日誌”、“批量化複製日誌”和“複製日誌pipeline機制”等優化手段來保證伺服器節點之間日誌複製效率達到最大化。

但如果遇到下面的兩個場景,僅依靠上面的優化方法並不能有效地根本解決問題:

  1. 當對某個 SOFAJRaft Group 叢集以新增節點方式來擴容,新節點需要從當前的 Leader 中獲取所有的日誌並重放到本身的狀態機中,這對 Leader 和網路頻寬都會帶來不小的開銷,還有其他方法可以優化或解決這個問題麼?
  2. 因為伺服器節點需要儲存的日誌不斷增加,但是磁碟空間有限,除了對磁碟卷大小擴容外,還有其他方式來解決麼?

帶著上面兩個疑問,我們可以先來看一個大家日常生活中都會遇到的場景—重新安裝作業系統,然後再通俗易懂地為大家介紹快照的概念與特點。

有一天,你的膝上型電腦的 Windows 作業系統因為某一些原因出現啟動後多次崩潰問題,不管通過任何方式都沒辦法解決。這時候,我們想到解決問題的第一個方案就是為這臺電腦重新安裝作業系統。如果,我們平時偶爾為自己電腦的作業系統做過映象,直接用之前的映象檔案即可快速還原系統至之前的某一時間點的狀態,而無需從零開始安裝 Windows 作業系統後,再花大量時間來重新安裝一些自己所需要的系統軟體(比如 Chrome 瀏覽器、印象筆記和 FoxMail 郵件客戶端等)。

在上面的例子中,電腦作業系統的映象就是系統某一時刻的“快照”,因為它包含了這一時刻,系統當前狀態機的值(對於使用者來說,就是安裝了哪些的應用軟體)。在需要重新安裝作業系統時候,通過映象這一“快照”,可以很高效地完成還原電腦作業系統這個任務,而無需從零開始安裝系統和相應的應用軟體。所以,我們這裡可以為“快照”下一個簡單的定義:一種通過某種資料格式檔案來儲存系統當前的狀態值的一個副本。

“快照”的特點,就如同它字面意思一樣,可以分為“快”和“照”:

  • “快”:高效快捷,通過快照可以很方便的將系統還原至某一時刻的狀態;
  • “照”:通過快照機制是儲存系統某一時刻的狀態值;

二、SOFAJRaft 的 Snapshot 機制

2.1 SOFAJRaft Snapshot 機制的原理

讀到這裡,再去回顧第一節內容開頭提出的兩個問題,大家應該可以想到解決問題的方法就是通過引入快照機制。

1. 解決日誌複製與節點擴容的瓶頸問題

在 SOFAJRaft 中,Snapshot 為當前 Raft 節點狀態機的最新狀態打了一個“映象”單獨儲存,儲存成功後在這個時刻之前的日誌即可刪除,減少了日誌檔案在磁碟中的佔用空間。而在 Raft 節點啟動時,可以直接載入最新的  Snapshot 映象,直接重放在此之後的日誌檔案即可。如果設定儲存 Snapshot 的時間間隔比較合理,那麼節點載入映象後重放的日誌檔案較少,啟動速度也會比較快。對於新 Raft 節點加入某個 SOFAJRaft Group 叢集的場景,新節點可先從 Leader 節點上拷貝最新的 Snapshot 安裝到本地狀態機,然後拷貝後續的日誌資料即可,這樣可以在快速跟上整個 SOFAJRaft Group 叢集進度的同時,又不會佔用 Leader 節點較大的網路頻寬資源。

2. 解決 Raft 節點故障恢復中的時效問題

在一個正常執行的 SOFAJRaft Group 叢集中,當其中某一個 Raft 節點出現故障了(假設該故障的原因不是由磁碟損壞等不可逆因素導致的),該 Raft 節點修復故障重新啟動時,如果節點禁用 Snapshot 快照機制,那麼會重放所有本地的日誌到狀態機以跟上最新的日誌,這樣節點啟動和達到日誌備份完整的耗時均會比較長。但是,如果此時節點開啟了 Snapshot 快照機制,那麼一切就會變得非常高效,節點只需要載入最新的 Snapshot 至狀態機,然後以 Snapshot 資料的日誌為起點開始繼續回放日誌至狀態機,直到使得狀態機達到最新狀態。

在 Snapshot 禁用情況下叢集節點擴容

圖1 在 Snapshot 禁用情況下叢集節點擴容

image.png

圖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]),也即為最終的資料狀態值。

2.2 SOFAJRaft Snapshot 機制的實踐應用

如果使用者需開啟 SOFAJRaft 的 Snapshot 機制,則需要在其客戶端中設定配置引數類 NodeOptions 的“snapshotUri”屬性(即為:Snapshot 檔案的儲存路徑),配置該屬性後,預設會啟動一個定時器任務(“JRaft-SnapshotTimer”)自動去完成 Snapshot 操作,間隔時間通過配置類 NodeOptions 的“snapshotIntervalSecs”屬性指定,預設 3600 秒。定時任務啟動程式碼如下:

定時任務啟動程式碼

從上面原始碼中可以看出,除了依靠定時任務觸發以外,SOFAJRaft 也支援使用者實現自定義的 Closure 類的回撥方法,通過 Node 介面主動觸發 Snapshot,並將結果通過 Closure 回撥。示例程式碼如下:

snapshot closure 原始碼

同時,使用者在繼承並實現業務狀態機類“StateMachineAdapter”(該類為抽象類)時候需要,一併實現其中的  onSnapshotSave()/onSnapshotLoad()  方法:

  • onSnapshotSave() 方法:定期儲存 Snapshot;
  • onSnapshotLoad() 方法:啟動或者安裝 Snapshot 後加載 Snapshot;

這裡需要注意的是,上面的  onSnapshotSave()  和  onSnapshotLoad()  方法均會阻塞 Raft 節點本身的狀態機,應該儘量通過非同步或其他方式進行優化,避免出現阻塞的情況。對於  onSnapshotSave()  方法,需要在儲存快照檔案後呼叫傳入的引數 closure.run(status)  通知呼叫者儲存成功或者失敗;具體的應用實踐示例,可以參考 github 上的 Counter 計數器示例。

Counter 計數器示例:https://www.sofastack.tech/projects/sofa-jraft/counter-example/

2.3 SOFAJRaft Snapshot 原始碼簡析

上一節 handleSnapshotTimeout  方法的關鍵程式碼為最後一行  doSnapshot(null)  方法,深入程式碼後發現,最終呼叫的是 Snapshot 執行器(SnapshotExecutor)的  doSnapshot(final Closure done)  方法。順著這條原始碼線路,接下來看最為核心的 SnapshotExecutor 快照執行器實現類:SnapshotExecutorImpl,並推出 Raft 節點生成快照、安裝快照和載入快照的整體的框架結構圖。

SOFAJRaft 中 Snapshot 機制的核心類是 SnapshotExecutorImpl。這個 SnapshotExecutor 快照執行器的核心方法是  doSnapshot(...)  和  installSnapshot(...) :

 doSnapshot(...)  方法:該方法用於生成 Raft 節點的快照檔案。在該方法中,要先完成以下幾個前置狀態的校驗和檢查:

  • 是否處於 Stopped 狀態;
  • 是否正在載入另外一個 Snapshot 檔案;
  • 是否正在生成另外一個 Snapshot 檔案;
  • 當前業務狀態機已經提交的 Index 索引是否等於 Snapshot 最後儲存的日誌 Index 索引(如果兩個值相等則表示,業務資料沒有新增,無需再生成一次沒有意義的 Snapshot);

在完成上面的狀態校驗和檢查後,SOFAJRaft 呼叫了業務狀態機實現的  onSnapshotSave()  方法,這裡呼叫者可以通過引數傳入的引數  closure.run(status)  通知自己儲存 Snapshot 檔案成功或者失敗。該方法具體的原始碼如下:

 doSnapshot(...) 方法原始碼

 installSnapshot(...)  方法:該方法主要適用於 SOFAJRaft 叢集中的 Follower 角色節點,在收到從 Leader 節點發送過來的安裝 Snapshot 的 RPC 請求後,先會對當前節點的狀態做一些前置狀態的校驗(這一點跟上面的 doSnapshot(...) 方法一樣):

  • 是否處於 Stopped 狀態;
  • 是否正在生成 Snapshot 檔案;
  • 節點的 term 值是否跟 RPC 請求的 term 值一致;
  • Leader 節點發送過來的待安裝 Snapshot 檔案中的資料是否為最新的;
  • 是否正在安裝前面的 Snapshot 檔案;

在完成上面的狀態校驗和檢查後,SOFAJRaft 在  loadDownloadingSnapshot()  中,呼叫了業務狀態機實現的  onSnapshotLoad()  方法。該方法具體的原始碼如下:

 installSnapshot(...) 方法原始碼

結合上文對 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 原始碼解析系列閱讀

本篇是《剖析 | SOFAJRaft 實現原理》系列的最後一篇,感謝 SOFAStack 社群的核心貢獻者們的編寫,也歡迎更多感興趣的技術同學加入,專案地址:SOFAJRaft:https://gitee.com/sofastack/sofa-jraft

歡迎閱讀原理解析系列,系統學習 SOFAJRaft 並讓它幫助到你的專案: