6.完善测试
我们的测试确定了一个故障,但需要一些运气和聪明的猜测才能发现它,现在是时候完善我们的测试了,使得它更快、更容易理解以及功能更加强大。
为了分析单个key的历史记录,Jepsen通过搜索并发操作的每种排列,以查找遵循cas寄存器操作规则的历史记录,这意味着在任何给定的时间点的并发操作数后,我们的搜索是指数级的。
Jepsen运行时需要指定一个工作线程数,这通常情况下也限制并发操作的数量。但是,当操作崩溃(或者是返回一个:info的结果,再或者是抛出一个异常),我们放弃该操作并且让当前线程去做新的事情。这可能会出现如下情况:崩溃的进程操作仍然在运行,并且可能会在后面的时间里被数据库执行。这意味着对于后面整个历史记录的剩余时间内,崩溃的操作跟其他操作是并发的。
崩溃的操作越多,历史记录结束时的并发操作就越多。并发数线性的增加伴随着验证时间的指数增加。我们的首要任务是减少崩溃的操作数量,下面我们将从读取开始。
崩溃读操作
当一个操作超时时,我们会得到类似下面的这样一长串的堆栈信息。
WARN [2018-02-02 16:14:37,588] jepsen worker 1 - jepsen.core Process 11 crashed
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_40]
...同时进程的操作转成了一个:info的消息,因为我们不能确定该操作是成功了还是失败了。 但是,幂等操作,像读操作,并不会改变系统的状态。读操作是否成功不影响,因为效果是相同的。因此我们可以安全的将崩溃的读操作转为读操作失败,并提升checker的性能。
(invoke! [_ test op]
(case (:f op)
:read (try (let [value (-> conn
(v/get "foo" {:quorum? true})
parse-long)]
(assoc op :type :ok, :value value))
(catch java.net.SocketTimeoutException ex
(assoc op :type :fail, :error :timeout)))
:write (do (v/reset! conn "foo" (:value op))
(assoc op :type :ok))
:cas (try+
(let [[old new] (:value op)]
(assoc op :type (if (v/cas! conn "foo" old new)
:ok
:fail)))
(catch [:errorCode 100] ex
(assoc op :type :fail, :error :not-found)))))更好的是,如果我们一旦能立即捕获三个路径中的网络超时异常,我们就可以避免所有的异常堆栈信息出现在日志中。我们也将处理key不存在错误(not-found errors),尽管它只出现在:cas操作中,处理该错误后,将能保持代码更加的清爽。
现在所有的操作,我们会得到很短的超时错误信息,不仅仅读操作。
独立的数个键
我们已经有了针对单个线性键的测试。但是,这些进程迟早将会crash,并且并发数将会上升,拖慢分析速度。我们需要一种方法来限制单个键的历史操作记录长度,同时又能执行足够多的操作来观察到并发错误。
由于独立的键的线性操作是彼此线性独立的,因此我们可以将对单个键的测试升级为对多个键的测试,jepsen.independent命名空间提供这样的支持。
我们已经有了一个对单个键生成操作的生成器,例如:{:type :invoke, :f :write, :value 3}。我们想升级这个操作为写多个key。我们想操作value [key v]而不是:value v。
我们的read、write和cas操作的组合仍然不变,但是它被包裹在一个函数内,这个函数有一个参数k并且返回一个指定键的值生成器。我们使用concurrent-generator,使得每个键有10个线程,多个键来自无限的整数序列(range),同时这些键的生成器生成自(fn [k] ...)。 concurrent-generator改变了我们的values的结构,从v变成了[k v],因此我们需要更新我们的客户端,以便知道如何读写不同的键。
看看我们的硬编码的键"foo"是如何消失的?现在每个键都被操作自身参数化了。注意我们修改数值的地方--例如:在:f :read中——我们必须构建一个指定independent/tuple的键值对。为元组使用特殊数据类型,才能允许jepsen.independent在后面将不同键的历史记录分隔开来。
最后,我们的检查器以单个值的角度来进行验证——但是我们可以把它转变成一个可以合理处理好多个独立值的检查器,即依靠多个键来辨识这些独立值。
写一个检查器,不费力地获得一个由n个checker构成的家族,哈哈哈哈!
阿哈,我们默认的并发是5个线程,但我们为了运行单个键,我们就要求了至少10个线程,运行10个键的话,需要100个线程。
看上述结果,在有限的时间窗口内我们可以执行更多的操作。这帮助我们能能快的发现bugs。
到目前为止,我们硬编码的地方很多,下面在命令行中,我们将让其中一些选项变得可配置。
Last updated
Was this helpful?