在本篇文章中主要介绍图数据库 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