SOFAStack
Scalable Open Financial Architecture Stack
是螞蟻金服自主研發的金融級分佈式架構,包含了構建金融級雲原生架構所需的各個組件,是在金融場景裏錘鍊出來的最佳實踐。
本文爲《剖析 | SOFAJRaft 實現原理》第三篇,本篇做者米麒麟,來自陸金所。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:<SOFA:JRaftLab/>,目前領取已經完成,感謝你們的參與。git
SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。github
SOFAJRaft :https://github.com/sofastack/sofa-jraft算法
線性一致讀是在分佈式系統中實現 Java volatile 語義,當客戶端向集羣發起寫操做的請求而且得到成功響應以後,該寫操做的結果要對全部後來的讀請求可見。實現線性一致讀常規手段是走 Raft 協議,將讀請求一樣按照 Log 處理,經過日誌複製和狀態機執行獲取讀結果返回給客戶端,SOFAJRaft 採用 ReadIndex 替代走 Raft 狀態機的方案。本文將圍繞 Raft Log Read,ReadIndex Read 以及 Lease Read 等方面剖析線性一致讀原理,闡述 SOFAJRaft 如何使用 ReadIndex 和 Lease Read 實現線性一致讀:緩存
什麼是線性一致讀? 所謂線性一致讀,一個簡單的例子是在 t1 的時刻咱們寫入了一個值,那麼在 t1 以後,咱們必定能讀到這個值,不可能讀到 t1 以前的舊值(想一想 Java 中的 volatile 關鍵字,即線性一致讀就是在分佈式系統中實現 Java volatile 語義)。簡而言之是須要在分佈式環境中實現 Java volatile 語義效果,即當 Client 向集羣發起寫操做的請求而且得到成功響應以後,該寫操做的結果要對全部後來的讀請求可見。和 volatile 的區別在於 volatile 是實現線程之間的可見,而 SOFAJRaft 須要實現 Server 之間的可見。安全
如上圖 Client A、B、C、D 均符合線性一致讀,其中 D 看起來是 Stale Read,其實並非,D 請求橫跨 3 個階段,而 Read 可能發生在任意時刻,因此讀到 1 或 2 都行。服務器
實現線性一致讀最常規的辦法是走 Raft 協議,將讀請求一樣按照 Log 處理,經過 Log 複製和狀態機執行來獲取讀結果,而後再把讀取的結果返回給 Client。由於 Raft 原本就是一個爲了實現分佈式環境下線性一致性的算法,因此經過 Raft 很是方便的實現線性 Read,也就是將任何的讀請求走一次 Raft Log,等此 Log 提交以後在 apply 的時候從狀態機裏面讀取值,必定可以保證這個讀取到的值是知足線性要求的。網絡
固然,由於每次 Read 都須要走 Raft 流程,Raft Log 存儲、複製帶來刷盤開銷、存儲開銷、網絡開銷,走 Raft Log不只僅有日誌落盤的開銷,還有日誌複製的網絡開銷,另外還有一堆的 Raft 「讀日誌」 形成的磁盤佔用開銷,致使 Read 操做性能是很是低效的,因此在讀操做不少的場景下對性能影響很大,在讀比重很大的系統中是沒法被接受的,一般都不會使用。架構
在 Raft 裏面,節點有三個狀態:Leader,Candidate 和 Follower,任何 Raft 的寫入操做都必須通過 Leader,只有 Leader 將對應的 Raft Log 複製到 Majority 的節點上面認爲這次寫入是成功的。因此若是當前 Leader 能肯定必定是 Leader,那麼可以直接在此 Leader 上面讀取數據,由於對於 Leader 來講,若是確認一個 Log 已經提交到大多數節點,在 t1 的時候 apply 寫入到狀態機,那麼在 t1 後的 Read 就必定能讀取到這個新寫入的數據。app
那麼如何確認 Leader 在處理此次 Read 的時候必定是 Leader 呢?在 Raft 論文裏面,提到兩種方法:異步
第一種是 ReadIndex Read,當 Leader 須要處理 Read 請求時,Leader 與過半機器交換心跳信息肯定本身仍然是 Leader 後可提供線性一致讀:
使用 ReadIndex Read 提供 Follower Read 的功能,很容易在 Followers 節點上面提供線性一致讀,Follower 收到 Read 請求以後:
不一樣於經過 Raft Log 的 Read,ReadIndex Read 使用 Heartbeat 方式來讓 Leader 確認本身是 Leader,省去 Raft Log 流程。相比較於走 Raft Log 方式,ReadIndex Read 省去磁盤的開銷,可以大幅度提高吞吐量。雖然仍然會有網絡開銷,可是 Heartbeat 原本就很小,因此性能仍是很是好的。
雖然 ReadIndex Read 比原來的 Raft Log Read 快不少,但畢竟仍是存在 Heartbeat 網絡開銷,因此考慮作更進一步的優化。Raft 論文裏面說起一種經過 Clock + Heartbeat 的 Lease Read 優化方法,也就是 Leader 發送 Heartbeat 的時候首先記錄一個時間點 Start,當系統大部分節點都回復 Heartbeat Response,因爲 Raft 的選舉機制,Follower 會在 Election Timeout 的時間以後才從新發生選舉,下一個 Leader 選舉出來的時間保證大於 Start+Election Timeout/Clock Drift Bound,因此能夠認爲 Leader 的 Lease 有效期能夠到 Start+Election Timeout/Clock Drift Bound 時間點。Lease Read 與 ReadIndex 相似但更進一步優化,不只節省 Log,並且省掉網絡交互,大幅提高讀的吞吐量而且可以顯著下降延時。
Lease Read 基本思路是 Leader 取一個比 Election Timeout 小的租期(最好小一個數量級),在租約期內不會發生選舉,確保 Leader 不會變化,因此跳過 ReadIndex 的第二步也就下降延時。因而可知 Lease Read 的正確性和時間是掛鉤的,依賴本地時鐘的準確性,所以雖然採用 Lease Read 作法很是高效,可是仍然面臨風險問題,也就是存在預設的前提即各個服務器的 CPU Clock 的時間是準的,即便有偏差,也會在一個很是小的 Bound 範圍裏面,時間的實現相當重要,若是時鐘漂移嚴重,各個服務器之間 Clock 走的頻率不同,這套 Lease 機制可能出問題。
Lease Read 實現方式包括:
SOFAJRaft 採用 ReadIndex 替代走 Raft 狀態機的方案,簡而言之是依靠 ReadIndex 原則直接從 Leader 讀取結果:全部已經複製到多數派上的 Log(可視爲寫操做)被視爲安全的 Log,Leader 狀態機只要按照順序執行到此條 Log以後,該 Log 所體現的數據就能對客戶端 Client 可見,具體分解爲如下四個步驟:
經過 ReadIndex 優化,SOFAJRaft 可以達到 RPC 上限的 80%。上面的步驟中發現第 3 步仍然須要 Leader 經過向 Followers 發送心跳確認本身的 Leader 身份,由於 Raft 集羣中的 Leader 身份隨時可能發生改變。因此 SOFAJRaft 採用 Lease Read 的方式把第 3 步 RPC 省略掉。租約理解爲 Raft 集羣給 Leader 一段租期 Lease 的身份保證,在此期間不會剝奪 Leader 的身份,這樣當 Leader 收到 Read 請求以後,若是發現租期還沒有到期,無需再經過和 Followers 通訊來確認本身的 Leader 身份,這樣跳過第 3 步的網絡通訊開銷。經過 Lease Read 優化,SOFAJRaft 幾乎已經可以達到 RPC 的上限。然而經過時鐘維護租期自己並非絕對的安全(時鐘漂移問題),因此 SOFAJRaft 默認配置是線性一致讀,由於一般狀況下線性一致讀性能已足夠好。
默認狀況下,SOFAJRaft 提供的線性一致讀是基於 Raft 協議的 ReadIndex 實現,三副本的狀況下 Leader 讀的吞吐接近於 RPC 的吞吐上限,延遲取決於多數派中最慢的一個 Heartbeat Response。使用 Node#readIndex(byte [] requestContext, ReadIndexClosure done) 發起線性一致讀請求,當安全讀取時傳入的 Closure 將被調用,正常狀況下從狀態機中讀取數據返回給客戶端, SOFAJRaft 將保證讀取的線性一致性。線性一致讀在任何集羣內的節點發起,並不須要強制要求放到 Leader 節點上,容許在 Follower 節點執行,所以大大下降 Leader 的讀取壓力。
SOFAJRaft 基於 Raft 協議的 ReadIndex 線性一致讀實現是調用 RaftServerService#handleReadIndexRequest 接口根據當前節點狀態爲 STATE_LEADER,STATE_FOLLOWER 以及 STATE_TRANSFERRING 狀況處理 ReadIndex 請求:
一、當前節點狀態是 STATE_LEADER 即爲 Leader 節點,接收 ReadIndex 請求調用 readLeader(request, ReadIndexResponse.newBuilder(), done) 方法提供線性一致讀:
二、當前節點狀態是 STATE_FOLLOWER 即爲 Follower 節點,接收 ReadIndex 請求經過 readFollower(request, done) 方法支持線性一致讀:
SOFAJRaft 基於 Batch+Pipeline Ack+ 全異步機制的 ReadIndex 核心邏輯:
SOFAJRaft 針對更高性能要求場景保證集羣內機器的 CPU 時鐘同步需求,採用 Clock+Heartbeat 的 Lease Read 優化,經過服務端設置 RaftOptions 的 ReadOnlyOption 參數爲 ReadOnlyLeaseBased 實現,ReadOnlyLeaseBased 經過依賴 Leader 租約確保只讀請求的可線性化,可能受時鐘漂移的影響。若是時鐘漂移無限制,Leader 節點可能保持租約長於應有的時間(時鐘能夠向後移動/暫停而沒有任何限制),此種狀況下 ReadIndex 是不安全的。
SOFAJRaft 基於 Lease Read 線性一致讀實現是經過 Leader 節點調用 handleReadIndexRequest 接口接收 ReadIndex 請求獲取 ReadIndex 請求級別 ReadOnlyOption 配置,當 ReadOnlyOption 配置爲 ReadOnlyLeaseBased 時確認 Leader 租約是否有效即檢查 Heartbeat 間隔是否小於 election timeout 時間,Leader 租約超時須要轉變爲 ReadIndex 模式。Leader 租約有效期間認爲當前 Leader 是 Raft Group 內的惟一有效 Leader,忽略 ReadIndex 發送 Heartbeat 確認身份步驟,直接返回 Follower 節點和本地節點 Read 請求成功響應。Leader 節點繼續等待狀態機執行,直到 applyIndex 超過 ReadIndex 安全提供 Linearizable Read。
SOFAJRaft 基於時鐘和心跳實現的線性一致讀 Lease Read 優化邏輯:
本文圍繞 Raft Log Read,ReadIndex Read 以及 Lease Read 線性一致讀實現細節方面剖析 SOFAJRaft 線性一致讀基本原理,闡述 SOFAJRaft 如何使用 Batch+Pipeline Ack+全異步機制和 Clock+Heartbeat 手段優化 ReadIndex 和 Lease Read 線性一致讀具體實現。