来源 | 混沌工程实践
作者 | 罗冈庭
头图 | 下载于ICphoto
Jepsen测试框架的工作模式和混沌工程的思想是一脉相承的。Jepsen测试框架可以在分布式系统上注入众多混沌事件,例如引入网络问题、杀死节点和生成随机负载等等,然后通过执行预先定义的测试操作,根据过程记录和结果分析,发现分布式系统(主要是数据库、协调服务和队列)中潜在的一致性问题。
Jepsen 是什么
根据CAP定理,一致性、可用性和分区容错性,分布式系统只能实现这三个关键属性中的两个。大多数分布式系统不会放弃分区容错性,一致性和可用性的权衡,像数据存储服务则偏向于一致性。
Jepsen致力于提高分布式数据库、队列和共识协调系统等的一致性。Jepsen是由凯尔·金斯伯里(Kyle Kingsbury)采用函数式编程语言Clojure编写的测试框架,用来验证分布式系统的一致性。Jepsen于2015年开源。(https://github.com/jepsen-io/jepsen)
Kyle也是开源分布式监控工具Riemann的创始人,可能大家没有听过Riemann。《监控的艺术》(The Art of Monitoring),这本书投入蛮多篇幅来介绍这款低延迟的监控系统。
Jepsen已被用来测试众多分布式系统,例如MongoDB、ElasticSearch和Zookeeper。
自2013年以来,Jepsen已经分析了二十多个业界知名的数据库、协调服务和队列,发现了副本分叉(replica divergence)、数据丢失(data loss)、过期读取(stale reads)、读偏序(read skew)、锁冲突(lock conflicts)等等多类型的一致性问题。(https://jepsen.io/analyses)
Clojure 的难度
Jepsen本身基于Clojure开发,想要深入了解该框架的内部实现以及上述业界知名分布式系统的Jespen测试代码,就需要学会Clojure。Clojure是一种基于JVM的函数式编程语言,跟Java可以进行很好的交互。Jepsen的作者Kyle也写过一篇关于Clojure入门相关的文章。(https://aphyr.com/posts/301-clojure-from-the-ground-up-welcome)
很多人听到函数式编程,就开始瑟瑟发抖,其实大可不必,因为对于Jepsen的使用者而言,看得懂,会修改现有的例子,往往都能满足大多数的需求,不过这确实在一定程度上影响了Jepsen在这个领域的应用范围。
这里就不详细介绍Clojure的安装和配置了。
Jepsen 的工作模式
如下图所示,Jepsen运行在控制节点(Control Node)上,测试过程中会启用生成器(Generator)进程、SSH客户端(Client)、克星(Nemesis)进程和检查器(Checker)进程。
以分布式数据库集群为例,Jepsen的工作大致包含以下几步:
Jepsen在控制节点(Control Node)上作为Clojure程序运行;
部署需要测试的分布式数据库集群(Distributed System),确认其工作正常;
控制节点启动进程,作为分布式数据库节点(DB Node)的SSH客户端;
生成器(Generator)进程为每个SSH客户端生成具体要执行的读写操作;
这些SSH客户端将在分布式数据库中执行这些读写操作;
每个操作从启动到结束的过程都会记录下来;
当读写操作进行时,克星进程(Nemesis)对分布式数据库进行故障注入,同样也是由生成器来调度和管理;
测试完成,分布式数据库集群会被销毁。Jepsen使用检查器(Checker)进程对测试过程的历史记录进行分析,并生成最终的图表和报告。
Jepsen 与混沌工程
采用基于经验和系统的方法解决分布式系统在规模增长时引发的问题, 并以此建立对系统抵御这些事件的能力和信心。在受控实验中观察分布式系统的行为,借此了解系统特性,我们称之为混沌工程。
混沌工程是在分布式系统上进行实验的学科, 目的是建立对系统抵御生产环境中失控条件的能力以及信心。
前面我们了解了Jepsen的工作模式,这和混沌工程的思想是一脉相承的。
Jepsen可以在分布式系统上注入众多混沌事件,例如引入网络问题、杀死节点和生成随机负载等等,然后通过执行预先定义的测试操作,发现分布式系统(主要是数据库、协调服务和队列)中的一致性问题。
详细的Jepsen介绍文档可以参考这里:https://github.com/jepsen-io/jepsen/blob/main/doc/tutorial/index.md
Hello World 示例
今天,我们先以一个空白场景为例,感受下Jepsen的用法。
在容器中进行Jepsen测试,需要多种类型的容器配合部署:
jepsen-control
: 控制节点,管理其他节点,创建和销毁,生成测试操作和故障注入场景jepsen-nX
: 目标分布式数据库集群(默认是5个节点)jepsen-node
: 克星节点,负责故障注入
下面的链接中提供了docker-compose up
的方式,快速部署个Jepsen测试环境:https://github.com/jepsen-io/jepsen/tree/main/docker
完成部署之后,要在容器中运行 Jepsen 测试,则需要通过以下命令
$ docker exec-ti jepsen-control bash
进入jepsen-control
节点,该容器中已经安装了Java和Clojure运行环境,以及lein(Clojure集成开发工具),然后就创建一个etcd的示例测试项目:
$ lein new jepsen.etcdemoGenerating a project called jepsen.etcdemo based on the 'default'template.Thedefaulttemplateis intended for library projects, not applications.To see other templates (app, plugin, etc), try`lein help new`.$ cd jepsen.etcdemo$ lsCHANGELOG.md doc/ LICENSE project.clj README.md resources/ src/ test/
像任何新的Clojure项目一样,项目文件夹下包含:
一个空白的变更日志
一个文档目录
一份 Eclipse 公共许可证的副本
一个
project.clj
,说明了lein
如何构建和运行代码一个描述文件
一个
resources
目录,存放数据文件的地方,例如:要测试的数据库的配置文件。一个
src
目录,存有源代码一个
test
目录,里面是大多数 Clojure 库的测试约定,我们不会在这里使用它。
先对project.clj
进行修改,指定项目的依赖项和其他元数据。添加一个:main
的命名空间jepsen.etcdemo
,这就是我们从命令行运行测试的入口。
除了依赖Clojure语言本身,我们还将引入2个依赖库:Jepsen
和Verschlimmbesserung
,后者用于与etcd的交互。
(defproject jepsen.etcdemo "0.1.0-SNAPSHOT":description "A Jepsen test for etcd":license {:name "Eclipse Public License":url "http://www.eclipse.org/legal/epl-v10.html"}:main jepsen.etcdemo:dependencies [[org.clojure/clojure "1.10.0"][jepsen "0.2.1-SNAPSHOT"][verschlimmbesserung "0.1.3"]])
我们来执行下lein run
$ lein runExceptionin thread "main" java.lang.Exception: Cannot find anything to run for: jepsen.etcdemo, compiling:(/tmp/form-init6673004597601163646.clj:1:73)...
回想下,刚添加一个:main
的命名空间jepsen.etcdemo
,里面现在还没有东西,我们需要在src/jepsen/etcdemo.clj
中添加具体的测试操作。
(ns jepsen.etcdemo)(defn -main"Handles command line arguments. Can either run a test, or a web server for browsing results."[& args](prn "Hello, world!" args))
我们再来执行下lein run
$ lein run hi there"Hello, world!"("hi""there")
可以跑起来了!
让我们使用jepsen.cli命名空间,简称cli,并将我们的main函数转换为Jepsen测试运行程序:
(ns jepsen.etcdemo(:require[jepsen.cli :as cli][jepsen.tests :as tests]))(defn etcd-test"Given an options map from the command line runner (e.g. :nodes, :ssh,:concurrency, ...), constructs a test map."[opts](merge tests/noop-test{:pure-generators true} opts))(defn -main"Handles command line arguments. Can either run a test, or a web server for browsing results."[& args](cli/run! (cli/single-test-cmd {:test-fn etcd-test}) args))
cli/single-test-cmd
由jepsen.cli
提供,将会解析命令行参数,并调用:test-fn
,该函数会返回一个包含Jepsen测试所需的所有信息映射。在这种情况下,测试函数etcd-test
接受命令行参数,这里选择的是空测试,即noop-test
。
是时候跑一下测试看看了!
$ lein run test13:04:30.927[main] INFO jepsen.cli - Test options:{:concurrency 5,:test-count 1,:time-limit 60,:nodes ["n1""n2""n3""n4""n5"],:ssh{:username "root",:password "root",:strict-host-key-checking false,:private-key-path nil}}INFO [2018-02-0213:04:30,994] jepsen test runner - jepsen.core Running test:...INFO [2018-02-0213:04:35,389] jepsen nemesis - jepsen.core Starting nemesisINFO [2018-02-0213:04:35,389] jepsen worker 1- jepsen.core Starting worker 1INFO [2018-02-0213:04:35,389] jepsen worker 2- jepsen.core Starting worker 2INFO [2018-02-0213:04:35,389] jepsen worker 0- jepsen.core Starting worker 0INFO [2018-02-0213:04:35,390] jepsen worker 3- jepsen.core Starting worker 3INFO [2018-02-0213:04:35,390] jepsen worker 4- jepsen.core Starting worker 4INFO [2018-02-0213:04:35,391] jepsen nemesis - jepsen.core Running nemesisINFO [2018-02-0213:04:35,391] jepsen worker 1- jepsen.core Running worker 1INFO [2018-02-0213:04:35,391] jepsen worker 2- jepsen.core Running worker 2INFO [2018-02-0213:04:35,391] jepsen worker 0- jepsen.core Running worker 0INFO [2018-02-0213:04:35,391] jepsen worker 3- jepsen.core Running worker 3INFO [2018-02-0213:04:35,391] jepsen worker 4- jepsen.core Running worker 4INFO [2018-02-0213:04:35,391] jepsen nemesis - jepsen.core Stopping nemesisINFO [2018-02-0213:04:35,391] jepsen worker 1- jepsen.core Stopping worker 1INFO [2018-02-0213:04:35,391] jepsen worker 2- jepsen.core Stopping worker 2INFO [2018-02-0213:04:35,391] jepsen worker 0- jepsen.core Stopping worker 0INFO [2018-02-0213:04:35,391] jepsen worker 3- jepsen.core Stopping worker 3INFO [2018-02-0213:04:35,391] jepsen worker 4- jepsen.core Stopping worker 4INFO [2018-02-0213:04:35,397] jepsen test runner - jepsen.core Run complete, writingINFO [2018-02-0213:04:35,434] jepsen test runner - jepsen.core AnalyzingINFO [2018-02-0213:04:35,435] jepsen test runner - jepsen.core Analysis completeINFO [2018-02-0213:04:35,438] jepsen results - jepsen.store Wrote/home/aphyr/jepsen/jepsen.etcdemo/store/noop/20180202T130430.000-0600/results.ednINFO [2018-02-0213:04:35,440] main - jepsen.core {:valid? true}Everything looks good! ヽ(‘ー`)ノ
我们可以看到,Jepsen启动了一系列SSH客户端进程,负责对数据库执行测试操作,还有就是启用了故障注入的克星进程。这里我们并没有执行任何操作,所以立即停止了。此后,Jepsen会将这个测试结果写到store目录中,并打印出一个简短的分析。
$ ls store/latest/history.txt jepsen.log results.edn test.fressian
history.txt
显示测试执行的操作,这里应该是空的,因为noop-test
不执行任何操作;jepsen.log
存有该测试的控制台日志;results.edn
包含了对测试的分析;最后,test.fressian
助力事后分析的测试原始数据,包括完整的机器可读的历史记录和分析结果。
若此时遇到SSH错误,应检查SSH客户端是否正常运行,并且是否已加载所有节点的密钥。
$ lein run test --help#object[jepsen.cli$test_usage 0x7ddd84b5 jepsen.cli$test_usage@7ddd84b5]-h, --help Printoutthis message andexit-n, --node HOSTNAME ["n1""n2""n3""n4""n5"] Node(s) to run test on--nodes-file FILENAME File containing node hostnames, one per line.--username USER root Usernamefor logins--password PASS root Passwordfor sudo access--strict-host-key-checking Whether to check host keys--ssh-private-key FILE Path to an SSH identity file--concurrency NUMBER 1nHow many workers should we run? Must be an integer, optionally followed by n (e.g. 3n) to multiply by the number of nodes.--test-count NUMBER 1How many times should we repeat a test?--time-limit SECONDS 60Excluding setup and teardown, how long should a test run for, in seconds?
最后,我们就可以销毁分布式系统集群的所有节点了。
结束语
本文我们介绍Jepsen测试框架的来历、基本的工作模式、与混沌工程的关系,也体会了一下Jepsen的Hello World示例。
虽然我们还没有真正选择一个分布式系统,进行实际的测试操作,也没有同时进行故障注入,但至少我们体验了一把Jepsen测试所需要的函数式编程语法。
后面我们会以一个具体的例子来详实地分享,如何使用Jepsen检验分布式系统的一致性问题。
更多精彩推荐
云上数据保护,你以为挡住黑客就够了?在这次采访中,Mendix 披露了低代码方法论DeVOpS 实战:Kubernetes 微服务监控体系还在担心无代码是否威胁程序员饭碗?