1.测试脚手架
在本新手教程中,我们打算为etcd编写一个测试。etcd是一个分布式共识系统。在此,我想建议各位在学习过程中能自己亲手敲一下代码,即便一开始还不是特别理解所有内容。如此一来既能帮你学得更快,也不会在我们开始修改更复杂函数代码时而感到茫然。
我们首先在任意目录下创建一个新的Leiningen(音读['laɪnɪŋən])项目。
$ lein new jepsen.etcdemo
Generating a project called jepsen.etcdemo based on the 'default' template.
The default template is intended for library projects, not applications.
To see other templates (app, plugin, etc), try `lein help new`.
$ cd jepsen.etcdemo
$ ls
CHANGELOG.md doc/ LICENSE project.clj README.md resources/ src/ test/
正如任何一个新创建的Clojure(音读/ˈkloʊʒər/)项目那样,我们会得到一个空白的变更日志、一个用于建立文档的目录、一个Eclipse公共许可证副本、一个project.clj文件(该文件告诉leiningen如何构建和运行我们的代码)以及一个名为README的自述文件。resources目录是用于存放数据文件的地方,比如我们想进行测试的数据库的配置文件。src目录存放着源代码,并按照代码中命名空间的结构被组织成一系列目录和文件。test目录是用于存放测试代码的目录。值得一提的是,这整个目录就是一个“Jepsen测试”;test目录是沿袭大多数Clojure库的习惯生成,而在本文中,我们不会用到它。
我们将从编辑一个指定项目的依赖项和其他元数据的project.clj文件来开始。我们将增加一个:main命名空间,正如下面一段命令行所示。除了依赖于Clojure自身的语言库,我们还添加了Jepsen库和一个用于与etcd进行通信的Verschlimmbesserung库。
(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 run
Exception in thread "main" java.lang.Exception: Cannot find anything to run for: jepsen.etcdemo, compiling:(/tmp/form-init6673004597601163646.clj:1:73)
...
运行完后看到这样的数据结果并不意外,因为我们尚未写任何实质性的代码让程序去运行。在jepsen.etcdemo命名空间下,我们需要一个main函数来接收命令行参数并运行测试。在src/jepsen/etcdemo.clj文件中我们定义如下main函数:
(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))
Clojure默认接收跟在lein run指令后的所有参数作为-main函数的调用参数。main函数接收长度可变的参数(即“&”符号),参数列表叫做args。在上述这段代码中,我们在“Hello World”之后打印参数列表。
$ lein run hi there
"Hello, world!" ("hi" "there")
Jepsen囊括了一些用于处理参数、运行测试、错误处理和日志记录等功能的脚手架。现在不妨引入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-cmdjepsen.cli提供。它为测试解析命令行的参数并调用提供的:test-fn,然后应该返回一个包含Jepsen运行测试所需的所有信息的键值映射表(map)。在上面这个样例中,测试函数etcd-test从命令行中接受一些选项,然后用它们来填充一个什么都不处理的空测试(即noop-test)。
$ lein run
Usage: lein run -- COMMAND [OPTIONS ...]
Commands: test
如上述代码块所示,没有参数的话,cli/run!会输出一个基本的帮助信息,提醒我们它接收一个命令作为其第一个参数。现在让我们尝试添加一下test命令吧!
$ lein run test
13: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-02 13:04:30,994] jepsen test runner - jepsen.core Running test:
{:concurrency 5,
:db
#object[jepsen.db$reify__1259 0x6dcf7b6a "[email protected]"],
:name "noop",
:start-time
#object[org.joda.time.DateTime 0x79d4ff58 "2018-02-02T13:04:30.000-06:00"],
:net
#object[jepsen.net$reify__3493 0xae3c140 "[email protected]"],
:client
#object[jepsen.client$reify__3380 0x20027c44 "[email protected]"],
:barrier
#object[java.util.concurrent.CyclicBarrier 0x2bf3ec4 "[email protected]"],
:ssh
{:username "root",
:password "root",
:strict-host-key-checking false,
:private-key-path nil},
:checker
#object[jepsen.checker$unbridled_optimism$reify__3146 0x1410d645 "[email protected]"],
:nemesis
#object[jepsen.nemesis$reify__3574 0x4e6cbdf1 "[email protected]"],
:active-histories #<[email protected]: #{}>,
:nodes ["n1" "n2" "n3" "n4" "n5"],
:test-count 1,
:generator
#object[jepsen.generator$reify__1936 0x1aac0a47 "[email protected]"],
:os
#object[jepsen.os$reify__1176 0x438aaa9f "[email protected]"],
:time-limit 60,
:model {}}
INFO [2018-02-02 13:04:35,389] jepsen nemesis - jepsen.core Starting nemesis
INFO [2018-02-02 13:04:35,389] jepsen worker 1 - jepsen.core Starting worker 1
INFO [2018-02-02 13:04:35,389] jepsen worker 2 - jepsen.core Starting worker 2
INFO [2018-02-02 13:04:35,389] jepsen worker 0 - jepsen.core Starting worker 0
INFO [2018-02-02 13:04:35,390] jepsen worker 3 - jepsen.core Starting worker 3
INFO [2018-02-02 13:04:35,390] jepsen worker 4 - jepsen.core Starting worker 4
INFO [2018-02-02 13:04:35,391] jepsen nemesis - jepsen.core Running nemesis
INFO [2018-02-02 13:04:35,391] jepsen worker 1 - jepsen.core Running worker 1
INFO [2018-02-02 13:04:35,391] jepsen worker 2 - jepsen.core Running worker 2
INFO [2018-02-02 13:04:35,391] jepsen worker 0 - jepsen.core Running worker 0
INFO [2018-02-02 13:04:35,391] jepsen worker 3 - jepsen.core Running worker 3
INFO [2018-02-02 13:04:35,391] jepsen worker 4 - jepsen.core Running worker 4
INFO [2018-02-02 13:04:35,391] jepsen nemesis - jepsen.core Stopping nemesis
INFO [2018-02-02 13:04:35,391] jepsen worker 1 - jepsen.core Stopping worker 1
INFO [2018-02-02 13:04:35,391] jepsen worker 2 - jepsen.core Stopping worker 2
INFO [2018-02-02 13:04:35,391] jepsen worker 0 - jepsen.core Stopping worker 0
INFO [2018-02-02 13:04:35,391] jepsen worker 3 - jepsen.core Stopping worker 3
INFO [2018-02-02 13:04:35,391] jepsen worker 4 - jepsen.core Stopping worker 4
INFO [2018-02-02 13:04:35,397] jepsen test runner - jepsen.core Run complete, writing
INFO [2018-02-02 13:04:35,434] jepsen test runner - jepsen.core Analyzing
INFO [2018-02-02 13:04:35,435] jepsen test runner - jepsen.core Analysis complete
INFO [2018-02-02 13:04:35,438] jepsen results - jepsen.store Wrote /home/aphyr/jepsen/jepsen.etcdemo/store/noop/20180202T130430.000-0600/results.edn
INFO [2018-02-02 13:04:35,440] main - jepsen.core {:valid? true}
Everything looks good!(‘ー`)
如上面展示的代码块所示,我们发现Jepsen启动了一系列的workers(类似于进程)。每一个worker负责执行针对数据库的操作。此外,Jepsen还启动了一个nemesis,用于制造故障。由于它们尚未被分配任何任务,所以它们马上便关闭了。Jepsen将这个简易测试的结果输出写到了store目录下,并打印出一个简要分析。
noop-test默认使用名为n1n2 ... n5的节点。如果你的节点有不一样的名称,该测试会因无法连接这些节点而失败。但是这并没关系。你可以在命令行中来指定这些节点名称:
$ lein run test --node foo.mycluster --node 1.2.3.4
亦或者通过传入一个文件名来达到相同目的。文件中要包含节点列表,且每行一个。如果你正在使用AWS Marketplace集群,一个名为nodes的文件已经生成于机器的home目录下,随时可用。
$ lein run test --nodes-file ~/nodes
如果你当前依然在不断地遇到SSH错误,你应该检查下你的SSH是否代理正在运行并且已经加载了所有节点的密钥。ssh some-db-node应该可以不用密码就连接上数据库。你也可以在命令行上重写对应的用户名、密码和身份文件。详见lein run test --help
$ lein run test --help
#object[jepsen.cli$test_usage 0x7ddd84b5 [email protected]]
-h, --help Print out this message and exit
-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 Username for logins
--password PASS root Password for sudo access
--strict-host-key-checking Whether to check host keys
--ssh-private-key FILE Path to an SSH identity file
--concurrency NUMBER 1n How 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 1 How many times should we repeat a test?
--time-limit SECONDS 60 Excluding setup and teardown, how long should a test run for, in seconds?
在本指导教程中,我们将全程使用lein run test ...来重新运行我们的Jepsen测试。每当我们运行一次测试,Jepsen将在store/下创建一个新目录。你可以在store/latest中看到最新的一次运行结果。
$ ls store/latest/
history.txt jepsen.log results.edn test.fressian
history.txt展示了测试执行的操作。不过此处运行完的结果是空的,因为noop测试不会执行任何操作。jepsen.log文件拥有一份测试输出到控制台日志的拷贝。results.edn展示了对测试的简要分析,也就是每次运行结束我们所看到的输出结果。最后,test.fressian拥有测试的原始数据,包括完整的机器可读的历史和分析,如果有需要可以对其进行事后分析。
Jepsen还带有内置的Web浏览器,用于浏览这些结果。 让我们将其添加到我们的main函数中:
(defn -main
"Handles command line arguments. Can either run a test, or a web server for
browsing results."
[& args]
(cli/run! (merge (cli/single-test-cmd {:test-fn etcd-test})
(cli/serve-cmd))
args))
上述代码之所以可以发挥作用,是因为cli/run!将命令名称与命令处理器做了映射。我们将这些映射关系用merge来合并。
$ lein run serve
13:29:21.425 [main] INFO jepsen.web - Web server running.
13:29:21.428 [main] INFO jepsen.cli - Listening on http://0.0.0.0:8080/
我们可以在网络浏览器中打开http://localhost:8080来探究我们的测试结果。当然,serve命令带有其自己的选项和帮助信息:
$ lein run serve --help
Usage: lein run -- serve [OPTIONS ...]
-h, --help Print out this message and exit
-b, --host HOST 0.0.0.0 Hostname to bind to
-p, --port NUMBER 8080 Port number to bind to
打开一个新的终端窗口,并在其中一直运行Web服务器。 那样我们可以看到测试结果,而无需反复启动和关闭服务器。
有了这个基础之后,我们将编写代码来设置和拆除数据库
Copy link