在本篇文章中主要介紹圖數據庫 Nebula Graph 在 Jepsen 這塊的實踐。html
Jepsen 是一款用於系統測試的開源軟件庫,致力於提升分佈式數據庫、隊列、共識系統等的安全性。做者 Kyle Kingsbury 使用函數式編程語言 Clojure 編寫了這款測試框架,並對多個著名的分佈式系統和數據庫進行了一致性測試。目前 Jepsen 仍在 GitHub 保持活躍,可否經過 Jepsen 的測試已經成爲各個分佈式數據庫對自身檢驗的一個標杆。node
Jepsen 測試推薦使用 Docker 搭建集羣。默認狀況下由 6 個 container 組成,其中一個是控制節點(control node),另外 5 個是數據庫的節點(默認爲 n1-n5)。控制節點在測試程序開始後會啓用多個 worker 進程,併發地經過 SSH 登入數據庫節點進行讀寫操做。git
測試開始後,控制節點會建立一組進程,進程包含了待測試分佈式系統的客戶端。另外一個 Generator 進程產生每一個客戶端執行的操做,並將操做應用於待測試的分佈式系統。每一個操做的開始和結束以及操做結果記錄在歷史記錄中。同時,一個特殊進程 Nemesis 將故障引入系統。github
測試結束後,Checker 分析歷史記錄是否正確,是否符合一致性。用戶可使用 Jepsen 的 knossos 中提供的驗證模型,也能夠本身定義符合需求的模型對測試結果進行驗證。同時,還能夠在測試中注入錯誤對集羣進行干擾測試。shell
最後根據本次測試所規定的驗證模型對結果進行分析。數據庫
使用 Jepsen 過程當中可能會遇到一些問題,能夠參考一下使用 Tips:編程
分佈式圖數據庫 Nebula Graph 主要由 3 部分組成,分別是 meta 層,graph 層和 storage 層。安全
咱們在使用 Jepsen 對 kv 存儲接口進行的測試中,搭建了一個由 8 個 container 組成的集羣:一個 Jepsen 的控制節點,一個 meta 節點,一個 graph 節點,和 5 個 storage 節點,集羣由 Docker-compose 啓動。須要注意的是,要創建一個集羣的 subnet 網絡,使集羣能夠連通,另外要安裝 ssh 服務,併爲 control node 與 5 個 storage 節點配置免密登入。微信
測試中使用了 Java 編寫的客戶端程序,生成 jar 包並加入到 Clojure 程序依賴,來對 DB 進行 put,get 和 cas (compare-and-set) 操做。另外 Nebula Graph 的客戶端有自動重試邏輯,當遇到錯誤致使操做失敗時,客戶端會啓用適當的重試機制以盡力確保操做成功。網絡
Nebula-Jepsen 的測試程序目前分爲三種常見的測試模型和三種常見的錯誤注入。
模擬一個寄存器,程序併發地對數據庫進行讀寫操做,每次成功的寫入操做都會使寄存器中存儲的值發生變化,而後經過對比每次從數據庫讀出的值是否和寄存器中記錄的值一致,來驗證結果是否知足線性要求。因爲寄存器是單一的,因此在此處咱們生成惟一的 key,隨機的 value 進行操做。
一個能夠存不一樣鍵的寄存器。和單一寄存器的效果同樣,但此處咱們可使 key 也隨機生成了。
4 :invoke :write [[:w 9 1]] 4 :ok :write [[:w 9 1]] 3 :invoke :read [[:r 5 nil]] 3 :ok :read [[:r 5 3]] 0 :invoke :read [[:r 7 nil]] 0 :ok :read [[:r 7 2]] 0 :invoke :write [[:w 7 1]] 0 :ok :write [[:w 7 1]] 1 :invoke :read [[:r 1 nil]] 1 :ok :read [[:r 1 4]] 0 :invoke :read [[:r 8 nil]] 0 :ok :read [[:r 8 3]] :nemesis :info :start nil :nemesis :info :start [:isolated {"n5" #{"n2" "n1" "n4" "n3"}, "n2" #{"n5"}, "n1" #{"n5"}, "n4" #{"n5"}, "n3" #{"n5"}}] 1 :invoke :write [[:w 4 2]] 1 :ok :write [[:w 4 2]] 2 :invoke :read [[:r 5 nil]] 3 :invoke :write [[:w 1 2]] 2 :ok :read [[:r 5 3]] 3 :ok :write [[:w 1 2]] 0 :invoke :read [[:r 4 nil]] 0 :ok :read [[:r 4 2]] 1 :invoke :write [[:w 6 4]] 1 :ok :write [[:w 6 4]]
以上片斷是截取的測試中一小部分不一樣的讀寫操做示例,
其中最左邊的數字是執行此次操做的 worker,也就是進程號。每發起一次操做,標誌都是 invoke,接下來一列會指出是 write 仍是 read操做,而以後一列的中括號內,則顯示了具體的操做,好比
:invoke :read [[:r 1 nil]]
就是讀取 key 爲 1 的值,由於是 invoke,操做剛剛開始,還不知道值是什麼,因此後面是 nil。:ok :read [[:r 1 4]]
中的 ok 則表示操做成功,能夠看到讀取到鍵 1 對應的值是 4。在這個片斷中,還能夠看到一次 nemesis 被注入的時刻。
:nemesis :info :start nil
標誌着 nemesis 的開始,後面的的內容 (:isolated ...)
表示了節點 n5 從整個集羣中被隔離,沒法與其餘 DB 節點進行網絡通訊。這是一個驗證 CAS 操做的寄存器。除了讀寫操做外,此次咱們還加入了隨機生成的 CAS 操做,cas-register 將會對結果進行線性分析。
0 :invoke :read nil 0 :ok :read 0 1 :invoke :cas [0 2] 1 :ok :cas [0 2] 4 :invoke :read nil 4 :ok :read 2 0 :invoke :read nil 0 :ok :read 2 2 :invoke :write 0 2 :ok :write 0 3 :invoke :cas [2 2] :nemesis :info :start nil 0 :invoke :read nil 0 :ok :read 0 1 :invoke :cas [1 3] :nemesis :info :start {"n1" ""} 3 :fail :cas [2 2] 1 :fail :cas [1 3] 4 :invoke :read nil 4 :ok :read 0
一樣的,在此次測試中,咱們採用惟一的鍵值,好比全部寫入和讀取操做都是對鍵 "f" 執行,在顯示上省略了中括號中的鍵,只顯示是什麼值。
:invoke :read nil
表示開始一次讀取 「f」 的值的操做,由於剛開始操做,因此結果是 nil(空)。:ok :read 0
表示成功讀取到了鍵 「f」 的值爲 0。:invoke :cas [1 2]
意思是進行 CAS 操做,當讀到的值爲 1 時,將值改成 2。在第二行能夠看到,當保存的 value 是 0 時,在第 4 行 cas[0 2]
會將 value 變爲 2。在第 14 行當值爲 0時,17 行的 cas[2 2] 就失敗了。
第 16 行顯示了 n1 節點被殺掉的操做,第 1七、18 行會有兩個 cas 失敗(fail)
Jepsen 的控制節點會在整個測試過程當中,屢次隨機 kill 某一節點中的數據庫服務而使服務中止。此時集羣中就少了一個節點。而後在必定時間後再將該節點的數據庫服務啓動,使之從新加入集羣。
Jepsen 會在測試過程當中,屢次隨機將某一節點與其餘節點網絡隔離,使該節點沒法與其餘節點通訊,其餘節點也沒法和它通訊。而後在必定時間後再恢復這一網絡隔離,使集羣恢復原狀。
在這種常見的網絡分區情景下,Jepsen 控制節點會將 5 個 DB 節點隨機分紅兩部分,一部分爲兩個節點,另外一部分爲三個。必定時間後恢復通訊。以下圖所示。
Jepsen 會根據需求對測試結果進行分析,並得出本次測試的結果,能夠看到控制檯的輸出,本次測試是經過的。
2020-01-08 03:24:51,742{GMT} INFO [jepsen test runner] jepsen.core: {:timeline {:valid? true}, :linear {:valid? true, :configs ({:model {:value 0}, :last-op {:process 0, :type :ok, :f :write, :value 0, :index 597, :time 60143184600}, :pending []}), :analyzer :linear, :final-paths ()}, :valid? true} Everything looks good! ヽ(‘ー`)ノ
Jepsen 在測試執行過程當中會自動生成一個名爲 timeline.html 文件,如下爲本次實踐生成的 timeline.html 文件部分截圖
上面的圖片展現了測試中執行操做的時間軸片斷,每一個執行塊有對應的執行信息,Jepsen 會將整個時間軸生成一個 HTML 文件。
Jepsen 就是這樣按照順序的歷史操做記錄進行 Linearizability 一致性驗證,這也是 Jepsen 的核心。咱們也能夠經過這個 HTML 文件來幫助咱們溯源錯誤。
下面是一些 Jepsen 生成的性能分析圖表,本次實踐項目名爲「basic-test」各位讀者閱讀時請自行腦補爲你項目名。
能夠看到,這一張圖表展現了 Nebula Graph 的讀寫操做延時。其中上方灰色的區域是錯誤注入的時段,在本次測試咱們注入了隨機 kill node。
而在這一張圖展現了讀寫操做的成功率,咱們能夠看出,最下方紅色集中突出的地方爲出現失敗的地方,這是由於 control node 在殺死節點時終止了某個 partition 的 leader 中的 nebula 服務。集羣此時須要從新選舉,在選舉出新的 leader 以後,讀寫操做也恢復到正常了。
經過觀察測試程序運行結果和分析圖表,能夠看到 Nebula Graph 完成了本次在單寄存器模型中注入 kill-node 錯誤的測試,讀寫操做延時也均處於正常範圍。
Jepsen 自己也存在一些不足,好比測試沒法長時間運行,由於大量數據在校驗階段會形成 Out of Memory。
但在實際場景中,許多 bug 須要長時間的壓力測試、故障模擬才能發現,同時系統的穩定性也須要長時間的運行才能被驗證。但與此同時,在使用 Jepsen 對 Nebula Graph 進行測試的過程當中,咱們也發現了一些以前沒有遇到過的 Bug,甚至其中一些在使用中可能永遠也不會出現。
目前,咱們已經在平常開發過程當中使用 Jepsen 對 Nebula Graph 進行測試。Nebula Graph 有代碼更新後,每晚都將編譯好的項目發佈在 Docker Hub 中,Nebula-Jepsen 將自動下拉最新的鏡像進行持續測試。
最後是 Nebula 的 GitHub 地址,歡迎你們試用,有什麼問題能夠向咱們提 issue。GitHub 地址:https://github.com/vesoft-inc/nebula, 加入 Nebula Graph 交流羣,請聯繫 Nebula Graph 官方小助手微信號:NebulaGraphbot