什麼是SOFAJRaft?
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。 使用 SOFAJRaft 你能夠專一於本身的業務領域,由 SOFAJRaft 負責處理全部與 Raft 相關的技術難題,而且 SOFAJRaft 很是易於使用,你能夠經過幾個示例在很短的時間內掌握它。
SOFAJRaft 是從百度的 braft 移植而來,作了一些優化和改進,感謝百度 braft 團隊開源瞭如此優秀的 C++ Raft 實現。
Raft 是一種更易於理解的分佈式共識算法,核心協議本質上仍是師承 Paxos 的精髓,不一樣的是依靠 Raft 模塊化的拆分以及更加簡化的設計,Raft 協議相對更容易實現。html
模塊化的拆分主要體如今:Raft 把一致性協議劃分爲 Leader 選舉、MemberShip 變動、日誌複製、Snapshot 等幾個幾乎徹底解耦的模塊。node
更加簡化的設計則體如今:Raft 不容許相似 Paxos 中的亂序提交、簡化系統中的角色狀態(只有 Leader、Follower、Candidate 三種角色)、限制僅 Leader 可寫入、使用隨機化的超時時間來設計 Leader Election 等等。git
一句話總結 Strong Leader: "大家不要 BB! 按我說的作,作完了向我彙報!"。github
另外,身爲 Leader 必須保持一直 BB(heartbeat) 的狀態,不然就會有別人跳出來想要 BB 。算法
篇幅有限,這裏只對 Raft 中的幾個概念作一個簡單介紹,詳細請參考 Raft paper。數據庫
本圖出自《Raft: A Consensus Algorithm for Replicated Logs》編程
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。 使用 SOFAJRaft 你能夠專一於本身的業務領域,由 SOFAJRaft 負責處理全部與 Raft 相關的技術難題,而且 SOFAJRaft 很是易於使用,你能夠經過幾個示例在很短的時間內掌握它。緩存
SOFAJRaft 是從百度的 braft 移植而來,作了一些優化和改進,感謝百度 braft 團隊開源瞭如此優秀的 C++ Raft 實現。安全
1.Leader election:Leader 選舉,這個很少說,上面已介紹過 Raft 中的 Leader 機制。性能優化
2.Log replication and recovery:日誌複製和日誌恢復。
2)Prev term 日誌恢復:主要針對 Leader 切換先後的日誌一致性。
3.Snapshot and log compaction:定時生成 snapshot,實現 log compaction 加速啓動和恢復,以及 InstallSnapshot 給 Followers 拷貝數據,以下圖:
本圖出自《In Search of an Understandable Consensus Algorithm》
4.Membership change:用於集羣線上配置變動,好比增長節點、刪除節點、替換節點等。
5.Transfer leader:主動變動 leader,用於重啓維護,leader 負載平衡等。
6.Symmetric network partition tolerance:對稱網絡分區容忍性。
如上圖 S1 爲當前 leader,網絡分區形成 S2 不斷增長本地 term,爲了不網絡恢復後 S2 發起選舉致使正在良心 工做的 leader step-down,從而致使整個集羣從新發起選舉,SOFAJRaft 中增長了 pre-vote 來避免這個問題的發生。
7.Asymmetric network partition tolerance:非對稱網絡分區容忍性。
如上圖 S1 爲當前 leader,S2 不斷超時觸發選主,S3 提高 term 打斷當前 lease,從而拒絕 leader 的更新。
8.Fault tolerance:容錯性,少數派故障不影響系統總體可用性,包括但不限於:
9.Workaround when quorate peers are dead:多數派故障時,整個 grop 已不具有可用性,安全的作法是等待多數節點恢復,只有這樣才能保證數據安全;可是若是業務更加追求系統可用性,能夠放棄數據一致性的話,SOFAJRaft 提供了手動觸發 reset_peers 的指令以迅速重建整個集羣,恢復集羣可用。
10.Metrics:SOFAJRaft 內置了基於 Metrics 類庫的性能指標統計,具備豐富的性能統計指標,利用這些指標數據能夠幫助用戶更容易找出系統性能瓶頸。
11.Jepsen:除了幾百個單元測試以及部分 chaos 測試以外, SOFAJRaft 還使用 jepsen 這個分佈式驗證和故障注入測試框架模擬了不少種狀況,都已驗證經過:
除了功能上的完整性,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 中不會對請求作延時的攢批處理。
2. Replication pipeline:流水線複製,一般 Leader 跟 Followers 節點的 Log 同步是串行 Batch 的方式,每一個 Batch 發送以後須要等待 Batch 同步完成以後才能繼續發送下一批(ping-pong),這樣會致使較長的延遲。SOFAJRaft 中經過 Leader 跟 Followers 節點之間的 pipeline 複製來改進,很是有效下降了數據同步的延遲,提升吞吐。經咱們測試,開啓 pipeline 能夠將吞吐提高 30% 以上,詳細數據請參照 Benchmark。
3. Append log in parallel:在 SOFAJRaft 中 Leader 持久化 log entries 和向 Followers 發送 log entries 是並行的。
4. 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 性能已足夠好)。
1. Node:Raft 分組中的一個節點,鏈接封裝底層的全部服務,用戶看到的主要服務接口,特別是 apply(task)
用於向 raft group 組成的複製狀態機集羣提交新任務應用到業務狀態機。
2.存儲:上圖靠下的部分均爲存儲相關。
Log 存儲,記錄 Raft 用戶提交任務的日誌,將日誌從 Leader 複製到其餘節點上。
LogStorage 是存儲實現,默認實現基於 RocksDB 存儲,你也能夠很容易擴展本身的日誌存儲實現;
LogManager 負責對底層存儲的調用,對調用作緩存、批量提交、必要的檢查和優化。
Metadata 存儲,元信息存儲,記錄 Raft 實現的內部狀態,好比當前 term、投票給哪一個節點等信息。
Snapshot 存儲,用於存放用戶的狀態機 snapshot 及元信息,可選:
SnapshotStorage 用於 snapshot 存儲實現;
SnapshotExecutor 用於 snapshot 實際存儲、遠程安裝、複製的管理。
3. 狀態機
StateMachine:用戶核心邏輯的實現,核心是 onApply(Iterator)
方法, 應用經過 Node#apply(task)
提交的日誌到業務狀態機;
FSMCaller:封裝對業務 StateMachine 的狀態轉換的調用以及日誌的寫入等,一個有限狀態機的實現,作必要的檢查、請求合併提交和併發處理等。
4. 複製
Replicator:用於 Leader 向 Followers 複製日誌,也就是 Raft 中的 AppendEntries 調用,包括心跳存活檢查等;
ReplicatorGroup:用於單個 Raft group 管理全部的 replicator,必要的權限檢查和派發。
5. RPC:RPC 模塊用於節點之間的網絡通信
RPC Server:內置於 Node 內的 RPC 服務器,接收其餘節點或者客戶端發過來的請求,轉交給對應服務處理;
RPC Client:用於向其餘節點發起請求,例如投票、複製日誌、心跳等。
6. KV Store:KV Store 是各類 Raft 實現的一個典型應用場景,SOFAJRaft 中包含了一個嵌入式的分佈式 KV 存儲實現(SOFAJRaft-RheaKV)。
單個節點的 SOFAJRaft-node 是沒什麼實際意義的,下面是三副本的 SOFAJRaft 架構圖:
單個 Raft group 是沒法解決大流量的讀寫瓶頸的,SOFAJRaft 天然也要支持 multi-raft-group。
什麼是線性一致讀? 所謂線性一致讀,一個簡單的例子就是在 t1 的時刻咱們寫入了一個值,那麼在 t1 以後,咱們必定能讀到這個值,不可能讀到 t1 以前的舊值 (想一想 Java 中的 volatile 關鍵字,說白了線性一致讀就是在分佈式系統中實現 Java volatile 語義)。
如上圖 Client A、B、C、D 均符合線性一致讀,其中 D 看起來是 stale read,其實並非,D 請求橫跨了 3 個階段,而讀可能發生在任意時刻,因此讀到 1 或 2 都行。
重要:接下來的討論均基於一個大前提,就是業務狀態機的實現必須是知足線性一致性的,簡單說就是也要具備 Java volatile 的語義。
1. 要實現線性一致讀,首先咱們簡單直接一些,是否能夠直接從當前 Leader 節點讀?
仔細一想,這顯然行不通,由於你沒法肯定這一刻當前的 "Leader" 真的是 Leader,好比在網絡分區的狀況下,它可能已經被推翻王朝卻不自知。
2. 最簡單易懂的實現方式:同 「寫」 請求同樣,「讀」 請求也走一遍 Raft 協議 (Raft Log)
本圖出自《Raft: A Consensus Algorithm for Replicated Logs》
這必定是能夠的,但性能上顯然不會太出色,走 Raft Log 不只僅有日誌落盤的開銷,還有日誌複製的網絡開銷,另外還有一堆的 Raft 「讀日誌」 形成的磁盤佔用開銷,這在讀比重很大的系統中一般是沒法被接受的。
3. 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,理論上對於下降延時的效果不會很是顯著。
4. 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()));
}
}
});
}複製代碼
PD
Store
Region
以上幾點(尤爲二、3) 基本都是依託於 SOFAJRaft 自身的功能來實現,詳細介紹請參考 SOFAJRaft 文檔 。
感謝 braft、etcd、tikv 貢獻了優秀的 Raft 實現,SOFAJRaft 受益良多。
螞蟻金服中間件團隊持續在尋找對於基礎中間件(如消息、數據中間件以及分佈式計算等)以及下一代高性能面向實時分析的時序數據庫等方向充滿熱情的小夥伴加入,有意者請聯繫 boyan@antfin.com。
公衆號:金融級分佈式架構(Antfin_SOFA)