從一個例子開始體驗 SOFAJRaft | SOFAChannel#8 直播整理

SOFA:Channel/,有趣實用的分佈式架構頻道。git

本文根據 SOFAChannel#8 直播分享整理,主題:從一個例子開始體驗 SOFAJRaft。 回顧視頻以及 PPT 查看地址見文末。 歡迎加入直播互動釘釘羣:23390449,不錯過每場直播。github

channel8-banner

你們好,我是力鯤,來自螞蟻金服, 如今是 SOFAJRaft 的開源負責人。今天分享主題是《從一個例子開始體驗 SOFAJRaft》,其實從這個題目你們也能看出來,今天是要從一個用戶而非 owner 的視角來了解 SOFAJRaft。這麼設計題目的緣由是 SOFAJRaft 做爲一種共識算法的實現,涉及到了一些概念和術語,而這些內容更適合經過一系列文章進行闡述,而在直播中咱們但願可以分享對用戶更有用、更容易理解的信息——SOFAJRaft 是什麼,以及咱們怎麼去用它。算法

首先介紹一下 SOFAJRaft 的背景知識,接下來講說這個例子源於什麼需求,第三部分是架構的選型,第四部分來看看咱們如何使用 SOFAJRaft,最後運行代碼,看看 SOFAJRaft 是如何支撐業務運行的。性能優化

歡迎加入社區成爲 Contributor,SOFAJRaft服務器

Raft 共識算法

Raft 是一種共識算法,其特色是讓多個參與者針對某一件事達成徹底一致:一件事,一個結論。同時對已達成一致的結論,是不可推翻的。能夠舉一個銀行帳戶的例子來解釋共識算法:假如由一批服務器組成一個集羣來維護銀行帳戶系統,若是有一個 Client 向集羣發出「存 100 元」的指令,那麼當集羣返回成功應答以後,Client 再向集羣發起查詢時,必定可以查到被存儲成功的這 100 元錢,就算有機器出現不可用狀況,這 100 元的帳也不可篡改。這就是共識算法要達到的效果。網絡

Raft 算法和其餘的共識算法相比,又有了以下幾個不一樣的特性:架構

  • Strong leader:Raft 集羣中最多隻能有一個 Leader,日誌只能從 Leader 複製到 Follower 上;
  • Leader election:Raft 算法採用隨機選舉超時時間觸發選舉來避免選票被瓜分的狀況,保證選舉的順利完成;
  • Membership changes:經過兩階段的方式應對集羣內成員的加入或者退出狀況,在此期間並不影響集羣對外的服務;

共識算法有一個很典型的應用場景就是複製狀態機。Client 向複製狀態機發送一系列可以在狀態機上執行的命令,共識算法負責將這些命令以 Log 的形式複製給其餘的狀態機,這樣不一樣的狀態機只要按照徹底同樣的順序來執行這些命令,就能獲得同樣的輸出結果。因此這就須要利用共識算法保證被複制日誌的內容和順序一致。併發

圖1 - 複製狀態機
圖1 - 複製狀態機

SOFAJRaft

SOFAJRaft 是基於 Raft 算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP。應用場景有 Leader 選舉、分佈式鎖服務、高可靠的元信息管理、分佈式存儲系統。app

圖2 - SOFAJRaft 結構
圖2 - SOFAJRaft 結構

這張圖就是 SOFAJRaft 的設計圖,Node 表明了一個 SOFAJRaft Server 節點,這些方框表明他內部的各個模塊,咱們依然用以前的銀行帳戶系統舉例來講明 SOFAJRaft 的各模塊是如何工做的。分佈式

當 Client 向 SOFAJRaft 發來一個「存 100 元」的命令以後,Node 的 Log 存儲模塊首先將這個命令以 Log 的形式存儲到本地,同時 Replicator 會把這個 Log 複製給其餘的 Node,Replicator 是有多個的,集羣中有多少個 Follower 就會有多少個 Replicator,這樣就能實現併發的日誌複製。當 Node 收到集羣中半數以上的 Follower 返回的「複製成功」 的響應以後,就能夠把這條 Log 以及以前的 Log 有序的送到狀態機裏去執行了。狀態機是由用戶來實現的,好比咱們如今舉的例子是銀行帳戶系統,因此狀態機執行的就是帳戶金額的借貸操做。若是 SOFAJRaft 在別的場景中使用,狀態機就會有其餘的執行方式。

Snapshot 是快照,所謂快照就是對數據當前值的一個記錄,Leader 生成快照有這麼幾個做用:

  • 當有新的 Node 加入集羣的時候,不用只靠日誌複製、回放去和 Leader 保持數據一致,而是經過安裝 Leader 的快照來跳過早期大量日誌的回放;
  • Leader 用快照替代 Log 複製能夠減小網絡上的數據量;
  • 用快照替代早期的 Log 能夠節省存儲空間;

圖3 - 須要用戶實現:StateMachine、Client
圖3 - 須要用戶實現:StateMachine、Client

SOFAJRaft 須要用戶去實現兩部分:StateMachine 和 Client。

由於 SOFAJRaft 只是一個工具,他的目的是幫助咱們在集羣內達成共識,而具體要對什麼業務邏輯達成共識是須要用戶本身去定義的,咱們將用戶須要去實現的部分定義爲 StateMachine 接口。好比帳務系統和分佈式存儲這兩種業務就須要用戶去實現不一樣的 StateMachine 邏輯。而 Client 也很好理解,根據業務的不一樣,用戶須要去定義不一樣的消息類型和客戶端的處理邏輯。

圖4 - 須要用戶實現一些接口
圖4 - 須要用戶實現一些接口

前面介紹了這麼多,咱們引出今天的主題:如何用 SOFAJRaft 實現一個分佈式計數器?

需求

咱們的需求其實很簡單,用一句話來講就是:提供一個 Counter,Client 每次計數時能夠指定步幅,也能夠隨時發起查詢。

咱們對這個需求稍做分析後,將它翻譯成具體的功能點,主要有三部分:

  • 實現:Counter server,具有計數功能,具體運算公式爲:Cn = Cn-1 + delta;
  • 提供寫服務,寫入 delta 觸發計數器運算;
  • 提供讀服務,讀取當前 Cn 值;

除此以外,咱們還有一個可用性的可選需求,須要有備份機器,讀寫服務不能不可用。

系統架構

根據剛纔分析出來的功能需求,咱們設計出 1.0 的架構,這個架構很簡單,一個節點 Counter Server 提供計數功能,接收客戶端發起的計數請求和查詢請求。

圖5 - 架構 1.0
圖5 - 架構 1.0

可是這樣的架構設計存在這樣兩個問題:一是 Server 是一個單點,一旦 Server 節點故障服務就不可用了;二是運算結果都存儲在內存當中,節點故障會致使數據丟失。

圖6 - 架構 1.0 的不足:單點
圖6 - 架構 1.0 的不足:單點

針對第二個問題,咱們優化一下,加一個本地文件存儲。這樣每次計數器完成運算以後都將數據落盤,當節點故障之時,咱們要新起一臺備用機器,將文件數據拷貝過來,而後接替故障機器對外提供服務。這樣就解決了數據丟失的風險,可是同時也引來另外的問題:磁盤 IO 很頻繁,同時這種冷備的模式也依然會致使一段時間的服務不可用。

圖7 - 架構 1.0 的不足:冷備
圖7 - 架構 1.0 的不足:冷備

因此咱們提出架構 2.0,採用集羣的模式提供服務。咱們用三個節點組成集羣,由一個節點對外提供服務,當 Server 接收到 Client 發來的寫請求以後,Server 運算出結果,而後將結果複製給另外兩臺機器,當收到其餘全部節點的成功響應以後,Server 向 Client 返回運算結果。

圖8 - 架構 2.0
圖8 - 架構 2.0

可是這樣的架構也存在這問題:

  • 咱們選擇哪一臺 Server 扮演 Leader 的角色對外提供服務;
  • 當 Leader 不可用以後,選擇哪一臺接替它;
  • Leader 處理寫請求的時候須要等到全部節點都響應以後才能響應 Client;
  • 也是比較重要的,咱們沒法保證 Leader 向 Follower 複製數據是有序的,因此任一時刻三個節點的數據均可能是不同的;

保證複製數據的順序和內容,這就有了共識算法的用武之地,因此在接下來的 3.0 架構裏,咱們使用 SOFAJRaft 來助力集羣的實現。

圖8 - 架構 3.0:使用 SOFAJRaft
圖8 - 架構 3.0:使用 SOFAJRaft

3.0 架構中,Counter Server 使用 SOFAJRaft 來組成一個集羣,Leader 的選舉和數據的複製都交給 SOFAJRaft 來完成。在時序圖中咱們能夠看到,Counter 的業務邏輯從新變得像架構 1.0 中同樣簡潔,維護數據一致的工做都交給 SOFAJRaft 來完成,因此圖中灰色的部分對業務就不感知了。

圖9 - 架構 3.0:時序圖
圖9 - 架構 3.0:時序圖

在使用 SOFAJRaft 的 3.0 架構中,SOFAJRaft 幫咱們完成了 Leader 選舉、節點間數據同步的工做,除此以外,SOFAJRaft 只須要半數以上節點響應便可,再也不須要集羣全部節點的應答,這樣能夠進一步提升寫請求的處理效率。

圖10 - 架構 3.0:SOFAJRaft 實現 Leader 選舉、日誌複製
圖10 - 架構 3.0:SOFAJRaft 實現 Leader 選舉、日誌複製

使用 SOFAJRaft

那麼怎麼使用 SOFAJRaft 呢?咱們以前說過,SOFAJRaft 主要暴露了兩個地方給咱們去實現,一是 Cilent,另外一個是 StateMachine,因此咱們的計數器也就是要去作這兩部分。

在 Client 上,咱們要定義具體的消息類型,針對不一樣的消息類型,還須要去實現消息的 Processor 來處理這些消息,接下來這些消息就交給 SOFAJRaft 去完成集羣內部的數據同步。

在 StateMachine 上,咱們要去實現狀態機暴露給咱們待實現的幾個接口,最重要的是 onApply 接口,要在這個接口裏將 Cilent 的請求指令進行運算,轉換成具體的計數器值。而 onSnapshotSave 和 onSnapshotLoad 接口則是負責快照的生成和加載。

圖11 - 模塊關係
圖11 - 模塊關係

下面這張圖是最終實現的模塊關係圖,其實他已是代碼實現以後的產物了,在這裏並無貼出具體的代碼,由於代碼已經隨咱們的項目一塊兒開源了。咱們實現了兩種消息類型 IncrementAndGetRequest 和 GetValueRequest,分別對應寫請求和讀請求,由於兩種請求的響應都是計數器的值,因此同用一個 ValueResponse。兩種請求,因此對應兩種 Processor:IncrementAndGetRequestProcessor 和 GetValueRequestProcessor,狀態機 CounterStateMachine 實現了以前提到的三個接口,除此以外還實現了 onLeaderStart 和 onLeaderStop,用來在節點成爲 leader 和失去 leader 資格時作一些處理。這個地方在寫請求的處理中使用了 IncrementAndAddClosure ,這樣就能夠經過 callback 的方式來實現響應。

圖12 - 類關係圖
圖12 - 類關係圖

啓動運行

來看看整個的啓動過程。首先來看 Follower 節點的啓動 (固然,在啓動以前,咱們並不知道哪一個節點會是 Leader),Counter 在本地起三個進程用來模擬三個節點,它們分別使用 808一、808二、8083 三個端口,標記其爲 A、B、C 節點。

A 節點率先啓動,而後開始向 B 和 C 發送 preVote 請求,可是這時候另外兩個節點都還沒有啓動,因此 A 節點通訊失敗,而後等待,再重試,如此往復。在 A 節點某次通訊失敗後的等待之中,它忽然收到了 B 節點發來的 preVote 請求,在通過一系列 check 以後,它承認了這個 preVote 請求,而且返回成功響應,隨後又對 B 節點發來的 vote 請求成功響應,而後咱們能夠看到,B 節點成功當選 Leader。這就是 Follower A 的啓動、投票過程。

圖13 - Follower 啓動日誌
圖13 - Follower 啓動日誌

咱們再看看 B 節點的啓動,B 節點在啓動以後,恰好處於 A 節點的一次等待間隙之中,因此它沒有收到其餘節點發來的 preVote 請求,所以它向另外兩個節點發起了 preVote 請求,試圖競選。接下來它收到了 A 節點發來的確認響應,接着 B 節點又發起了 vote 請求,依然收到了 A 節點的響應。這樣 B 節點就收到了超過集羣半數以上的投票併成功當選 (A 節點和 B 節點本身,達到 2/3) 。在此過程當中,C 節點一直沒有啓動,可是因爲 A 和 B 構成半數以上,因此共識算法已經能夠正常 work。

圖14 - Leader 啓動日誌
圖14 - Leader 啓動日誌

在剛纔的過程當中,咱們提到了兩個關鍵詞:preVote 和 vote,這是選舉中的兩個階段,之因此要設置 preVote,是爲了應對網絡分區的狀況。關於 SOFAJRaft 的選舉,咱們有專門的文章去解析 ,你們能夠進一步瞭解。在這裏咱們將選舉的評選原則粗略的描述爲:哪一個節點保存的日誌最新最完整,它就更有資格成爲 leader。

接下來咱們看看 Client 發起的一次寫請求。Client 共發起了三次寫請求,分別是 "+0"、"+1"、"+2"。從日誌上咱們能夠看到,Leader 在收到這些請求以後,先把他們以日誌的形式發送給其餘節點 (而且是批量的),當它收到其餘節點對日誌複製的成功響應以後,再更新 committedIndex,最後調用 onApply 接口,執行 counter 的計數運算,將 client 發來的指令加到計數器當中。在這個過程當中,能夠看到 Leader 在處理寫請求的時候一個很重要的步驟就是將日誌複製給其餘節點。來詳細看下這個過程,以及當中提到的 committedIndex。

圖15 - Leader 處理寫請求
圖15 - Leader 處理寫請求

CommittedIndex 標誌了一個位點,它標誌在此以前的全部日誌都已經複製到了集羣半數以上的節點之中。圖中能夠看到,committedIndex 初始指在 "3" 這個位置上,表示 "0-3" 的日誌都已經複製到了半數以上節點之中 (在 Follower 上咱們也已經看到),接下來 Leader 又把 "4"、"5" 兩條日誌批量的複製到了 Follower 上,這是就能夠把 committedIndex 右滑動到 "5" 的位置,表示 "0-5" 的日誌都已經複製到了半數以上節點之中。

圖16 - 日誌複製
圖16 - 日誌複製

這時又產生了另外一個問題:咱們如何知道 StateMachine 執行到哪一條日誌了?經過 committedIndex 咱們能夠知道哪些日誌已經成功複製到集羣其餘節點之中了,可是 StateMachine 中此刻的狀態表明哪一條日誌執行以後的結果呢?這就要用 applyIndex 來表示。在圖中,applyIndex 指向 "3",這表示:"0-3" 的日誌表明的指令都已經被 StateMachine 執行,狀態機此刻的狀態表明 "3" 日誌執行完畢以後的結果,當 committedIndex 向右滑動以後,applyIndex 就能夠伴隨狀態機的執行繼續向右滑動了。ApplyIndex 和 committedIndex 就能夠支持線性一致性讀,關於這個概念,咱們也已經有文章去專門解析了,能夠在文末連接中瞭解。

圖17 - ApplyIndex 更新
圖17 - ApplyIndex 更新

小結

今天以 Counter 爲例,先介紹了 SOFAJRaft 的概念,而後從需求提出開始,一步步完善架構,明確業務要實現哪些接口,最後啓動日誌觀察 SOFAJRaft 如何支撐業務執行。在此過程當中涉及到了一些 SOFAJRaft 的在直播中沒有繼續深刻的概念,也給出了相關的解析文章。

若是你們對於 Counter 例子還想要有更多的瞭解,歡迎在官網瀏覽相關文章 ,或者在項目中查看具體代碼

文中提到的相關連接

本期視頻回顧以及 PPT 查看地址

tech.antfin.com/community/l…

往期直播精彩回顧

公衆號:金融級分佈式架構(Antfin_SOFA)

相關文章
相關標籤/搜索