# 5.分区

## 故障引入

nemesis是一个不绑定到任何特定节点的特殊客户端，用于引入整个集群内运行过程中可能遇到的故障。我们需要导入`jepsen.nemesis`来提供数个内置的故障模式。

```
(ns jepsen.etcdemo
  (:require [clojure.tools.logging :refer :all]
            [clojure.string :as str]
            [jepsen [checker :as checker]
                    [cli :as cli]
                    [client :as client]
                    [control :as c]
                    [db :as db]
                    [generator :as gen]
                    [nemesis :as nemesis]
                    [tests :as tests]]
            [jepsen.checker.timeline :as timeline]
            [jepsen.control.util :as cu]
            [jepsen.os.debian :as debian]
            [knossos.model :as model]
            [slingshot.slingshot :refer [try+]]
            [verschlimmbesserung.core :as v]))
```

我们将选取一个简单的nemesis进行介绍，并在测试中添加名为`:nemesis`的主键。当它收到`:start`操作指令时，它会将网络分成两部分并随机选择其中一个。当收到`:stop`指令时则恢复网络分区。

```
(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
         opts
         {:pure-generators true
          :name            "etcd"
          :os              debian/os
          :db              (db "v3.1.5")
          :client          (Client. nil)
          :nemesis         (nemesis/partition-random-halves)
          :checker         (checker/compose
                             {:perf   (checker/perf)
                              :linear (checker/linearizable
                                        {:model     (model/cas-register)
                                         :algorithm :linear})
                              :timeline (timeline/html)})
          :generator       (->> (gen/mix [r w cas])
                                (gen/stagger 1)
                                (gen/nemesis nil)
                                (gen/time-limit 15))}))
```

像常规的客户端一样，nemesis从生成器中获取操作。现在我们的生成器会将操作分发给常规的客户端——而nemesis只会收到`nil`，即什么都不用做。我们将专门用于nemesis操作的生成器来替换它。我们也准备增加时间限制，那样就有足够的时间等着nemesis发挥作用了。

```
          :generator (->> (gen/mix [r w cas])
                          (gen/stagger 1)
                          (gen/nemesis
                            (cycle [(gen/sleep 5)
                              {:type :info, :f :start}
                              (gen/sleep 5)
                              {:type :info, :f :stop}]))
                          (gen/time-limit 30))
```

Clojure sequence数据结构可以扮演生成器的角色，因此我们可以使用Clojure自带的函数来构建它们。这里，我们使用`cycle`来构建一个无限的睡眠、启动、睡眠、停止循环，直至超时。

网络分区造成一些操作出现崩溃：

```
WARN [2018-02-02 15:54:53,380] jepsen worker 1 - jepsen.core Process 1 crashed
java.net.SocketTimeoutException: Read timed out
```

如果我们*知道*一个操作没有触发，我们可以通过返回带有`:type :fail`代替`client/invoke!`抛出异常让checker更有效率（也能发现更多的bugs！），但每个错误引发程序崩溃依旧是安全的：jepsen的checkers知道一个已经崩溃的操作可能触发也可能没触发。

## 发现bug

我们已经在测试中写死了超时时间为30s，但是如果能够在命令行中控制它就好了。Jepsen的cli工具箱提供了一个`--time-limit`开关，在参数列表中，它作为`:time-limit`传给`etcd-test`。现在我们把它的使用方法展示出来。

```
          :generator (->> (gen/mix [r w cas])
                          (gen/stagger 1)
                          (gen/nemesis
                            (gen/seq (cycle [(gen/sleep 5)
                                             {:type :info, :f :start}
                                             (gen/sleep 5)
                                             {:type :info, :f :stop}])))
                          (gen/time-limit (:time-limit opts)))}
```

```bash
$ lein run test --time-limit 60
...
```

现在我们的测试时间可长可短，让我们加速请求访问速率。如果两次请求时间间隔太长，那么我们就看不到一些有趣的行为。我们将两次请求的时间间隔设置为1/10s。

```
          :generator (->> (gen/mix [r w cas])
                          (gen/stagger 1/50)
                          (gen/nemesis
                           (cycle [(gen/sleep 5)
                            {:type :info, :f :start}
                            (gen/sleep 5)
                            {:type :info, :f :stop}]))
                          (gen/time-limit (:time-limit opts)))
```

如果你多次运行这个测试，你会注意到一个有趣的结果。有些时候它会失败！

```bash
$ lein run test --test-count 10
...
     :model {:msg "can't read 3 from register 4"}}]
...
Analysis invalid! (ﾉಥ益ಥ）ﾉ ┻━┻
```

Knossos参数有误：它认为寄存器需要的合法参数个数是4，但是程序成功读取到的是3。当出现线性验证失败时，Knossos将绘制一个SVG图展示错误——我们可以读取历史记录来查看更详细的操作信息。

```bash
$ open store/latest/linear.svg
$ open store/latest/history.txt
```

这是读取操作常见的脏读问题：尽管最近得一些写操作已完成了，我们依然获取了一个`过去`值。这种情况出现是因为etcd允许我们读取任何副本的局部状态，而不需要经过共识特性来确保我们拥有最新的状态。

## 线性一致读

etcd文档宣称"默认情况下etcd确保所有的操作都是线性一致性"，但是显然事实并非如此，在[第二版api文档](https://coreos.com/etcd/docs/latest/v2/api.html)隐藏着这么一条不引人注意的注释：

> 如果你想让一次读取是完全的线性一致，可以使用quorum=true。读取和写入的操作路径会因而变得非常相似，并且具有相近的速度（译者注：暗指速率变慢）。如果不确定是否需要此功能，请随时向etcd开发者发送电子邮件以获取建议。

啊哈！所以我们需要使用*quorum*读取，Verschlimmbesserung中有这样的案例：

```
    (invoke! [this test op]
      (case (:f op)
        :read (let [value (-> conn
                              (v/get "foo" {:quorum? true})
                              parse-long)]
                (assoc op :type :ok, :value value))
      ...
```

引入quorum读取后测试通过。

```
$ lein run test
...
Everything looks good! ヽ(‘ー`)ノ
```

恭喜！你已经成功写完了第一个Jepsen测试，我在[2014](https://aphyr.com/posts/316-jepsen-etcd-and-consul)年提出了这个issue，并且联系了etcd开发团队请他们介绍*quorum*读机制。

休息一下吧，这是你应得的！如果你喜欢的话，可以继续[完善Jepsen测试](https://github.com/jaydenwen123/jepsen/blob/main/doc/cn_tutorial/06-cn-refining.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jaydenwen123.gitbook.io/zh_jepsen_doc/05-cn-nemesis.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
