SOFAStack( Scalable Open Financial Architecture Stack)
是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。git
本文爲《剖析 | SOFAJRaft 實現原理》第六篇,本篇做者徐家鋒,來自專偉信息,力鯤,來自螞蟻金服。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:<SOFA:JRaftLab/>
,文章尾部有參與方式,歡迎一樣對源碼熱情的你加入。github
SOFAJRaft :https://github.com/sofastack/sofa-jraft算法
本文的目的是要介紹 SOFAJRaft 在日誌複製中所採用的 pipeline 機制,可是做者落筆時忽然以爲這個題目有些唐突,咱們不該該假設讀者理所應當的對日誌複製這個概念已經瞭然於胸,因此做爲一篇解析,我以爲仍是應該先介紹一下 SOFAJRaft 中的日誌複製是要解決什麼問題。服務器
SOFAJRaft 是對 Raft 共識算法的 Java 實現。既然是共識算法,就不可避免的要對須要達成共識的內容在多個服務器節點之間進行傳輸,在 SOFAJRaft 中咱們將這些內容封裝成一個個日誌塊 (LogEntry),這種服務器節點間的日誌傳輸行爲在 SOFAJRaft 中也就有了專門的術語:日誌複製。網絡
爲了便於閱讀理解,咱們用一個象棋的故事來類比日誌複製的流程和可能遇到的問題。架構
假設咱們穿越到古代,要爲一場即將舉辦的象棋比賽設計直播方案。固然全部電子通信技術此時都已經不可用了,幸虧象棋比賽是一種能用精簡的文字描述賽況的項目,好比:「炮二平五」, 「馬8進7」, 「車2退3」等,咱們將這些描述性文字稱爲棋譜。這樣只要咱們在場外一樣擺上棋盤 (可能很大,方便圍觀),經過棋譜就能夠把棋手的對弈過程直播出來。併發
圖1 - 經過棋譜直播框架
因此咱們的直播方案就是:賽場內兩位棋手正常對弈,設一個專門的記錄員來記錄棋手走出的每一步,安排一個旗童飛奔於賽場內外,棋手每走一步,旗童就將其以棋譜的方式傳遞給場外,這樣觀衆就能在場外準實時的觀看對弈的過程,得到同觀看直播相同的體驗。分佈式
圖2 - 一個簡單的直播方案性能
這即是 SOFAJRaft 日誌複製的人肉版,接下來咱們完善一下這個「直播系統」,讓它逐步對齊真實的日誌複製。
假設咱們的比賽得到了很高的關注度,咱們須要在賽場外擺出更多的直播場地以供更多的觀衆觀看。
這樣咱們就要安排更多的旗童來傳遞棋譜,場外的每一臺直播都須要一個旗童來負責,這些旗童不停的在賽場內外奔跑傳遞棋譜信息。有的直播平臺離賽場遠一些,旗童要跑好久才行,相應的直播延遲就會大一些,而有些直播平臺離得很近,對應的旗童就能很快的將對弈狀況同步到直播。
隨着直播場地的增長,負責記錄棋局的記錄員的壓力就會增長,由於他要針對不一樣的旗童每次提供不一樣的棋譜內容,有的慢有的快。若是記錄員一旦記混了或者眼花了,就會出現嚴重的直播事故(觀衆看到的再也不是棋手真正的棋局)。
圖4 - 壓力很大的記錄員
爲此咱們要做出一些優化,爲每一個場外的直播平臺安排一個專門的記錄員,這樣 「賽局-記錄員-旗童-直播局」 就構成了單線模式,專人專職高效可靠。
圖5 - 「賽局-記錄員-旗童-直播棋局」
起初咱們要求棋手每走一步,旗童就向外傳遞一次棋譜。但是隨着比賽進行,其弊端也逐漸顯現,一方面記錄員記錄了不少棋局信息沒有傳遞出去,以致於不得不請求棋手停下來等待 (難以想象);另外一方面,場外的觀衆對於這種「卡幀」的直播模式也很不滿意。
因此咱們作出改進,要求旗童每次多記幾步棋,這樣記錄員不會積攢太多的待直播信息,觀衆也能一次看到好幾步,而這對於聰明的旗童來講並非什麼難事,如此改進達到了雙贏的局面。
圖6 - 旗童批量攜帶信息
棋局愈發精彩,應棋迷的強烈要求,咱們臨時增長了幾個直播場地,這時棋手已經走了不少步了,按照咱們的常規手段,負責新直播的記錄員和旗童須要把過去的每一步都在直播棋盤上還原一遍(回放的過程),與此同時棋手還在不斷下出新的內容。
從直覺上來講這也是一種很不聰明的方式,因此這時咱們採用快照模式,再也不要求旗童傳遞過去的每一步棋譜,而是把當前的棋局圖直接描下來,旗童將圖帶出去後,按照圖譜直接擺子。這樣新直播平臺就能快速追上棋局進度,讓觀衆欣賞到賽場同步的棋局對弈了。
圖7 - 採用快照模式
雖然咱們以前已經在改進 2 中增長了旗童每次攜帶的信息量,可是在一些狀況下(棋手下快棋、直播平臺很遠等),記錄員依然沒法將信息及時同步給場外。這時咱們須要增長多個旗童,各旗童有次序的將信息攜帶到場外,這樣記錄員就能夠更快速的把信息同步給場外直播平臺。
圖8 - 利用多個旗童傳遞信息,實現 pipeline 效果
如今這我的肉的直播平臺在咱們的逐步改進下已經具有了 SOFAJRaft 日誌複製的下面幾個主要特色:
若是棋譜傳遞的順序不同,最後下出的棋局可能也是徹底不一樣的。而 SOFAJRaft 在日誌複製時,其日誌傳輸的順序也要保證嚴格的順序,全部日誌既不能亂序也不能有空洞 (也就是說不能被漏掉)。
圖9 - 日誌保持嚴格有序且連續
SOFAJRaft 中 Leader 節點會同時向多個 Follower 節點複製日誌,在 Leader 中爲每個 Follower 分配一個 Replicator,專用來處理複製日誌任務。在棋局中咱們也針對每一個直播平臺安排一個記錄員,用來將對弈棋譜同步給對應的直播平臺。
圖10 - 併發複製日誌
SOFAJRaft 中 Leader 節點會將日誌成批的複製給 Follower,就像旗童會每次攜帶多步棋信息到場外。
圖11 - 日誌被批量複製
在改進 3 中,咱們讓新加入的直播平臺直接複製當前的棋局,而再也不回放過去的每一步棋譜,這就是 SOFAJRaft 中的快照 (Snapshot) 機制。用 Snapshot 可以讓 Follower 快速跟上 Leader 的日誌進度,再也不回放很早之前的日誌信息,即緩解了網絡的吞吐量,又提高了日誌同步的效率。
在改進 4 中,咱們讓多個旗童參與信息傳遞,這樣記錄員和直播平臺間就能夠以「流式」的方式傳遞信息,這樣既能保證信息傳遞有序也能保證信息傳遞持續。
在 SOFAJRaft 中咱們也有相似的機制來保證日誌複製流式的進行,這種機制就是 pipeline。Pipeline 使得 Leader 和 Follower 雙方再也不須要嚴格聽從 「Request - Response - Request」 的交互模式,Leader 能夠在沒有收到 Response 的狀況下,持續的將複製日誌的 AppendEntriesRequest 發送給 Follower。
在具體實現時,Leader 只須要針對每一個 Follower 維護一個隊列,記錄下已經複製的日誌,若是有日誌複製失敗的狀況,就將其後的日誌重發給 Follower。這樣就能保證日誌複製的可靠性,具體細節咱們在源碼解析中再談。
圖12 - 日誌複製的 pipeline 機制
上面就是日誌複製在原理層面的介紹,而在代碼實現中主要是由 Replicator
和 NodeImpl
來分別實現 Leader 和 Follower 的各自邏輯,主要的方法列於下方。在處理源碼中有三點值得咱們關注。
圖13 - 相關的方法
圖14 - Replicator 的狀態
Leader 節點在經過 Replicator 和 Follower 創建鏈接以後,要發送一個 Probe 類型的探針請求,目的是知道 Follower 已經擁有的的日誌位置,以便於向 Follower 發送後續的日誌。
圖15 - 發送探針來知道 follower 的 logindex
Inflight 是對批量發送出去的 logEntry 的一種抽象,他表示哪些 logEntry 已經被封裝成日誌複製 request 發送出去了。
圖16 - Inflight 結構
Leader 維護一個 queue,每發出一批 logEntry 就向 queue 中 添加一個表明這一批 logEntry 的 Inflight,這樣當它知道某一批 logEntry 複製失敗以後,就能夠依賴 queue 中的 Inflight 把該批次 logEntry 以及後續的全部日誌從新複製給 follower。既保證日誌複製可以完成,又保證了複製日誌的順序不變。
這部分從邏輯上來講比較清晰,可是代碼層面須要考慮的東西比較多,因此咱們在此處貼出源碼,讀者能夠在源碼中繼續探索。
圖17 - 複製日誌的主要方法
圖18 - 添加 Inflight 到隊列中
固然在日誌複製中其實還要考慮更加複雜的狀況,好比一旦發生切換 leader 的狀況,follower 該如何應對,這些問題但願你們可以進入源碼來尋找答案。
在 pipeline 機制中,雖然咱們在 SOFAJRaft 層面經過 Inflight 隊列保證了日誌是被有序的複製,對於亂序傳輸的 LogEntry 經過各類異常流程去排除掉,可是這些被排除掉的亂序日誌最終仍是要經過重傳來保證最終成功,這就會影響日誌複製的效率。
圖19 - 通訊層不能保證有序
如上圖所示,發送端的 Connection Pool 和 接收端的 Thread Pool 都會讓本來「單行道」上有序傳輸的日誌進入「多車道」,於是沒法保證有序。因此在通訊層面 SOFAJRaft 作了兩部分優化去儘可能保證 LogEntry 在傳輸中不會亂序。
圖20 - 經過 uniqueKey 定製 Url
_dispatch_msg_list_in_default_executor_
使得咱們能夠經過 io 線程直接將任務投遞到 Processor 中。咱們對 SOFABolt 作過一些功能加強,這裏提供相關 PR #84 ,有興趣的讀者能夠前往瞭解。
圖21 - SOFABolt 利用 IO 線程派發 AppendEntriesRequest 到 Processor
這樣日誌複製的通訊模型就變成了咱們指望的「單行道」的模式。這種「單行道」可以很大程度上保證傳輸的日誌是有序且連續的,從而提高了 pipeline 的效率。
圖22 - 優化通訊模型
日誌複製並非一個複雜的概念,pipeline 機制也是一種符合直覺思惟的優化方式,甚至在咱們的平常生活中也能找到這些概念的實踐。在 SOFAJRaft 中,日誌複製的真正難點是如何在分佈式環境下既考慮到各類細節和異常,又保證高性能。本文只是從概念上嘗試介紹了日誌複製,更多的細節還需讀者進入代碼去尋找答案。