SOFAJRaft 線性一致讀實現剖析 | SOFAJRaft 實現原理

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

SOFAJRaft 線性一致讀實現剖析.png

本文爲《剖析 | 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 實現線性一致讀:緩存

  • 什麼是線性一致讀?共識算法只能保證多個節點對某個對象的狀態是一致的,以 Raft 爲例只能保證不一樣節點對 Raft Log 達成一致,那麼 Log 後面的狀態機的一致性呢?
  • 基於 ReadIndex 和 Lease Read 方式 SOFAJRaft 如何實現高效的線性一致讀?

線性一致讀

什麼是線性一致讀? 所謂線性一致讀,一個簡單的例子是在 t1 的時刻咱們寫入了一個值,那麼在 t1 以後,咱們必定能讀到這個值,不可能讀到 t1 以前的舊值(想一想 Java 中的 volatile 關鍵字,即線性一致讀就是在分佈式系統中實現 Java volatile 語義)。簡而言之是須要在分佈式環境中實現 Java volatile 語義效果,即當 Client 向集羣發起寫操做的請求而且得到成功響應以後,該寫操做的結果要對全部後來的讀請求可見。和 volatile 的區別在於 volatile 是實現線程之間的可見,而 SOFAJRaft 須要實現 Server 之間的可見。安全

image.png

如上圖 Client A、B、C、D 均符合線性一致讀,其中 D 看起來是 Stale Read,其實並非,D 請求橫跨 3 個階段,而 Read 可能發生在任意時刻,因此讀到 1 或 2 都行。服務器

Raft Log read

實現線性一致讀最常規的辦法是走 Raft 協議,將讀請求一樣按照 Log 處理,經過 Log 複製和狀態機執行來獲取讀結果,而後再把讀取的結果返回給 Client。由於 Raft 原本就是一個爲了實現分佈式環境下線性一致性的算法,因此經過 Raft 很是方便的實現線性 Read,也就是將任何的讀請求走一次 Raft Log,等此 Log 提交以後在 apply 的時候從狀態機裏面讀取值,必定可以保證這個讀取到的值是知足線性要求的。網絡

image.png

固然,由於每次 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
  • Lease Read

ReadIndex Read

第一種是 ReadIndex Read,當 Leader 須要處理 Read 請求時,Leader 與過半機器交換心跳信息肯定本身仍然是 Leader 後可提供線性一致讀:

  1. Leader 將本身當前 Log 的 commitIndex 記錄到一個 Local 變量 ReadIndex 裏面;
  2. 接着向 Followers 節點發起一輪 Heartbeat,若是半數以上節點返回對應的 Heartbeat Response,那麼 Leader就可以肯定如今本身仍然是 Leader;
  3. Leader 等待本身的 StateMachine 狀態機執行,至少應用到 ReadIndex 記錄的 Log,直到 applyIndex 超過 ReadIndex,這樣就可以安全提供 Linearizable Read,也沒必要管讀的時刻是否 Leader 已飄走;
  4. Leader 執行 Read 請求,將結果返回給 Client。

使用 ReadIndex Read 提供 Follower Read 的功能,很容易在 Followers 節點上面提供線性一致讀,Follower 收到 Read 請求以後:

  1. Follower 節點向 Leader 請求最新的 ReadIndex;
  2. Leader 仍然走一遍以前的流程,執行上面前 3 步的過程(肯定本身真的是 Leader),而且返回 ReadIndex 給 Follower;
  3. Follower 等待當前的狀態機的 applyIndex 超過 ReadIndex;
  4. Follower 執行 Read 請求,將結果返回給 Client。

不一樣於經過 Raft Log 的 Read,ReadIndex Read 使用 Heartbeat 方式來讓 Leader 確認本身是 Leader,省去 Raft Log 流程。相比較於走 Raft Log 方式,ReadIndex Read 省去磁盤的開銷,可以大幅度提高吞吐量。雖然仍然會有網絡開銷,可是 Heartbeat 原本就很小,因此性能仍是很是好的。

Lease Read

雖然 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 實現方式包括:

  1. 定時 Heartbeat 得到多數派響應,確認 Leader 的有效性;
  2. 在租約有效時間內,能夠認爲當前 Leader 是 Raft Group 內的惟一有效 Leader,可忽略 ReadIndex 中的 Heartbeat 確認步驟(2);
  3. Leader 等待本身的狀態機執行,直到 applyIndex 超過 ReadIndex,這樣就可以安全的提供 Linearizable Read。

SOFAJRaft 線性一致讀實現

SOFAJRaft 採用 ReadIndex 替代走 Raft 狀態機的方案,簡而言之是依靠 ReadIndex 原則直接從 Leader 讀取結果:全部已經複製到多數派上的 Log(可視爲寫操做)被視爲安全的 Log,Leader 狀態機只要按照順序執行到此條 Log以後,該 Log 所體現的數據就能對客戶端 Client 可見,具體分解爲如下四個步驟:

  • Client 發起 Read 請求;
  • Leader 確認最新複製到多數派的 LogIndex;
  • Leader 確認身份;
  • 在 LogIndex apply 後執行 Read 操做。

經過 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 默認配置是線性一致讀,由於一般狀況下線性一致讀性能已足夠好。

image.png

ReadIndex Read 實現

默認狀況下,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) 方法提供線性一致讀:

  • 檢查當前 Raft 集羣節點數量,若是集羣只有一個 Peer 節點直接獲取投票箱 BallotBox 最新提交索引 lastCommittedIndex 即 Leader 節點當前 Log 的 commitIndex 構建 ReadIndexClosure 響應;
  • 日誌管理器 LogManager 基於投票箱 BallotBox 的 lastCommittedIndex 獲取任期檢查是否等於當前任期,若是不等於當前任期表示此 Leader 節點未在其任期內提交任何日誌,須要拒絕只讀請求;
  • 校驗 Raft 集羣節點數量以及 lastCommittedIndex 所屬任期符合預期,那麼響應構造器設置其索引爲投票箱 BallotBox 的 lastCommittedIndex,而且來自 Follower 的請求須要檢查 Follower 是否在當前配置;
  • 獲取 ReadIndex 請求級別 ReadOnlyOption 配置,ReadOnlyOption 參數默認值爲 ReadOnlySafe,ReadOnlySafe 經過與 Quorum 通訊來保證只讀請求的可線性化。按照 ReadOnlyOption 配置爲ReadOnlySafe 調用 Replicator#sendHeartbeat(rid, closure) 方法向 Followers 節點發送 Heartbeat 心跳請求,發送心跳成功執行 ReadIndexHeartbeatResponseClosure 心跳響應回調;
  • ReadIndex 心跳響應回調檢查是否超過半數節點包括 Leader 節點自身投票同意,半數以上節點返回客戶端Heartbeat 請求成功響應,即 applyIndex 超過 ReadIndex 說明已經同步到 ReadIndex 對應的 Log 可以提供 Linearizable Read。

二、當前節點狀態是 STATE_FOLLOWER 即爲 Follower 節點,接收 ReadIndex 請求經過 readFollower(request,  done) 方法支持線性一致讀:

  • 檢查當前 Leader 節點是否爲空,若是 Leader 節點爲空表示固然任期沒有 Leader 節點;
  • Follower 節點調用 RpcService#readIndex(leaderId.getEndpoint(), newRequest, -1, closure) 方法向 Leader 發送 ReadIndex 請求,Leader 節點調用 readIndex(requestContext, done) 方法啓動可線性化只讀查詢請求,只讀服務添加請求發佈 ReadIndex 事件到隊列 readIndexQueue 即 Disruptor 的 Ring Buffer;
  • ReadIndex 事件處理器 ReadIndexEventHandler 經過 MPSC Queue 模型攢批消費觸發使用 executeReadIndexEvents(events) 執行 ReadIndex 事件,輪詢 ReadIndex 事件封裝 ReadIndexState 狀態列表構建 ReadIndexResponseClosure 響應回調提交給 Leader 節點處理 ReadIndex 請求;
  • Leader 節點調用 handleReadIndexRequest(request, readIndexResponseClosure) 方法進行 readLeader 線性一致讀過程,返回投票箱 BallotBox 的 lastCommittedIndex。ReadIndex 響應回調遍歷狀態列表記錄當前提交日誌 Index,檢查申請狀態機最新 Log Entry 的 committedIndex 是否已經申請即比較狀態機 appliedIndex 是否大於等於當前 committedIndex。因爲 Leader 節點處理添加 Log Entry 請求發送心跳後投票箱 BallotBox 更新 lastCommittedIndex,當 Leader 節點的 lastCommittedIndex 大於當前的 lastCommittedIndex 就會建立提交 Log Entry 異步任務發佈到 taskQueue 隊列,申請任務處理器 ApplyTaskHandler 執行提交 LogEntry 申請任務,通知 Follower 節點最新申請的 committedIndex 已經更新。若是當前申請狀態機的 applyIndex 超過 ReadIndex,那麼通知 ReadIndex 請求成功返回給客戶端。當前 Follower 節點落後於 Leader 時把 Leader 節點返回的committedIndex 放到 pendingNotifyStatus 緩存等待 Leader 節點同步完日誌更新 applyIndex。

SOFAJRaft 基於 Batch+Pipeline Ack+ 全異步機制的 ReadIndex 核心邏輯:

carbon.png

Lease Read 實現

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 優化邏輯:

carbon (1).png

總結

本文圍繞 Raft Log Read,ReadIndex Read 以及 Lease Read 線性一致讀實現細節方面剖析 SOFAJRaft 線性一致讀基本原理,闡述 SOFAJRaft 如何使用 Batch+Pipeline Ack+全異步機制和 Clock+Heartbeat 手段優化 ReadIndex 和 Lease Read 線性一致讀具體實現。

《剖析 | SOFAJRaft 實現原理》系列文章回顧:

相關文章
相關標籤/搜索