基於 Jepsen 來發現幾個 Raft 實現中的一致性問題(2)

image

Nebula Graph 是一個高性能、高可用、強一致的分佈式圖數據庫。因爲 Nebula Graph 採用的是存儲計算分離架構,在存儲層實際只是暴露了簡單的 kv 接口,採用 RocksDB 做爲狀態機,經過 Raft 一致性協議來保證多副本數據一致的問題。Raft 協議雖然比 Paxos 更加容易理解,但在工程實現上仍是有不少須要注意和優化的地方。git

另外,如何測試基於 Raft 的分佈式系統也是困擾業界的問題,目前 Nebula 主要採用了 Jepsen 做爲一致性驗證工具。以前個人小夥伴已經在《Jepsen 測試框架在圖數據庫 Nebula Graph 中的實踐》中作了詳細的介紹,對 Jepsen 不太瞭解的同窗能夠先移步這篇文章。github

在這篇文章中將着重介紹如何經過 Jepsen 來對 Nebula Graph 的分佈式 kv 進行一致性驗證。算法

強一致的定義

首先,咱們須要什麼瞭解叫強一致,它實際就是 Linearizability,也被稱爲線性一致性。引用《Designing Data-Intensive Applications》裏一書裏的定義:數據庫

In a linearizable system, as soon as one client successfully completes a write, all clients reading from the database must be able to see the value just written.

也就是說,強一致的分佈式系統雖然其內部可能有多個副本,但對外暴露的就好像只有一個副本同樣,客戶端的任何讀請求獲取到的都是最新寫入的數據。微信

Jepsen 如何檢查系統是否知足強一致

以一個 Jepsen 測試的 timeline 爲例,採用的模型爲 single-register,也就是整個系統只有一個寄存器(初始值爲空),客戶端只能對該寄存器進行 read 或者 write 操做(全部操做均爲知足原子性,不存在中間狀態)。同時有 4 個客戶端對這個系統發出請求,圖中每個方框的上沿和下沿表明發出請求的時間和收到響應的時間。網絡

image

從客戶端的角度來看,對於任何一次請求,服務端處理這個請求可能發生在從客戶端發出請求到接收到對應的結果這段時間的任何一個時間點。能夠看到在時間上,客戶端 1/3/4 的三個操做 write 1/write 4/read 1 在時間上其實是存在 overlap 的,但咱們能夠經過不一樣客戶端所收到的響應,肯定系統真正的狀態。架構

因爲初始值爲空,客戶端 4 的讀請求卻獲取到了 1,說明客戶端 4 的 read 操做必定在客戶端 1 的 write 1 以後,且 write 4 發生在 write 1 以前(不然會讀出 4),則能夠確認三個操做實際發生的順序爲 write 4 -> write 1 -> read 1。儘管從全局角度看,read 1 的請求最早發出,但實際倒是最後被處理的。後面的幾個操做在時間上是不存在 overlap,是依次發生的,最終客戶端 2 最後讀到了最後一次寫入的 4,整個過程當中沒有違反強一致的定義,驗證經過。app

若是客戶端 3 的那次 read 獲取到的值是 4,那麼整個系統就不是強一致的了,由於根據以前的分析,最後一次成功寫入的值爲 1,而客戶端 3 卻讀到了 4,是一個過時的值,也就違背了線性一致性。事實上,Jepsen 也是經過相似的算法來驗證分佈式系統是否知足強一致的。框架

經過 Jepsen 的一致性驗證找到對應問題

咱們先簡單介紹一下 Nebula Raft 裏面處理一個請求的流程(以三副本爲例),以便更好地理解後面的問題。讀請求相對簡單,因爲客戶端只會將請求發送給 leader,leader 節點只須要在確保本身是 leader 的前提下,直接從狀態機獲取對應結果返回給客戶端便可。分佈式

寫請求的流程則複雜一些,如 Raft Group 圖所示:

  1. Leader(圖中綠色圈) 收到 client 發送的 request,寫入到本身的 wal(write ahead log)中。
  2. Leader將 wal 中對應的 log entry 發送給 follower,並進入等待。
  3. Follower 收到 log entry 後寫入本身的 wal 中(不等待應用到狀態機),並返回成功。
  4. Leader 接收到至少一個 follower 返回成功後,應用到狀態機,向 client 發送 response。

image

下面我將用示例來講明經過 Jepsen 測試在以前的Raft實現中發現的一致性問題:

如上圖所示,ABC 組成一個三副本 raft group,圓圈爲狀態機(爲了簡化,假設其爲一個 single-register),方框中則是保存的相應 log entry。

  • 在初始狀態,三個副本中的狀態機中都爲 1,Leader 爲 A,term爲 1
  • 客戶端發送了 write 2 的請求,Leader 根據上面的流程進行處理,在向 client 告知寫入成功後被 kill。(step 4 完成後)
  • 此後 C 被選爲 term 2 的 leader,但因爲 C 此時有可能尚未將以前 write 2 的 log entry 應用到狀態機(此時狀態機中仍爲1)。若是此時 C 接受到客戶端的讀請求,那麼 C 會直接返回 1。這違背了強一致的定義,以前已經成功寫入 2,卻讀到了過時的結果。

這個問題是出在 C 被選爲 term 2 的 leader 後,須要發送心跳來保證以前 term 的 log entry 被大多數節點接受,在這個心跳成功以前是不能對外提供讀(不然可能會讀到過時數據)。有興趣的同窗能夠參考 raft parer 中的 Figure 8 以及 5.4.2 小節。

從上一個問題出發,經過 Jepsen 咱們又發現了一個相關的問題:leader 如何確保本身仍是 leader?這個問題常常出如今網絡分區的時候,當 leader 由於網絡問題沒法和其餘節點通訊從而被隔離後,此時若是仍然容許處理讀請求,有可能讀到的就是過時的值。爲此咱們引入了 leader lease 的概念。

當某個節點被選爲 leader 以後,該節點須要按期向其餘節點發送心跳,若是心跳確認大多數節點已經收到,則獲取一段時間的租約,並確保在這段時間內不會出現新的 leader,也就保證該節點的數據必定是最新的,從而在這段時間內能夠正常處理讀請求。

image

和 TiKV 的處理方法不一樣的是,咱們沒有采起心跳間隔乘以係數做爲租約時間,主要是考慮到不一樣機器的時鐘漂移不一樣的問題。而是保存了上一次成功的 heartbeat 或者 appendLog 所消耗的時間 cost,用心跳間隔減去 cost 即爲租約時間長度。

當發生網絡分區時, leader 儘管被隔離,可是在這個租約時間仍然能夠處理讀請求(對於寫請求,因爲被隔離,都會告知客戶端寫入失敗), 超出租約時間後則會返回失敗。當 follower 在至少一個心跳間隔時間以上沒有收到 leader 的消息,會發起選舉,選出新 leader 處理後續的客戶端請求。

結語

對於一個分佈式系統,不少問題須要長時間的壓力測試和故障模擬才能發現,經過 Jepsen 可以在不一樣注入故障的狀況下驗證分佈式系統。以後咱們也會考慮採用其餘混沌工程工具來驗證 Nebula Graph,在保證數據高可靠的前提下不斷提升性能。

本文中若有錯誤或疏漏歡迎去 GitHub:https://github.com/vesoft-inc/nebula issue 區向咱們提 issue 或者前往官方論壇:https://discuss.nebula-graph....建議反饋 分類下提建議 👏;加入 Nebula Graph 交流羣,請聯繫 Nebula Graph 官方小助手微信號:NebulaGraphbot

推薦閱讀

做者有話說:Hi,我是 critical27,是 Nebula Graph 的研發工程師,目前主要從事存儲相關的工做,但願能爲圖數據庫領域帶來一些本身的貢獻。但願本文對你有所幫助,若是有錯誤或不足也請與我交流,不甚感激。
相關文章
相關標籤/搜索