6.完善测试
我们的测试确定了一个故障,但需要一些运气和聪明的猜测才能发现它,现在是时候完善我们的测试了,使得它更快、更容易理解以及功能更加强大。
为了分析单个key的历史记录,Jepsen通过搜索并发操作的每种排列,以查找遵循cas寄存器操作规则的历史记录,这意味着在任何给定的时间点的并发操作数后,我们的搜索是指数级的。
Jepsen运行时需要指定一个工作线程数,这通常情况下也限制并发操作的数量。但是,当操作崩溃(或者是返回一个:info的结果,再或者是抛出一个异常),我们放弃该操作并且让当前线程去做新的事情。这可能会出现如下情况:崩溃的进程操作仍然在运行,并且可能会在后面的时间里被数据库执行。这意味着对于后面整个历史记录的剩余时间内,崩溃的操作跟其他操作是并发的。
崩溃的操作越多,历史记录结束时的并发操作就越多。并发数线性的增加伴随着验证时间的指数增加。我们的首要任务是减少崩溃的操作数量,下面我们将从读取开始。

崩溃读操作

当一个操作超时时,我们会得到类似下面的这样一长串的堆栈信息。
1
WARN [2018-02-02 16:14:37,588] jepsen worker 1 - jepsen.core Process 11 crashed
2
java.net.SocketTimeoutException: Read timed out
3
at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_40]
4
...
Copied!
同时进程的操作转成了一个:info的消息,因为我们不能确定该操作是成功了还是失败了。 但是,幂等操作,像读操作,并不会改变系统的状态。读操作是否成功不影响,因为效果是相同的。因此我们可以安全的将崩溃的读操作转为读操作失败,并提升checker的性能。
1
(invoke! [_ test op]
2
(case (:f op)
3
:read (try (let [value (-> conn
4
(v/get "foo" {:quorum? true})
5
parse-long)]
6
(assoc op :type :ok, :value value))
7
(catch java.net.SocketTimeoutException ex
8
(assoc op :type :fail, :error :timeout)))
9
:write (do (v/reset! conn "foo" (:value op))
10
(assoc op :type :ok))
11
:cas (try+
12
(let [[old new] (:value op)]
13
(assoc op :type (if (v/cas! conn "foo" old new)
14
:ok
15
:fail)))
16
(catch [:errorCode 100] ex
17
(assoc op :type :fail, :error :not-found)))))
Copied!
更好的是,如果我们一旦能立即捕获三个路径中的网络超时异常,我们就可以避免所有的异常堆栈信息出现在日志中。我们也将处理key不存在错误(not-found errors),尽管它只出现在:cas操作中,处理该错误后,将能保持代码更加的清爽。
1
(invoke! [_ test op]
2
(try+
3
(case (:f op)
4
:read (let [value (-> conn
5
(v/get "foo" {:quorum? true})
6
parse-long)]
7
(assoc op :type :ok, :value value))
8
:write (do (v/reset! conn "foo" (:value op))
9
(assoc op :type :ok))
10
:cas (let [[old new] (:value op)]
11
(assoc op :type (if (v/cas! conn "foo" old new)
12
:ok
13
:fail))))
14
15
(catch java.net.SocketTimeoutException e
16
(assoc op
17
:type (if (= :read (:f op)) :fail :info)
18
:error :timeout))
19
20
(catch [:errorCode 100] e
21
(assoc op :type :fail, :error :not-found))))
Copied!
现在所有的操作,我们会得到很短的超时错误信息,不仅仅读操作。
1
INFO [2017-03-31 19:34:47,351] jepsen worker 4 - jepsen.util 4 :info :cas [4 4] :timeout
Copied!

独立的数个键

我们已经有了针对单个线性键的测试。但是,这些进程迟早将会crash,并且并发数将会上升,拖慢分析速度。我们需要一种方法来限制单个键的历史操作记录长度,同时又能执行足够多的操作来观察到并发错误。
由于独立的键的线性操作是彼此线性独立的,因此我们可以将对单个键的测试升级为对多个键的测试,jepsen.independent命名空间提供这样的支持。
1
(ns jepsen.etcdemo
2
(:require [clojure.tools.logging :refer :all]
3
[clojure.string :as str]
4
[jepsen [checker :as checker]
5
[cli :as cli]
6
[client :as client]
7
[control :as c]
8
[db :as db]
9
[generator :as gen]
10
[independent :as independent]
11
[nemesis :as nemesis]
12
[tests :as tests]]
13
[jepsen.checker.timeline :as timeline]
14
[jepsen.control.util :as cu]
15
[jepsen.os.debian :as debian]
16
[knossos.model :as model]
17
[slingshot.slingshot :refer [try+]]
18
[verschlimmbesserung.core :as v]))
Copied!
我们已经有了一个对单个键生成操作的生成器,例如:{:type :invoke, :f :write, :value 3}。我们想升级这个操作为写多个key。我们想操作value [key v]而不是:value v
1
:generator (->> (independent/concurrent-generator
2
10
3
(range)
4
(fn [k]
5
(->> (gen/mix [r w cas])
6
(gen/stagger 1/50)
7
(gen/limit 100))))
8
(gen/nemesis
9
(cycle [(gen/sleep 5)
10
{:type :info, :f :start}
11
(gen/sleep 5)
12
{:type :info, :f :stop}]))
13
(gen/time-limit (:time-limit opts)))}))
Copied!
我们的read、write和cas操作的组合仍然不变,但是它被包裹在一个函数内,这个函数有一个参数k并且返回一个指定键的值生成器。我们使用concurrent-generator,使得每个键有10个线程,多个键来自无限的整数序列(range),同时这些键的生成器生成自(fn [k] ...)concurrent-generator改变了我们的values的结构,从v变成了[k v],因此我们需要更新我们的客户端,以便知道如何读写不同的键。
1
(invoke! [_ test op]
2
(let [[k v] (:value op)]
3
(try+
4
(case (:f op)
5
:read (let [value (-> conn
6
(v/get k {:quorum? true})
7
parse-long)]
8
(assoc op :type :ok, :value (independent/tuple k value)))
9
10
:write (do (v/reset! conn k v)
11
(assoc op :type :ok))
12
13
:cas (let [[old new] v]
14
(assoc op :type (if (v/cas! conn k old new)
15
:ok
16
:fail))))
17
18
(catch java.net.SocketTimeoutException e
19
(assoc op
20
:type (if (= :read (:f op)) :fail :info)
21
:error :timeout))
22
23
(catch [:errorCode 100] e
24
(assoc op :type :fail, :error :not-found)))))
Copied!
看看我们的硬编码的键"foo"是如何消失的?现在每个键都被操作自身参数化了。注意我们修改数值的地方--例如:在:f :read中——我们必须构建一个指定independent/tuple的键值对。为元组使用特殊数据类型,才能允许jepsen.independent在后面将不同键的历史记录分隔开来。
最后,我们的检查器以单个值的角度来进行验证——但是我们可以把它转变成一个可以合理处理好多个独立值的检查器,即依靠多个键来辨识这些独立值。
1
:checker (checker/compose
2
{:perf (checker/perf)
3
:indep (independent/checker
4
(checker/compose
5
{:linear (checker/linearizable {:model (model/cas-register)
6
:algorithm :linear})
7
:timeline (timeline/html)}))})
Copied!
写一个检查器,不费力地获得一个由n个checker构成的家族,哈哈哈哈!
1
$ lein run test --time-limit 30
2
...
3
ERROR [2017-03-31 19:51:28,300] main - jepsen.cli Oh jeez, I'm sorry, Jepsen broke. Here's why:
4
java.util.concurrent.ExecutionException: java.lang.AssertionError: Assert failed: This jepsen.independent/concurrent-generator has 5 threads to work with, but can only use 0 of those threads to run 0 concurrent keys with 10 threads apiece. Consider raising or lowering the test's :concurrency to a multiple of 10.
Copied!
阿哈,我们默认的并发是5个线程,但我们为了运行单个键,我们就要求了至少10个线程,运行10个键的话,需要100个线程。
1
$ lein run test --time-limit 30 --concurrency 100
2
...
3
142 :invoke :read [134 nil]
4
67 :invoke :read [133 nil]
5
66 :ok :read [133 1]
6
101 :ok :read [137 3]
7
181 :ok :write [135 3]
8
116 :ok :read [131 3]
9
111 :fail :cas [131 [0 0]]
10
151 :invoke :read [138 nil]
11
129 :ok :write [130 2]
12
159 :ok :read [138 1]
13
64 :ok :write [133 0]
14
69 :ok :cas [133 [0 0]]
15
109 :ok :cas [137 [4 3]]
16
89 :ok :read [135 1]
17
139 :ok :read [139 4]
18
19 :fail :cas [131 [2 1]]
19
124 :fail :cas [130 [4 4]]
Copied!
看上述结果,在有限的时间窗口内我们可以执行更多的操作。这帮助我们能能快的发现bugs。
到目前为止,我们硬编码的地方很多,下面在命令行中,我们将让其中一些选项变得可配置