在本篇文章中主要介紹圖數(shù)據(jù)庫 Nebula Graph 在 Jepsen 這塊的實踐裆悄。
Jepsen 簡介
Jepsen 是一款用于系統(tǒng)測試的開源軟件庫梧油,致力于提高分布式數(shù)據(jù)庫、隊列鳞疲、共識系統(tǒng)等的安全性奴迅。作者 Kyle Kingsbury 使用函數(shù)式編程語言 Clojure 編寫了這款測試框架,并對多個著名的分布式系統(tǒng)和數(shù)據(jù)庫進行了一致性測試岖瑰。目前 Jepsen 仍在 GitHub 保持活躍,能否通過 Jepsen 的測試已經(jīng)成為各個分布式數(shù)據(jù)庫對自身檢驗的一個標(biāo)桿砂代。
Jepsen 的測試流程
Jepsen 測試推薦使用 Docker 搭建集群蹋订。默認情況下由 6 個 container 組成,其中一個是控制節(jié)點(control node)刻伊,另外 5 個是數(shù)據(jù)庫的節(jié)點(默認為 n1-n5)露戒。控制節(jié)點在測試程序開始后會啟用多個 worker 進程捶箱,并發(fā)地通過 SSH 登入數(shù)據(jù)庫節(jié)點進行讀寫操作智什。
測試開始后,控制節(jié)點會創(chuàng)建一組進程丁屎,進程包含了待測試分布式系統(tǒng)的客戶端荠锭。另一個 Generator 進程產(chǎn)生每個客戶端執(zhí)行的操作,并將操作應(yīng)用于待測試的分布式系統(tǒng)晨川。每個操作的開始和結(jié)束以及操作結(jié)果記錄在歷史記錄中证九。同時,一個特殊進程 Nemesis 將故障引入系統(tǒng)共虑。
測試結(jié)束后愧怜,Checker 分析歷史記錄是否正確,是否符合一致性妈拌。用戶可以使用 Jepsen 的 knossos 中提供的驗證模型拥坛,也可以自己定義符合需求的模型對測試結(jié)果進行驗證。同時,還可以在測試中注入錯誤對集群進行干擾測試猜惋。
最后根據(jù)本次測試所規(guī)定的驗證模型對結(jié)果進行分析丸氛。
如何使用 Jepsen
使用 Jepsen 過程中可能會遇到一些問題,可以參考一下使用 Tips:
- 在 Jepsen 框架中著摔,用戶需要在 DB 接口中對自己的數(shù)據(jù)庫定義下載缓窜,安裝,啟動與終止操作梨撞。在終止后雹洗,可以將 log 文件清除香罐,同時也可以指定 log 的存儲位置卧波,Jepsen 會將其拷貝至 Jepsen 的 log 文件夾中,以便連同 Jepsen 自身的 log 進行分析庇茫。
- 用戶還需要提供訪問自己數(shù)據(jù)庫的客戶端港粱,這個客戶端可以是你用 Clojure 實現(xiàn)的,比如 etcd 的verschlimmbesserung旦签,也可以是 JDBC查坪,等等。然后需要定義 Client 接口宁炫,告訴 Jepsen 如何對你的數(shù)據(jù)庫進行操作偿曙。
- 在 Checker 中,你可以選擇需要的測試模型羔巢,比如望忆,性能測試(checker/perf)將會生成 latency 和整個測試過程的圖表,時間軸(timeline/html)會生成一個記錄著所有操作時間軸的 html 頁面竿秆。
- 另外一個不可或缺的組件就是在 nemesis 中注入想要測試的錯誤了启摄。網(wǎng)絡(luò)分區(qū)(nemesis/partition-random-halves)和殺掉數(shù)據(jù)節(jié)點(kill-node)是比較常見的注入錯誤。
- 在 Generator 中幽钢,用戶可以告知 worker 進程需要生成哪些操作歉备,每一次操作的時間間隔,每一次錯誤注入的時間間隔等等匪燕。
用 Jepsen 測試圖數(shù)據(jù)庫 Nebula Graph
分布式圖數(shù)據(jù)庫 Nebula Graph 主要由 3 部分組成蕾羊,分別是 meta 層,graph 層和 storage 層帽驯。
[圖片上傳失敗...(image-e66ba4-1579054256513)]
我們在使用 Jepsen 對 kv 存儲接口進行的測試中肚豺,搭建了一個由 8 個 container 組成的集群:一個 Jepsen 的控制節(jié)點,一個 meta 節(jié)點界拦,一個 graph 節(jié)點吸申,和 5 個 storage 節(jié)點,集群由 Docker-compose 啟動。需要注意的是截碴,要建立一個集群的 subnet 網(wǎng)絡(luò)梳侨,使集群可以連通,另外要安裝 ssh 服務(wù)日丹,并為 control node 與 5 個 storage 節(jié)點配置免密登入走哺。
測試中使用了 Java 編寫的客戶端程序,生成 jar 包并加入到 Clojure 程序依賴哲虾,來對 DB 進行 put丙躏,get 和 cas (compare-and-set) 操作。另外 Nebula Graph 的客戶端有自動重試邏輯束凑,當(dāng)遇到錯誤導(dǎo)致操作失敗時晒旅,客戶端會啟用適當(dāng)?shù)闹卦嚈C制以盡力確保操作成功。
Nebula-Jepsen 的測試程序目前分為三種常見的測試模型和三種常見的錯誤注入汪诉。
Jepsen 測試模型
single-register
模擬一個寄存器废恋,程序并發(fā)地對數(shù)據(jù)庫進行讀寫操作,每次成功的寫入操作都會使寄存器中存儲的值發(fā)生變化扒寄,然后通過對比每次從數(shù)據(jù)庫讀出的值是否和寄存器中記錄的值一致鱼鼓,來驗證結(jié)果是否滿足線性要求。由于寄存器是單一的该编,所以在此處我們生成唯一的 key迄本,隨機的 value 進行操作。
multi-register
一個可以存不同鍵的寄存器课竣。和單一寄存器的效果一樣嘉赎,但此處我們可以使 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]]
以上片段是截取的測試中一小部分不同的讀寫操作示例稠氮,
[圖片上傳失敗...(image-96d94b-1579054256513)]
其中最左邊的數(shù)字是執(zhí)行這次操作的 worker曹阔,也就是進程號。每發(fā)起一次操作隔披,標(biāo)志都是 invoke赃份,接下來一列會指出是 write 還是 read操作,而之后一列的中括號內(nèi)奢米,則顯示了具體的操作抓韩,比如
-
:invoke :read [[:r 1 nil]]
就是讀取 key 為 1 的值,因為是 invoke鬓长,操作剛剛開始谒拴,還不知道值是什么,所以后面是 nil涉波。 -
:ok :read [[:r 1 4]]
中的 ok 則表示操作成功英上,可以看到讀取到鍵 1 對應(yīng)的值是 4炭序。
在這個片段中,還可以看到一次 nemesis 被注入的時刻苍日。
-
:nemesis :info :start nil
標(biāo)志著 nemesis 的開始惭聂,后面的的內(nèi)容(:isolated ...)
表示了節(jié)點 n5 從整個集群中被隔離,無法與其他 DB 節(jié)點進行網(wǎng)絡(luò)通信相恃。
cas-register
這是一個驗證 CAS 操作的寄存器辜纲。除了讀寫操作外,這次我們還加入了隨機生成的 CAS 操作拦耐,cas-register 將會對結(jié)果進行線性分析耕腾。
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" 執(zhí)行,在顯示上省略了中括號中的鍵火脉,只顯示是什么值牵舵。
-
:invoke :read nil
表示開始一次讀取 “f” 的值的操作柒啤,因為剛開始操作倦挂,所以結(jié)果是 nil(空)。 -
:ok :read 0
表示成功讀取到了鍵 “f” 的值為 0担巩。 -
:invoke :cas [1 2]
意思是進行 CAS 操作方援,當(dāng)讀到的值為 1 時,將值改為 2涛癌。
在第二行可以看到犯戏,當(dāng)保存的 value 是 0 時,在第 4 行 cas[0 2]
會將 value 變?yōu)?2拳话。在第 14 行當(dāng)值為 0時先匪,17 行的 cas[2 2] 就失敗了。
第 16 行顯示了 n1 節(jié)點被殺掉的操作弃衍,第 17呀非、18 行會有兩個 cas 失敗(fail)
Jepsen 錯誤注入
kill-node
Jepsen 的控制節(jié)點會在整個測試過程中镜盯,多次隨機 kill 某一節(jié)點中的數(shù)據(jù)庫服務(wù)而使服務(wù)停止岸裙。此時集群中就少了一個節(jié)點。然后在一定時間后再將該節(jié)點的數(shù)據(jù)庫服務(wù)啟動速缆,使之重新加入集群降允。
partition-random-node
Jepsen 會在測試過程中,多次隨機將某一節(jié)點與其他節(jié)點網(wǎng)絡(luò)隔離艺糜,使該節(jié)點無法與其他節(jié)點通信剧董,其他節(jié)點也無法和它通信幢尚。然后在一定時間后再恢復(fù)這一網(wǎng)絡(luò)隔離,使集群恢復(fù)原狀翅楼。
partition-random-halves
在這種常見的網(wǎng)絡(luò)分區(qū)情景下侠草,Jepsen 控制節(jié)點會將 5 個 DB 節(jié)點隨機分成兩部分,一部分為兩個節(jié)點犁嗅,另一部分為三個边涕。一定時間后恢復(fù)通信。如下圖所示褂微。
測試結(jié)束后
Jepsen 會根據(jù)需求對測試結(jié)果進行分析功蜓,并得出本次測試的結(jié)果,可以看到控制臺的輸出宠蚂,本次測試是通過的式撼。
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! ヽ(‘ー`)ノ
自動生成的 timeline.html 文件
Jepsen 在測試執(zhí)行過程中會自動生成一個名為 timeline.html 文件,以下為本次實踐生成的 timeline.html 文件部分截圖
[圖片上傳失敗...(image-eda2e2-1579054256514)]
上面的圖片展示了測試中執(zhí)行操作的時間軸片段求厕,每個執(zhí)行塊有對應(yīng)的執(zhí)行信息著隆,Jepsen 會將整個時間軸生成一個 HTML 文件。
Jepsen 就是這樣按照順序的歷史操作記錄進行 Linearizability 一致性驗證呀癣,這也是 Jepsen 的核心美浦。我們也可以通過這個 HTML 文件來幫助我們溯源錯誤。
Jepsen 生成的性能分析圖
下面是一些 Jepsen 生成的性能分析圖表项栏,本次實踐項目名為「basic-test」各位讀者閱讀時請自行腦補為你項目名浦辨。
[圖片上傳失敗...(image-c454a8-1579054256514)]
可以看到,這一張圖表展示了 Nebula Graph 的讀寫操作延時沼沈。其中上方灰色的區(qū)域是錯誤注入的時段流酬,在本次測試我們注入了隨機 kill node。
而在這一張圖展示了讀寫操作的成功率列另,我們可以看出芽腾,最下方紅色集中突出的地方為出現(xiàn)失敗的地方,這是因為 control node 在殺死節(jié)點時終止了某個 partition 的 leader 中的 nebula 服務(wù)页衙。集群此時需要重新選舉摊滔,在選舉出新的 leader 之后,讀寫操作也恢復(fù)到正常了拷姿。
通過觀察測試程序運行結(jié)果和分析圖表惭载,可以看到 Nebula Graph 完成了本次在單寄存器模型中注入 kill-node 錯誤的測試,讀寫操作延時也均處于正常范圍响巢。
小結(jié)
Jepsen 本身也存在一些不足描滔,比如測試無法長時間運行,因為大量數(shù)據(jù)在校驗階段會造成 Out of Memory踪古。
但在實際場景中含长,許多 bug 需要長時間的壓力測試券腔、故障模擬才能發(fā)現(xiàn),同時系統(tǒng)的穩(wěn)定性也需要長時間的運行才能被驗證拘泞。但與此同時纷纫,在使用 Jepsen 對 Nebula Graph 進行測試的過程中,我們也發(fā)現(xiàn)了一些之前沒有遇到過的 Bug陪腌,甚至其中一些在使用中可能永遠也不會出現(xiàn)辱魁。
目前,我們已經(jīng)在日常開發(fā)過程中使用 Jepsen 對 Nebula Graph 進行測試诗鸭。Nebula Graph 有代碼更新后染簇,每晚都將編譯好的項目發(fā)布在 Docker Hub 中,Nebula-Jepsen 將自動下拉最新的鏡像進行持續(xù)測試强岸。
最后是 Nebula 的 GitHub 地址锻弓,歡迎大家試用,有什么問題可以向我們提 issue蝌箍。GitHub 地址:https://github.com/vesoft-inc/nebula青灼,加入 Nebula Graph 交流群,請聯(lián)系 Nebula Graph 官方小助手微信號:NebulaGraphbot
參考文獻
- Jepsen 主頁:https://jepsen.io/
- Jepsen GitHub:https://github.com/jepsen-io/jepsen
- Raft Understandable Distributed Consensus
- The Raft Consensus Algorithm
- Nebula Graph GitHub:https://github.com/vesoft-inc/nebula