天天看點

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

什麼是 SOFAJRaft ?

SOFAJRaft 是一個基于 Raft 一緻性算法的生産級高性能 Java 實作,支援 >MULTI-RAFT-GROUP,适用于高負載低延遲的場景。 使用 SOFAJRaft 你可以專注>于自己的業務領域,由 SOFAJRaft 負責處理所有與 Raft 相關的技術難題,并且 >SOFAJRaft 非常易于使用,你可以通過幾個示例在很短的時間内掌握它。

SOFAJRaft 是從百度的 braft 移植而來,做了一些優化和改進,感謝百度 braft 團隊開源了如此優秀的 C++ Raft 實作。

SOFAJRaft :

https://github.com/alipay/sofa-jraft

基礎知識:分布式共識算法 (Consensus Algorithm)

如何了解分布式共識?

  • 多個參與者 針對 某一件事 達成完全 一緻 :一件事,一個結論
  • 已達成一緻的結論,不可推翻

有哪些分布式共識算法?

  • Paxos:被認為是分布式共識算法的根本,其他都是其變種,但是 Paxos 論文中隻給出了單個提案的過程,并沒有給出複制狀态機中需要的 multi-paxos 的相關細節的描述,實作 Paxos 具有很高的工程複雜度(如多點可寫,允許日志空洞等)。
  • Zab:被應用在 Zookeeper 中,業界使用廣泛,但沒有抽象成通用的 library。
  • Raft:以容易了解著稱,業界也湧現出很多 Raft 實作,比如大名鼎鼎的 etcd, braft, tikv 等。

什麼是 Raft?

Raft 是一種更易于了解的分布式共識算法,核心協定本質上還是師承 Paxos 的精髓,不同的是依靠 Raft 子產品化的拆分以及更加簡化的設計,Raft 協定相對更容易實作。

子產品化的拆分主要展現在:Raft 把一緻性協定劃分為 Leader 選舉、Membership 變更、日志複制、Snapshot 等幾個幾乎完全解耦的子產品。

更加簡化的設計則展現在:Raft 不允許類似 Paxos 中的亂序送出、簡化系統中的角色狀态(隻有 Leader、Follower、Candidate 三種角色)、限制僅 Leader 可寫入、使用随機化的逾時時間來設計 Leader Election 等等。

特點:Strong Leader

  • 系統中必須存在且同一時刻隻能有一個 Leader,隻有 Leader 可以接受 Clients 發過來的請求;
  • Leader 負責主動與所有 Followers 通信,負責将“提案”發送給所有 Followers,同時收集多數派的 Followers 應答;
  • Leader 還需向所有 Followers 主動發送心跳維持上司地位(保持存在感)。

一句話總結 Strong Leader:"你們不要 BB! 按我說的做,做完了向我彙報!"。

另外,身為 Leader 必須保持一直 BB(heartbeat) 的狀态,否則就會有别人跳出來想要 BB 。

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

Raft 中的基本概念

篇幅有限,這裡隻對 Raft 中的幾個概念做一個簡單介紹,詳細請參考 Raft paper。

Raft-node 的 3 種角色/狀态

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫
  1. Follower:完全被動,不能發送任何請求,隻接受并響應來自 Leader 和 Candidate 的 Message,每個節點啟動後的初始狀态一定是 Follower;
  2. Leader:處理所有來自用戶端的請求,以及複制 Log 到所有 Followers;
  3. Candidate:用來競選一個新 Leader (Candidate 由 Follower 觸發逾時而來)。

Message 的 3 種類型

  1. RequestVote RPC:由 Candidate 發出,用于發送投票請求;
  2. AppendEntries (Heartbeat) RPC:由 Leader 發出,用于 Leader 向 Followers 複制日志條目,也會用作 Heartbeat (日志條目為空即為 Heartbeat);
  3. InstallSnapshot RPC:由 Leader 發出,用于快照傳輸,雖然多數情況都是每個伺服器獨立建立快照,但是 Leader 有時候必須發送快照給一些落後太多的 Follower,這通常發生在 Leader 已經丢棄了下一條要發給該 Follower 的日志條目(Log Compaction 時清除掉了) 的情況下。

任期邏輯時鐘

  1. 時間被劃分為一個個任期 (term),term id 按時間軸單調遞增;
  2. 每一個任期開始後要做的第一件事都是 Leader 選舉,選舉成功之後,Leader 在任期内管理整個叢集,也就是 “選舉 + 正常操作”;
  3. 每個任期最多一個 Leader,可能沒有 Leader (spilt-vote 導緻)。
    螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

本圖出自《Raft: A Consensus Algorithm for Replicated Logs》

什麼是 SOFAJRaft?

SOFAJRaft 是一個基于 Raft 一緻性算法的生産級高性能 Java 實作,支援 MULTI-RAFT-GROUP,适用于高負載低延遲的場景。 使用 SOFAJRaft 你可以專注于自己的業務領域,由 SOFAJRaft 負責處理所有與 Raft 相關的技術難題,并且 SOFAJRaft 非常易于使用,你可以通過幾個示例在很短的時間内掌握它。

SOFAJRaft 整體功能&性能優化

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

功能支援

  1. Leader election:Leader 選舉,這個不多說,上面已介紹過 Raft 中的 Leader 機制。
  2. Log replication and recovery:日志複制和日志恢複。
  • Log replication 就是要保證已經被 commit 的資料一定不會丢失,即一定要成功複制到多數派。
  • Log recovery 包含兩個方面:

Current term 日志恢複:主要針對一些 Follower 節點重新開機加入叢集或者是新增 Follower 節點後如何追日志;

Prev term 日志恢複:主要針對 Leader 切換前後的日志一緻性。

  1. Snapshot and log compaction:定時生成 snapshot,實作 log compaction 加速啟動和恢複,以及 InstallSnapshot 給 Followers 拷貝資料,如下圖:
螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

本圖出自《In Search of an Understandable Consensus Algorithm》

  1. Membership change:用于叢集線上配置變更,比如增加節點、删除節點、替換節點等。
  2. Transfer leader:主動變更 Leader,用于重新開機維護,Leader 負載平衡等。
  3. Symmetric network partition tolerance:對稱網絡分區容忍性。
螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

如上圖 S1 為目前 Leader,網絡分區造成 S2 不斷增加本地 term,為了避免網絡恢複後 S2 發起選舉導緻正在良心 工作的 Leader step-down,進而導緻整個叢集重新發起選舉,SOFAJRaft 中增加了 pre-vote 來避免這個問題的發生。

  • SOFAJRaft 中在 request-vote 之前會先進行 pre-vote(currentTerm + 1, lastLogIndex, lastLogTerm),多數派成功後才會轉換狀态為 candidate 發起真正的 request-vote,是以分區後的節點,pre-vote 不會成功,也就不會導緻叢集一段時間内無法正常提供服務。
  1. Asymmetric network partition tolerance:非對稱網絡分區容忍性。
    螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

如上圖 S1 為目前 Leader,S2 不斷逾時觸發選主,S3 提升 term 打斷目前 lease,進而拒絕 Leader 的更新。

在 SOFAJRaft 中增加了一個 tick 的檢查,每個 Follower 維護一個時間戳記錄下收到 Leader 上資料更新的時間(也包括心跳),隻有超過 election timeout 之後才允許接受 request-vote 請求。

  1. Fault tolerance:容錯性,少數派故障不影響系統整體可用性,包括但不限于:
  • 機器掉電
  • 強殺應用
  • 慢節點(GC, OOM 等)
  • 網絡故障
  • 其他各種奇葩原因導緻 Raft 節點無法正常工作
  1. Workaround when quorate peers are dead:多數派故障時,整個 group 已不具備可用性,安全的做法是等待多數節點恢複,隻有這樣才能保證資料安全;但是如果業務更加追求系統可用性,可以放棄資料一緻性的話,SOFAJRaft 提供了手動觸發 reset_peers 的指令以迅速重建整個叢集,恢複叢集可用。
  2. Metrics:SOFAJRaft 内置了基于 Metrics 類庫的性能名額統計,具有豐富的性能統計名額,利用這些名額資料可以幫助使用者更容易找出系統性能瓶頸。
  3. Jepsen:除了幾百個單元測試以及部分 chaos 測試之外, SOFAJRaft 還使用 jepsen 這個分布式驗證和故障注入測試架構模拟了很多種情況,都已驗證通過:
  • 随機分區,一大一小兩個網絡分區
  • 随機增加和移除節點
  • 随機停止和啟動節點
  • 随機 kill -9 和啟動節點
  • 随機劃分為兩組,互通一個中間節點,模拟分區情況
  • 随機劃分為不同的 majority 分組

性能優化

除了功能上的完整性,SOFAJRaft 還做了很多性能方面的優化,這裡有一份 KV 場景(get/put)的 Benchmark 資料(連結見文末), 在小資料包,讀寫比例為 9:1,保證線性一緻讀的場景下,三副本最高可以達到 40w+ 的 ops。

這裡挑重點介紹幾個優化點:

  1. Batch: 我們知道網際網路兩大優化法寶便是 Cache 和 Batch,SOFAJRaft 在 Batch 上花了較大心思,整個鍊路幾乎都是 Batch 的,依靠 disruptor 的 MPSC 模型批量消費,對整體性能有着極大的提升,包括但不限于:
  • 批量送出 task
  • 批量網絡發送
  • 本地 IO batch 寫入

要保證日志不丢,一般每條 log entry 都要進行 fsync 同步刷盤,比較耗時,SOFAJRaft 中做了合并寫入的優化。

  • 批量應用到狀态機
  • 需要說明的是,雖然 SOFAJRaft 中大量使用了 Batch 技巧,但對單個請求的延時并無任何影響,SOFAJRaft 中不會對請求做延時的攢批處理。
  1. Replication pipeline:流水線複制,通常 Leader 跟 Followers 節點的 Log 同步是串行 Batch 的方式,每個 Batch 發送之後需要等待 Batch 同步完成之後才能繼續發送下一批(ping-pong),這樣會導緻較長的延遲。SOFAJRaft 中通過 Leader 跟 Followers 節點之間的 pipeline 複制來改進,非常有效降低了資料同步的延遲,提高吞吐。經我們測試,開啟 pipeline 可以将吞吐提升 30% 以上,詳細資料請參照 Benchmark。
  1. Append log in parallel:在 SOFAJRaft 中 Leader 持久化 log entries 和向 Followers 發送 log entries 是并行的。
  1. Fully concurrent replication:Leader 向所有 Follwers 發送 Log 也是完全互相獨立和并發的。

Asynchronous:SOFAJRaft 中整個鍊路幾乎沒有任何阻塞,完全異步的,是一個完全的 callback 程式設計模型。

ReadIndex:優化 Raft read 走 Raft log 的性能問題,每次 read,僅記錄 commitIndex,然後發送所有 peers heartbeat 來确認 Leader 身份,如果 Leader 身份确認成功,等到 appliedIndex >= commitIndex,就可以傳回 Client read 了,基于 ReadIndex Follower 也可以很友善的提供線性一緻讀,不過 commitIndex 是需要從 Leader 那裡擷取,多了一輪 RPC;關于線性一緻讀文章後面會詳細分析。

Lease Read:SOFAJRaft 還支援通過租約 (lease) 保證 Leader 的身份,進而省去了 ReadIndex 每次 heartbeat 确認 Leader 身份,性能更好,但是通過時鐘維護 lease 本身并不是絕對的安全(時鐘漂移問題,是以 SOFAJRaft 中預設配置是 ReadIndex,因為通常情況下 ReadIndex 性能已足夠好)。

SOFAJRaft 設計

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫
  1. Node:Raft 分組中的一個節點,連接配接封裝底層的所有服務,使用者看到的主要服務接口,特别是 apply(task)用于向 raft group 組成的複制狀态機叢集送出新任務應用到業務狀态機。

2.存儲:上圖靠下的部分均為存儲相關。

Log 存儲,記錄 Raft 使用者送出任務的日志,将日志從 Leader 複制到其他節點上。

LogStorage 是存儲實作,預設實作基于 RocksDB 存儲,你也可以很容易擴充自己的日志存儲實作;

LogManager 負責對底層存儲的調用,對調用做緩存、批量送出、必要的檢查和優化。

Metadata 存儲,元資訊存儲,記錄 Raft 實作的内部狀态,比如目前 term、投票給哪個節點等資訊。

Snapshot 存儲,用于存放使用者的狀态機 snapshot 及元資訊,可選:

SnapshotStorage 用于 snapshot 存儲實作;

SnapshotExecutor 用于 snapshot 實際存儲、遠端安裝、複制的管理。

  1. 狀态機

StateMachine:使用者核心邏輯的實作,核心是 onApply(Iterator) 方法, 應用通過 Node#apply(task) 送出的日志到業務狀态機;

FSMCaller:封裝對業務 StateMachine 的狀态轉換的調用以及日志的寫入等,一個有限狀态機的實作,做必要的檢查、請求合并送出和并發處理等。

  1. 複制

Replicator:用于 Leader 向 Followers 複制日志,也就是 Raft 中的 AppendEntries 調用,包括心跳存活檢查等;

ReplicatorGroup:用于單個 Raft group 管理所有的 replicator,必要的權限檢查和派發。

  1. RPC:RPC 子產品用于節點之間的網絡通訊

RPC Server:内置于 Node 内的 RPC 伺服器,接收其他節點或者用戶端發過來的請求,轉交給對應服務處理;

RPC Client:用于向其他節點發起請求,例如投票、複制日志、心跳等。

  1. KV Store:KV Store 是各種 Raft 實作的一個典型應用場景,SOFAJRaft 中包含了一個嵌入式的分布式 KV 存儲實作(SOFAJRaft-RheaKV)。

SOFAJRaft Group

單個節點的 SOFAJRaft-node 是沒什麼實際意義的,下面是三副本的 SOFAJRaft 架構圖:

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

SOFAJRaft Multi Group

單個 Raft group 是無法解決大流量的讀寫瓶頸的,SOFAJRaft 自然也要支援 multi-raft-group。

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

SOFAJRaft 實作細節解析之高效的線性一緻讀

什麼是線性一緻讀? 所謂線性一緻讀,一個簡單的例子就是在 t1 的時刻我們寫入了一個值,那麼在 t1 之後,我們一定能讀到這個值,不可能讀到 t1 之前的舊值 (想想 Java 中的 volatile 關鍵字,說白了線性一緻讀就是在分布式系統中實作 Java volatile 語義)。

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

如上圖 Client A、B、C、D 均符合線性一緻讀,其中 D 看起來是 stale read,其實并不是,D 請求橫跨了 3 個階段,而讀可能發生在任意時刻,是以讀到 1 或 2 都行。

重要:接下來的讨論均基于一個大前提,就是業務狀态機的實作必須是滿足線性一緻性的,簡單說就是也要具有 Java volatile 的語義。

  1. 要實作線性一緻讀,首先我們簡單直接一些,是否可以直接從目前 Leader 節點讀?

仔細一想,這顯然行不通,因為你無法确定這一刻目前的 "Leader" 真的是 Leader,比如在網絡分區的情況下,它可能已經被推翻王朝卻不自知。

  1. 最簡單易懂的實作方式:同 “寫” 請求一樣,“讀” 請求也走一遍 Raft 協定 (Raft Log)
螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

這一定是可以的,但性能上顯然不會太出色,走 Raft Log 不僅僅有日志落盤的開銷,還有日志複制的網絡開銷,另外還有一堆的 Raft “讀日志” 造成的磁盤占用開銷,這在讀比重很大的系統中通常是無法被接受的。

  1. ReadIndex Read

這是 Raft 論文中提到的一種優化方案,具體來說:

Leader 将自己目前 Log 的 commitIndex 記錄到一個 Local 變量 ReadIndex 裡面;

接着向 Followers 發起一輪 heartbeat,如果半數以上節點傳回了對應的 heartbeat response,那麼 Leader 就能夠确定現在自己仍然是 Leader (證明了自己是自己);

Leader 等待自己的狀态機執行,直到 applyIndex 超過了 ReadIndex,這樣就能夠安全的提供 Linearizable Read 了,也不必管讀的時刻是否 Leader 已飄走 (思考:為什麼等到 applyIndex 超過了 ReadIndex 就可以執行讀請求?);

Leader 執行 read 請求,将結果傳回給 Client。

通過 ReadIndex,也可以很容易在 Followers 節點上提供線性一緻讀:

Follower 節點向 Leader 請求最新的 ReadIndex;

Leader 執行上面前 3 步的過程(确定自己真的是 Leader),并傳回 ReadIndex 給 Follower;

Follower 等待自己的 applyIndex 超過了 ReadIndex;

Follower 執行 read 請求,将結果傳回給 Client。(SOFAJRaft 中可配置是否從 Follower 讀取,預設不打開)

ReadIndex小結:

相比較于走 Raft Log 的方式,ReadIndex 省去了磁盤的開銷,能大幅度提升吞吐,結合 SOFAJRaft 的 batch + pipeline ack + 全異步機制,三副本的情況下 Leader 讀的吞吐可以接近于 RPC 的吞吐上限;

延遲取決于多數派中最慢的一個 heartbeat response,理論上對于降低延時的效果不會非常顯著。

  1. Lease Read

Lease Read 與 ReadIndex 類似,但更進一步,不僅省去了 Log,還省去了網絡互動。它可以大幅提升讀的吞吐也能顯著降低延時。

基本的思路是 Leader 取一個比 election timeout 小的租期(最好小一個數量級),在租約期内不會發生選舉,這就確定了 Leader 不會變,是以可以跳過 ReadIndex 的第二步,也就降低了延時。可以看到 Lease Read 的正确性和時間是挂鈎的,是以時間的實作至關重要,如果時鐘漂移嚴重,這套機制就會有問題。

實作方式:

定時 heartbeat 獲得多數派響應,确認 Leader 的有效性 (在 SOFAJRaft 中預設的 heartbeat 間隔是 election timeout 的十分之一);

在租約有效時間内,可以認為目前 Leader 是 Raft Group 内的唯一有效 Leader,可忽略 ReadIndex 中的 heartbeat 确認步驟(2);

Leader 等待自己的狀态機執行,直到 applyIndex 超過了 ReadIndex,這樣就能夠安全的提供 Linearizable Read 了 。

在 SOFAJRaft 中發起一次線性一緻讀請求的代碼展示:

// KV 存儲實作線性一緻讀
public void readFromQuorum(String key, AsyncContext asyncContext) {
    // 請求 ID 作為請求上下文傳入
    byte[] reqContext = new byte[4];
    Bits.putInt(reqContext, 0, requestId.incrementAndGet());
    // 調用 readIndex 方法, 等待回調執行
    this.node.readIndex(reqContext, new ReadIndexClosure() {
        @Override
        public void run(Status status, long index, byte[] reqCtx) {
            if (status.isOk()) {
                try {
                    // ReadIndexClosure 回調成功,可以從狀态機讀取最新資料傳回
                    // 如果你的狀态實作有版本概念,可以根據傳入的日志 index 編号做讀取
                    asyncContext.sendResponse(new ValueCommand(fsm.getValue(key)));
                } catch (KeyNotFoundException e) {
                    asyncContext.sendResponse(GetCommandProcessor.createKeyNotFoundResponse());
                }
            } else {
                // 特定情況下,比如發生選舉,該讀請求将失敗
                asyncContext.sendResponse(new BooleanCommand(false, status.getErrorMsg()));
            }
        }
    });
}           

SOFAJRaft 應用場景?

Leader 選舉;

分布式鎖服務,比如 Zookeeper,在 SOFAJRaft 中的 RheaKV 子產品提供了完整的分布式鎖實作;

高可靠的元資訊管理,可直接基于 SOFAJRaft-RheaKV 存儲;

分布式存儲系統,如分布式消息隊列、分布式檔案系統、分布式塊系統等等。

使用案例

RheaKV:基于 SOFAJRaft 實作的嵌入式、分布式、高可用、強一緻的 KV 存儲類庫。

AntQ Streams QCoordinator:使用 SOFAJRaft 在 Coordinator 叢集内做選舉、使用 SOFAJRaft-RheaKV 做元資訊存儲等功能。

Schema Registry:高可靠 schema 管理服務,類似 kafka schema registry,存儲部分基于 SOFAJRaft-RheaKV。

SOFA 服務注冊中心元資訊管理子產品:IP 資料資訊注冊,要求寫資料達到各個節點一緻,并且在少數派節點挂掉時保證不影響資料正常存儲。

實踐

一、基于 SOFAJRaft 設計一個簡單的 KV Store

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

二、基于 SOFAJRaft 的 RheaKV 的設計

螞蟻金服開源 SOFAJRaft:生産級 Java Raft 算法庫

功能名詞

PD

  • 全局的中心總控節點,負責整個叢集的排程,不需要自管理的叢集可不啟用 PD (一個 PD 可管理多個叢集,基于 clusterId 隔離)。

Store

  • 叢集中的一個實體存儲節點,一個 Store 包含一個或多個 Region。

Region

  • 最小的 KV 資料單元,每個 Region 都有一個左閉右開的區間 [startKey, endKey), 可根據請求流量/負載/資料量大小等名額自動分裂以及自動副本搬遷。

特點

  • 嵌入式
  • 強一緻性
  • 自驅動
  • 自診斷、 自優化、自決策

以上幾點(尤其2、3) 基本都是依托于 SOFAJRaft 自身的功能來實作,詳細介紹請參考 SOFAJRaft 文檔 。

緻謝

感謝 braft、etcd、tikv 貢獻了優秀的 Raft 實作,SOFAJRaft 受益良多。

招聘

螞蟻金服中間件團隊持續在尋找對于基礎中間件(如消息、資料中間件以及分布式計算等)以及下一代高性能面向實時分析的時序資料庫等方向充滿熱情的小夥伴加入,有意者請聯系 [email protected]

參考資料

SOFAJRaft 源碼:

SOFAJRaft 詳細文檔:

https://github.com/alipay/sofa-jraft/wiki

Raft:

https://raft.github.io

Raft paper:

https://raft.github.io/raft.pdf

Raft: A Consensus Algorithm for Replicated Logs:

https://raft.github.io/slides/raftuserstudy2013.pdf

Paxos/Raft 分布式一緻性算法原理剖析及其在實戰中的應用:

https://github.com/hedengcheng/tech/tree/master/distributed

braft 文檔:

https://github.com/brpc/braft/blob/master/docs/cn/raft_protocol.md

線性一緻性和 Raft:

https://pingcap.com/blog-cn/linearizability-and-raft

Strong consistency models:

https://aphyr.com/posts/313-strong-consistency-models

etcd raft 設計與實作《一》:

https://zhuanlan.zhihu.com/p/51063866

Metrics:

https://metrics.dropwizard.io/4.0.0/getting-started.html

Jepsen:

https://github.com/jepsen-io/jepsen

Benchmark:

https://github.com/alipay/sofa-jraft/wiki/Benchmark-%E6%95%B0%E6%8D%AE