詳解螞蟻金服 SOFAJRaft | 生產級高性能 Java 實現

SOFAStackgit

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


本文根據 SOFA Meetup#1 北京站 現場分享整理,完整的分享 PPT 獲取方式見文章底部。算法


前言

SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。SOFAJRaft 是從百度的 braft 移植而來,作了一些優化和改進,感謝百度 braft 團隊開源瞭如此優秀的 C++ Raft 實現。編程

GitHub 地址:github.com/alipay/sofa…安全

以前,咱們有一篇介紹 SOFAJRaft 的文章,可在文末得到連接,延續這個內容,今天的演講分爲三部分,先簡要介紹 Raft 算法,而後介紹 SOFAJRaft 的設計,最後說說它的優化。性能優化

sofa-23.png

分享嘉賓:力鯤 螞蟻金服 SOFAJRaft 核心成員服務器


Raft 共識算法

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

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

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

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


Leader 選舉

複製狀態機集羣在利用 Raft 算法保證一致性時,要作的第一件事情就是 Leader 選舉。在講 Leader 選舉以前咱們先要說一個重要的概念:Term。Term 用來將一個連續的時間軸在邏輯上切割成一個個區間,它的含義相似於「美國第 26 屆總統」這個表述中的「26」。

每個 Term 期間集羣要作的第一件事情就是選舉 Leader。起初全部的 Server 都是 Follower 角色,若是 Follower 通過一段時間( election timeout )的等待卻依然沒有收到其餘 Server 發來的消息時,Follower 就能夠認爲集羣中沒有可用的 Leader,遂開始準備發起選舉。在發起選舉的時候 Server 會從 Follower 角色轉變成 Candidate,而後開始嘗試競選 Term + 1 屆的 Leader,此時他會向其餘的 Server 發送投票請求,當收到集羣內多數機器贊成其當選的應答以後,Candidate 成功當選 Leader。可是以下兩種狀況會讓 Candidate 退回 (step down) 到 Follower,放棄競選本屆 Leader:

1. 若是在 Candidate 等待 Servers 的投票結果期間收到了其餘擁有更高 Term 的 Server 發來的投票請求;

2. 若是在 Candidate 等待 Servers 的投票結果期間收到了其餘擁有更高 Term 的 Server 發來的心跳;

固然了,當一個 Leader 發現有 Term 更高的 Leader 時也會退回到 Follower 狀態。

當選舉 Leader 成功以後,整個集羣就能夠向外提供正常讀寫服務了,如圖所示,集羣由一個 Leader 兩個 Follower 組成,Leader 負責處理 Client 發起的讀寫請求,同時還要跟 Follower 保持心跳或者把 Log 複製給 Follower。


Log 複製

下面咱們就詳細說一下 Log 複製。咱們以前已經說了 Log 就是 Client 發送給複製狀態機的一系列命令,。這裏咱們再舉例解釋一下 Log,好比咱們的複製狀態機要實現的是一個銀行帳戶系統,那麼這個 Log 就能夠是 Client 發給帳戶系統的一條存錢的命令,好比「存 100 元錢」。

Leader 與 Follower 之間的日誌複製是共識算法運用於複製狀態機的重要目的,在 Raft 算法中 Log 由 TermId、LogIndex、LogValue 這三要素構成,在這張圖上每個小格表明一個 Log。當 Leader 在向 Follower 複製 Log 的時候,Follower 還須要對收到的 Log 作檢查,以確保這些 Log 能和本地已有的 Log 保持連續。咱們以前說了,Raft 算法是要嚴格保證 Log 的連續性的,因此 Follower 會拒絕沒法和本地已有 Log 保持連續的複製請求,那麼這種狀況下就須要走 Log 恢復的流程。總之,Log 複製的目的就是要讓全部的 Server 上的 Log 不管在內容上仍是在順序上都要保持徹底一致,這樣才能保證全部狀態機執行結果一致。

目前已經有一些很優秀的對 Raft 的實現,好比 C++ 寫的 braft,Go 寫的 etcd,Rust 寫的 TiKV。固然了,SOFAJRaft 並非 Raft 算法的第一個 Java 實現,在咱們以前已經有了不少項目。可是通過咱們的評估,以爲目前仍是沒有一個 Raft 的 Java 實現庫類可以知足螞蟻生產環境的要求,這也是咱們去寫 SOFAJRaft 的主要緣由。


SOFAJRaft 介紹

接下來咱們介紹 SOFAJRaft。

SOFAJRaft 是基於 Raft 算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP。從去年 3 月開發到今年 2 月完成,並在今年 3 月開源。應用場景有 Leader 選舉、分佈式鎖服務、高可靠的元信息管理、分佈式存儲系統,目前使用案例有 RheaKV,這是 SOFAJRaft 中自帶的一個分佈式 KV 存儲,還有今天開源的 SOFA 服務註冊中心中的元信息管理模塊也是用到了 SOFAJRaft,除此以外還有一些內部的項目也有使用,可是由於沒有開源,因此就再也不詳述了。

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

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

Meta Storage 是用來存儲記錄 Raft 實現的內部狀態,好比當前 Term 、投票給哪一個節點等信息。

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

1. 當有新的 Node 加入集羣的時候,不用只靠日誌複製、回放去和 Leader 保持數據一致,而是經過安裝 Leader 的快照來跳過早期大量日誌的回放;

2. Leader 用快照替代 Log 複製能夠減小網絡上的數據量;

3. 用快照替代早期的 Log 能夠節省存儲空間。

剛纔咱們說的是一個節點內部的狀況,那在 Raft Group 中至少須要 3 個節點,因此這是一個三副本的架構圖。

咱們會由於各類各樣的需求而去構建一個 Raft 集羣,若是你的目標是實現一個存儲系統的話,那單個 Raft 集羣可能沒有辦法承載你全部的存儲需求;若是你的目標是實現一個爲用戶請求提供 Service 的系統的話,由於 Raft 集羣內只有 Leader 提供讀寫服務,因此讀寫也會造成單點的瓶頸。所以爲了支持水平擴展,SOFAJRaft 提供了 Multi-Group 部署模式。如圖所示,咱們能夠按某種 Key 進行分片部署,好比用戶 ID,咱們讓 Group 1 對 [0, 10000) 的 ID 提供服務,讓 Group 2 對 [10000, 20000) 的 ID 提供服務,以此類推。



SOFAJRaft 特性

這是咱們所支持的 Raft 特性,其中:

  • Membership change 成員管理:集羣內成員的加入和退出不會影響集羣對外提供服務;
  • Transfer leader:除了集羣根據算法自動選出 Leader 以外,還支持經過指令強制指定一個節點成爲 Leader。
  • Fault tolerance 容錯性:當集羣內有節點由於各類緣由不能正常運行時,不會影響整個集羣的正常工做。
  • 多數派故障恢復:當集羣內半數以上的節點都不能正常服務的時候,正常的作法是等待集羣自動恢復,不過 SOFAJRaft 也提供了 Reset 的指令,可讓整個集羣當即重建。
  • Metrics:SOFAJRaft 內置了基於 Metrics 類庫的性能指標統計,具備豐富的性能統計指標,利用這些指標數據能夠幫助用戶更容易找出系統性能瓶頸。

SOFAJRaft 定位是生產級的 Raft 算法實現,因此除了幾百個單元測試以及部分 Chaos 測試以外, SOFAJRaft 還使用 jepsen 這個分佈式驗證和故障注入測試框架模擬了不少種狀況,都已驗證經過:

  • 隨機分區,一大一小兩個網絡分區
  • 隨機增長和移除節點
  • 隨機中止和啓動節點
  • 隨機 kill -9 和啓動節點
  • 隨機劃分爲兩組,互通一箇中間節點,模擬分區狀況
  • 隨機劃分爲不一樣的 majority 分組

網絡分區包括兩種,一種是非對稱網絡分區,一種是對稱網絡分區。

在對稱網絡分區中,S2 和其餘節點通訊中斷,因爲沒法和 Leader 通訊,致使它不斷嘗試競選 Leader,這樣等到網絡恢復的時候,S2 因爲以前的不斷嘗試,其 Term 已經高於 Leader 了。這會迫使 S1 退回到 Follower 狀態,集羣從新進行選舉。爲避免這種因爲對稱網絡分區形成的沒必要要選舉,SOFAJRaft 增長了預投票(pre-vote),一個 Follower 在發起投票前會先嚐試預投票,只有超過半數的機器承認它的預投票,它才能繼續發起正式投票。在上面的狀況中,S2 在每次發起選舉的時候會先嚐試預選舉,因爲在預選舉中它依然得不到集羣內多數派的承認,因此預投票沒法成功,S2 也就不會發起正式投票了,所以他的 Term 也就不會在網絡分區的時候持續增長了。

在非對稱網絡分區中,S2 和 Leader S1 沒法通訊,可是它和另外一個 Follower S3 依然可以通訊。在這種狀況下,S2 發起預投票獲得了 S3 的響應,S2 能夠發起投票請求。接下來 S2 的投票請求會使得 S3 的 Term 也增長以致於超過 Leader S1(S3 收到 S2 的投票請求後,會相應把本身的 Term 提高到跟 S2 一致),所以 S3 接下來會拒絕 Leader S1 的日誌複製。爲解決這種狀況,SOFAJRaft 在 Follower 本地維護了一個時間戳來記錄收到 Leader 上一次數據更新的時間,Follower S3 只有超過 election timeout 以後才容許接受預投票請求,這樣也就避免了 S2 發起投票請求。


SOFAJRaft 優化

接下來咱們說一下 SOFAJRaft 的優化。

爲了提供支持生產環境運行的高性能,SOFAJRaft 主要作了以下幾部分的性能優化,其中:

  • 並行 append log:在 SOFAJRaft 中 Leader 持久化 Log 和向 Followers 發送 Log 是並行的。
  • 併發複製:Leader 向全部 Follwers 發送 Log 也是徹底相互獨立和併發的。
  • 異步化:SOFAJRaft 中整個鏈路幾乎沒有任何阻塞,徹底異步的,是一個徹底的 Callback 編程模型。

下面咱們再說說另外三項:批量化、複製流水線以及線性一致讀。

批量化是性能優化最經常使用的手段之一。SOFAJRaft 經過批量化的手段合併 IO 請求、減小方法調用和上下文切換,具體包括批量提交 Task、批量網絡發送、本地 IO 批量寫入以及狀態機批量應用。值得一提的是 SOFAJRaft 主要是經過 Disruptor 來實現批量的消費模型,經過這種 Ring Buffer 的方式既能夠實現批量消費,又不須要爲了攢批而等待。

複製流水線主要是利用 Pipeline 的通訊方式來提升日誌複製的效率,若是 Leader 跟 Followers 節點的 Log 同步是串行 Batch 的方式,那麼每一個 Batch 發送以後須要等待 Batch 同步完成以後才能繼續發送下一批(ping-pong), 這樣會致使較長的延遲。經過 Leader 跟 Followers 節點之間的 Pipeline 複製能夠有效下降更新的延遲, 提升吞吐。

什麼是線性一致讀呢?簡單來講就是要在分佈式環境中實現 Java volatile 語義的效果,也就是說當一個 Client 向集羣發起寫操做的請求而且獲得成功響應以後,該寫操做的結果要對全部後來的讀請求可見。和 volatile 的區別是 volatile 是實現線程之間的可見,而 SOFAJRaft 須要實現 Server 之間的可見。實現這個目的最常規的辦法是走 Raft 協議,將讀請求一樣按照 Log 處理,經過 Log 複製和狀態機執行來獲得讀結果,而後再把結果返回給 Client。這種辦法的缺點是須要 Log 存儲、複製,這樣會帶來刷盤開銷、存儲開銷、網絡開銷,所以在讀操做不少的場景下對性能影響很大。因此 SOFAJRaft 採用 ReadIndex 來替代走 Raft 狀態機的方案,簡單來講就是依靠這樣的原則直接從 Leader 讀取結果:全部已經複製到多數派上的 Log(可視爲寫操做)就能夠被視爲安全的 Log,Leader 狀態機只要按序執行到這條 Log 以後,該 Log 所體現的數據就能對 Client 可見了。具體能夠分解爲如下四個步驟:

  1. Client 發起讀請求;
  2. Leader 確認最新複製到多數派的 LogIndex;
  3. Leader 確認身份;
  4. 在 LogIndex apply 後執行讀操做。

經過 ReadIndex 的優化,SOFAJRaft 已經可以達到 RPC 上限的 80%了。可是咱們其實還能夠再往前走一步,上面的步驟中能夠看到第 3 步仍是須要 Leader 經過向 Followers 發心跳來確認本身的 Leader 身份,由於 Raft 集羣中的 Leader 身份隨時可能發生改變。因此咱們能夠採用 LeaseRead 的方式把這一步 RPC 省略掉。租約能夠理解爲集羣會給 Leader 一段租期(lease)的身份保證,在此期間 Leader 的身份不會被剝奪,這樣當 Leader 收到讀請求以後,若是發現租期還沒有到期,就無需再經過和 Followers 通訊來確認本身的 Leader 身份,這樣就能夠跳過第 3 步的網絡通訊開銷。經過 LeaseRead 優化,SOFAJRaft 幾乎已經可以達到 RPC 的上限。可是經過時鐘維護租期自己並非絕對的安全(時鐘漂移問題),因此 SOFAJRaft 中默認配置是線性一致讀,由於一般狀況下線性一致讀性能已足夠好。


性能

image.png

這是咱們性能測試的狀況,測試條件以下:

  • 3 臺 16C 20G 內存的 Docker 容器做爲 Server Node (3 副本)
  • 2 ~ 8 臺 8C Docker 容器 做爲 Client
  • 24 個 Raft 複製組,平均每臺 Server Node 上各自有 8 個 Leader 負責讀寫請求,不開啓 Follower 讀
  • 壓測目標爲 JRaft 中的 RheaKV 模塊,只壓測 Put、Get 兩個接口,其中 get 是保證線性一致讀的,Key 和 Value 大小均爲 16 字節
  • 讀比例 10%,寫比例 90%

能夠看到在開啓複製流水線以後,性能能夠提高大約 30%。而當複製流水線和 Client-Batching 都開啓以後,8 臺 Client 可以達到 40w+ ops。

目前 SOFARaft 最新的版本是 v1.2.4,因爲 Raft 算法自己也比較複雜,並且 SOFAJRaft 在實現中還作了不少優化,因此若是對今天的講演有什麼不清楚的地方,歡迎繼經過 SOFAJRaft wiki 繼續瞭解更多細節,另外咱們還有一個如何使用 SOFAJRaft 的示例,在 wiki 上也有詳細的說明。除此以外,家純同窗寫過一篇很詳細的介紹文章《螞蟻金服開源 SOFAJRaft:生產級 Java Raft 算法庫》,你們也能夠看一看。

歡迎 Star SOFAJRaft 幫助咱們改進。

文中涉及到的相關連接

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

相關文章
相關標籤/搜索