Jepsen 是一个用clojure
编写的,用于测试分布式系统的混沌工程测试框架。Jepsen 允许用户编写一系列的读写事件,以及故障注入组合来模拟分布式系统遇到的各种异常,最终通过验证数据的线性一致性或者事务一致性来帮助用户确认分布式系统是否具备一致性读写。
业界主流的数据库,都使用了 jepsen 作为它们的测试框架,例如MySQL
,etcd
,TiDB
等。
jepsen 通过一个控制节点和多个 node 组成的被测集群组成,多个 node 节点形成一个分布式系统。jepsen 通过SSH
的方式来与 node 节点进行通信。而 control 节点本身通常由以下几个部分组成:
setup
方法一般用于安装和启动数据库,teardown
方法用于测试完成后销毁数据库,另外提供logFiles
用于挂载每个 node 节点的日志文件open!
方法用于初始化连接,setup!
用于前置操作,invoke!
执行具体的读写在运行的时候,我们通常会并发的生成多个生成器用于对同一个 key 进行读写,然后对多个 key 来进行统计判断,查看分布式系统的一致性要求。jepsen 的检查器,通过内置的elle检查事务一致性,通过knossos检查线性一致性
我们使用公司内部基于jraft的强一致 KV 存储引擎作为分布式测试系统。一共使用 7 个节点,5 个节点作为 leader 或 follower,2 个节点作为 learner。因为 jraft 是强一致存储,所以我们使用 jepsen 来进行强一致存储验证。
通过db
模块初始化我们的 node 集群
(defn db
"mdb-store DB for a particular version."
[version]
(reify db/DB
(setup! [_ test node]
(info node "installing mdb-store" version)
(c/su
(c/exec :sh mdb-store-download version)
(c/exec :mkdir :-p (str mdb-store-home (str/replace node "." "")))
(c/exec :echo (mdb-store-cfg-servers test)
:> (str mdb-store-home (str/replace node "." "") "/member.conf"))
(c/exec :cp mdb-store-raft-cfg (str mdb-store-home (str/replace node "." "") "/"))
(Thread/sleep 5000)
(c/exec :sh mdb-store-start)
(info node "started")
(Thread/sleep 20000)))
(teardown! [_ test node]
(info node "tearing down mdb-store")
(c/su
(c/exec :sh mdb-store-stop)
(c/exec :rm :-f (str mdb-store-path "mdbStore.jar"))
(c/exec :rm :-rf "/data")))
db/LogFiles
(log-files [_ test node]
[mdb-store-log-file])))
通过Client
模块进行读写操作
(defrecord Client [client]
client/Client
(open! [this test node]
(let [client (str "http://" node ":" mdb-store-port)]
(assoc this :client client)))
(setup! [this node]
(create-table client))
(invoke! [this test op]
(let [[k v] (:value op)
crash (if (= :read (:f op)) :fail :info)]
(try+
(case (:f op)
:read (let [value (get-mdb-store client (pr-str k))]
(assoc op :type :ok, :value (independent/tuple k value)))
:write (do
(write-mdb-store client (pr-str k) v)
(assoc op :type :ok)))
(catch KeyHasExistException e
(assoc op :type :info, :error :key-has-exist))
(catch KeyNotFoundException e
(assoc op :type :info, :error :key-not-found))
(catch IllegalStateException e
(let [^String msg (.getMessage e)]
(assoc op :type :fail, :error msg)))
(catch Exception e
(let [^String msg (.getMessage e)]
(cond
(and msg (.contains msg "TIMEOUT")) (assoc op :type crash, :error :timeout)
:else
(assoc op :type crash :error (.getMessage e))))))))
(teardown! [this test])
(close! [_ test]))
由于我们的业务原因,读写请求都是通过 http 接口对外进行暴露。在系统内部分为多张表,所以初始化会创建一张表。写操作时,会首先查询一下这个 key 是否存在,如果不存在会新增,如果存在会修改,这是调用的不同接口,所以这里我 catch 住了KeyHasExistException
和KeyNotFoundException
两种异常,并且将:type
置为:info
,即该部分报错不参加最终的一致性检查,也不算报错。不过,如果是其他错误类型,将被判断为:fail
类型,会计算为错误
最后,我使用默认的随机网络分区来进行测试
(defn mdb-store-test
"Given an options map from the command line runner (e.g. :nodes, :ssh,\n :concurrency ...), constructs a test map."
[opts]
(merge tests/noop-test
opts
{:name "mdb-store"
:os debian/os
:db (db (:version opts))
:pure-generators true
:client (Client. nil)
:nemesis (nemesis/partition-random-halves)
:checker (checker/compose
{:perf (checker/perf)
:indep (independent/checker
(checker/compose
{:linear (checker/linearizable
{:model (model/cas-register)
:algorithm :linear})
:timeline (timeline/html)}))})
:generator (->> (independent/concurrent-generator
10
(range)
(fn [k]
(->> (gen/mix [r w])
(gen/stagger (/ (:rate opts)))
(gen/limit 100))))
(gen/nemesis
(cycle [(gen/sleep (:interval opts))
{:type :info, :f :start}
(gen/sleep (:interval opts))
{:type :info, :f :stop}]))
(gen/time-limit (:time-limit opts)))}))
我使用 10 个生成器来对相同的 key 进行读写操作,并最终会产生多个 key 来构成测试数据。使用 20 个并发数,以每秒 20 个请求的频率,故障间隔时间为 60s,一共运行 10 分钟。通过漫长的分析,最终输出
Everything looks good! ヽ(‘ー`)ノ
说明我们的读写是满足线性一致性的
根据最终生成的测试报告
由于我们设置的rpc_request_timeout
是 3s,我们可以看出,耗时大于 1000ms 的都是 3s 超时的报错。且网络恢复后,集群都能在 1 分钟内收敛完成,不过还是有少量的耗时较长的请求
总体来看,jepsen 提供了丰富的功能,我们可以自定义修改我们 raft 的配置 (比如可以修改election_timeout
,以模拟快速超时的场景),集群配置,以及把混沌测试接入 CI 流程。jepsen 帮助了我们完成自动化混沌实验,且功能足够强大,后续我们也会基于该脚本进行优化,扩展更多,更复杂的故障注入和检查方式。不过 jepsen 并不是完美的,也存在以下问题
由于 jepsen 的学习成本,所以近几年有不同的厂商基于 jepsen 实现其他语言的版本,例如 pingcap 的chaos,openmessaging 的openchaos。不过从系统的成熟度,还是业界认可度,jepsen 目前还是处于业界最主流的分布式测试框架。
github 仓库:https://github.com/jepsen-io/jepsen