SOFAJRaft 線性一致讀實現剖析

SOFAStack

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

SOFAJRaft 線性一致讀實現剖析

本文為《剖析 | SOFAJRaft 實現原理》第三篇,本篇作者米麒麟,來自陸金所。《剖析 | SOFAJRaft 實現原理》系列由 SOFA 團隊和源碼愛好者們出品,項目代號:,目前領取已經完成,感謝大傢的參與。

SOFAJRaft 是一個基於 Raft 一致性算法的生產級高性能 Java 實現,支持 MULTI-RAFT-GROUP,適用於高負載低延遲的場景。

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 之間的可見。

SOFAJRaft 線性一致讀實現剖析

如上圖 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 的時候從狀態機裡面讀取值,一定能夠保證這個讀取到的值是滿足線性要求的。

SOFAJRaft 線性一致讀實現剖析

當然,因為每次 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 就一定能讀取到這個新寫入的數據。

那麼如何確認 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 默認配置是線性一致讀,因為通常情況下線性一致讀性能已足夠好。

SOFAJRaft 線性一致讀實現剖析

ReadIndex Read 實現

默認情況下,SOFAJRaft 提供的線性一致讀是基於 Raft 協議的 ReadIndex 實現,三副本的情況下 Leader 讀的吞吐接近於 RPC 的吞吐上限,延遲取決於多數派中最慢的一個 Heartbeat Response。使用 Node#readIndex(byte [] requestContext, ReadIndexClosure done) 發起線性一致讀請求,當安全讀取時傳入的 Closure 將被調用,正常情況下從狀態機中讀取數據返回給客戶端, SOFAJRaft 將保證讀取的線性一致性。線性一致讀在任何集群內的節點發起,並不需要強制要求放到 Leader 節點上,允許在 Follower 節點執行,因此大大降低 Leader 的讀取壓力。

RaftServerService#handleReadIndexRequest 接口根據當前節點狀態為 STATE_LEADER,STATE_FOLLOWER 或者 STATE_TRANSFERRING 情況處理 ReadIndex 請求:

1、當前節點狀態是 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。

2、當前節點狀態是 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 核心邏輯:

SOFAJRaft 線性一致讀實現剖析

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

SOFAJRaft 線性一致讀實現剖析

總結

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

歡迎 Star SOFAJRaft :

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

Published in News by Awesome.

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *