jepsen-io/jepsen
GitHub: jepsen-io/jepsen
分布式系统正确性验证框架,通过故障注入与形式化分析检验系统在异常条件下是否真正满足其宣称的一致性保证。
Stars: 7379 | Forks: 749
# Jepsen
为你破坏分布式系统,让你无需亲自动手。
Jepsen 是一个 Clojure 库。测试是一个使用 Jepsen 库的 Clojure 程序,用于设置分布式系统,对该系统运行一系列操作,并验证这些操作的历史记录是否合理。Jepsen 已被用于验证各种系统,从最终一致性可交换数据库到线性化协调系统,再到分布式任务调度器。它还可以生成性能和可用性图表,帮助你描述系统对不同故障的响应方式。请访问 [jepsen.io](https://jepsen.io/analyses) 查看你可以使用 Jepsen 进行的各种分析示例。
[](https://clojars.org/jepsen)
[](https://travis-ci.com/jepsen-io/jepsen)
## 设计概述
Jepsen 测试作为一个 Clojure 程序在*控制节点*上运行。该程序使用 SSH 登录到一组 *db nodes*,并在其中使用测试可插拔的 *os* 和 *db* 设置你要测试的分布式系统。
系统运行后,控制节点会启动一组逻辑上单线程的*进程*,每个进程都有自己的分布式系统*客户端*。*生成器*为每个进程生成要执行的新操作。然后,进程使用其客户端将这些操作应用到系统中。每个操作的开始和结束都记录在*历史记录*中。在执行操作时,一个特殊的 *nemesis* 进程会向系统引入故障——这同样由生成器进行调度。
最后,DB 和 OS 会被拆除。Jepsen 使用*检查器*来分析测试历史记录的正确性,并生成报告、图表等。测试、历史记录、分析结果以及任何补充结果都会被写入文件系统中的 `store///` 目录下,以供日后审查。为了方便,每个层级都会维护指向最新结果的符号链接。
## 文档
这篇[教程](doc/tutorial/index.md)将带你从头开始编写 Jepsen 测试。这里还提供了一个独立翻译的[中文版本](https://jaydenwen123.gitbook.io/zh_jepsen_doc/)。
作为参考,请查阅 [API 文档](http://jepsen-io.github.io/jepsen/)。
[这里有什么](doc/whats-here.md) 提供了 Jepsen 命名空间及其协同工作方式的概述。
## 搭建 Jepsen 环境
那么,你已经编写好了一个 Jepsen 测试,并且想要运行它!或者你可能想开始学习如何编写测试。你有以下几种选择:
### AWS
如果你拥有 AWS 账号,可以从 [AWS Marketplace](https://aws.amazon.com/marketplace/pp/Jepsen-LLC-Jepsen/B01LZ7Y7U0) 启动一个完整的 Jepsen 集群——包括控制节点和 DB 节点。
点击 "Continue to Subscribe"、"Continue to Configuration",然后选择 "CloudFormation Template"。你可以选择要部署的节点数量,调整实例类型和磁盘大小等等。这些都是完整的虚拟机,这意味着它们可以测试时钟偏移。
AWS 市场集群需要支付每小时费用(通常为 $1/小时/节点),这有助于资助 Jepsen 的开发。
### LXC
你可以将 DB 节点设置为 LXC 容器,并使用本地机器作为控制节点。有关指导,请参阅 [LXC 文档](doc/lxc.md))。对于测试开发来说,这可能是最简单的设置:你将能够在本地节点上编辑源代码、运行分析器等。容器没有真实的时钟,因此你通常无法使用它们来测试时钟偏移。
### 虚拟机、真实硬件等
你应该能够在几乎所有满足以下条件的机器上运行 Jepsen:
- TCP 网络
- SSH 服务器
- Sudo 或 root 访问权限
每个 DB 节点都应该可以通过 SSH 从控制节点访问:你需要能够运行 `ssh myuser@some-node` 并获得一个 shell。默认情况下,DB 节点的名称为 n1、n2、n3、n4 和 n5,但这一切(包括 SSH 用户名、密码、身份文件等)都可以在你的测试中或 CLI 中进行定义。你在这些机器上使用的账户需要具有 sudo 访问权限,以便设置 DB、控制防火墙等。
请注意:测试可能会干扰时钟、添加 apt 仓库、对进程运行 `killall -9`,甚至通常会破坏系统,因此,除非你喜欢冒险,或者测试是你自己编写的并且你确切知道它在做什么,否则你不应该将 Jepsen 指向你的生产环境机器。
注意:大多数 Jepsen 测试在编写时都考虑了更具体的要求——比如在 Debian 上运行、使用 `iptables` 进行网络操作等。有关更多详细信息,请参阅具体的测试代码。
### Docker(不受支持)
虽然 Jepsen 官方不支持 Docker,但社区维护了一个 [Jepsen in Docker, Community Edition](https://github.com/nurturenature/jepsen-docker)。
### 设置控制节点
对于 AWS,你的控制节点已经预配置了运行大多数 Jepsen 测试所需的所有软件。如果你构建自己的控制节点(或者如果你使用本地机器作为控制节点),你需要以下几点:
- 一个 [JVM](https://openjdk.java.net/install/)——版本 21 或更高。
- JNA,以便 JVM 可以与你的 SSH 通信。
- [Leiningen](https://leiningen.org/):一个 Clojure 构建工具。
- [Gnuplot](http://www.gnuplot.info/):Jepsen 用于渲染性能图的方式。
- [Graphviz](https://graphviz.org/):Jepsen 用于渲染事务异常的方式。
在 Debian 上,请尝试:
```
sudo apt install default-jdk libjna-java gnuplot graphviz
```
……以安装基本要求。Debian 的 Leiningen 包非常陈旧,因此请改为[从网络下载 lein](https://leiningen.org/)。
## 运行测试
一切设置完成后,你应该能够运行 `cd aerospike; lein test`,它会输出类似以下内容:
```
INFO jepsen.core - Analysis invalid! (ノಥ益ಥ)ノ ┻━┻
{:valid? false,
:counter
{:valid? false,
:reads
[[190 193 194]
[199 200 201]
[253 255 256]
...}}
```
## 使用 REPL 工作
Jepsen 测试会在 `store/` 目录中生成 `.jepsen` 文件。你可以使用这些文件在 REPL 中调查测试。在测试目录(应包含 `store...`)中运行 `lein repl`,然后使用 `store/test` 加载测试:
```
user=> (def t (store/test -1))
```
-1 是最后一次测试运行,-2 是倒数第二次。0 是第一次,1 是第二次,以此类推。你也可以通过字符串目录名称进行加载。作为一个方便的快捷方式,在 Web 界面中点击测试标题会将其路径复制到剪贴板。
```
user=> (def t (store/test "/home/aphyr/jepsen.etcd/store/etcd append etcdctl kill/20221003T124714.485-0400"))
```
它们具有与你所习惯在 Jepsen 中使用的测试 map 相同的结构,只是没有一些不适合序列化的字段——没有 `:checker`、`:client` 等。
```
jepsen.etcd=> (:name t)
"etcd append etcdctl kill"
jepsen.etcd=> (:ops-per-key t)
200
```
这些测试 map 也是惰性的:为了加快 REPL 中的工作速度,在你请求它们之前,它们不会加载历史记录或结果。然后它们会从磁盘加载并被缓存。
```
jepsen.etcd=> (count (:history t))
52634
```
你可以使用所有常用的 Clojure 技巧来内省结果和历史记录。这是一个中止读(G1a)异常——我们将提取执行了写入和读取该中止读的操作:
```
jepsen.etcd=> (def writer (-> t :results :workload :anomalies :G1a first :writer))
#'jepsen.etcd/writer
jepsen.etcd=> (def reader (-> t :results :workload :anomalies :G1a first :op))
#'jepsen.etcd/reader
```
写入者将 11 和 12 追加到键 559,但失败了,返回了一个重复键错误:
```
jepsen.etcd=> (:value writer)
[[:r 559 nil] [:r 558 nil] [:append 559 11] [:append 559 12]]
jepsen.etcd=> (:error writer)
[:duplicate-key "rpc error: code = InvalidArgument desc = etcdserver: duplicate key given in txn request"]
```
然而,读取者观察到了一个以 12 开头的键 559 的值!
```
jepsen.etcd=> (:value reader)
[[:r 559 [12]] [:r 557 [1]]]
```
让我们找出所有成功的事务:
```
jepsen.etcd=> (def txns (->> t :history (filter #(and (= :txn (:f %)) (= :ok (:type %)))) (map :value)))
#'jepsen.etcd/txns
```
并将这些限制为仅影响键 559 的操作:
```
jepsen.etcd=> (->> txns (filter (partial some (comp #{559} second))) pprint)
([[:r 559 [12]] [:r 557 [1]]]
[[:r 559 [12]] [:append 559 1] [:r 559 [12 1]]]
[[:append 556 32]
[:r 556 [1 18 29 32]]
[:r 556 [1 18 29 32]]
[:r 559 [12 1]]]
[[:r 559 [12 1]]]
[[:append 559 9] [:r 557 [1 5]] [:r 558 [1]] [:r 558 [1]]]
[[:r 559 [12 1 9]] [:r 559 [12 1 9]]]
[[:append 559 17]]
[[:r 559 [12 1 9 17]] [:append 558 5]]
[[:r 559 [12 1 9 17]]
[:append 557 22]
[:append 559 27]
[:r 557 [1 5 12 22]]])
```
果然,没有成功将 12 追加到键 559 的操作!
你会在 `jepsen.store` 中找到更多用于切片和剖析测试的函数。
## 常见问题
### JSCH 认证错误
如果你看到 `com.jcraft.jsch.JSchException: Auth fail`,这意味着你的测试的 `:ssh` map 有问题,或者你的控制节点的 SSH 环境有点奇怪。
0. 确认你可以通过 SSH 登录到 Jepsen 未能连接的节点。尝试使用 `ssh -v` 获取详细信息——特别注意它使用的是密码还是私钥。
1. 如果你打算使用用户名和密码,请确认它们在你的测试的 `:ssh` map 中指定正确。
2. 如果你打算使用私钥登录,请确保你的 SSH agent 正在运行。
- `ssh-add -l` 应该会显示你用于登录的密钥。
- 如果你的 agent 未运行,请尝试使用 `ssh-agent` 启动一个。
- 如果你的 agent 没有显示任何密钥,你可能需要使用 `ssh-add` 添加它。
- 如果你正在通过 SSH 连接到控制节点,SSH 可能是在转发本地 agent 的密钥,而不是使用控制节点上的密钥。尝试使用 `ssh -a` 禁用 agent 转发。
如果你之前已经 SSH 到过 DB 节点,你可能还会遇到一个 JSCH bug,即它不知道如何读取哈希过的 `known_hosts` 文件。从你的 `known_hosts` 文件中删除 DB 主机的所有密钥,然后:
```
ssh-keyscan -t rsa n1 >> ~/.ssh/known_hosts
ssh-keyscan -t rsa n2 >> ~/.ssh/known_hosts
ssh-keyscan -t rsa n3 >> ~/.ssh/known_hosts
ssh-keyscan -t rsa n4 >> ~/.ssh/known_hosts
ssh-keyscan -t rsa n5 >> ~/.ssh/known_hosts
```
以将每个节点主机密钥的未哈希版本添加到你的 `~/.ssh/known_hosts` 中。
### SSHJ 认证错误
如果你遇到类似 `net.schmizz.sshj.transport.TransportException: Could not verify 'ssh-ed25519' host key with fingerprint 'bf:4a:...' for 'n1' on port 22` 的异常,但你确定密钥已经在你的 `~/.ssh/known-hosts` 中,这是因为(我认为)SSHJ 尝试只验证 ed25519 密钥并*忽略*了 RSA 密钥。你可以通过以下命令显式添加 ed25519 密钥:
```
ssh-keyscan -t ed25519 n1 >> ~/.ssh/known_hosts
...
```
## 其他项目
其他可能感兴趣的项目:
- [Jecci](https://github.com/michaelzenz/jecci):一个围绕 Jepsen 的封装框架
- [Porcupine](https://github.com/anishathalye/porcupine):一个用 Go 编写的线性一致性检查器。
- [elle-cli](https://github.com/ligurio/elle-cli):针对黑盒数据库的事务一致性检查器的命令行前端。
- [Tickloom](https://github.com/unmeshjoshi/tickloom):一个用于构建分布式系统的确定性模拟框架,集成了 Jepsen 以进行一致性检查。
标签:Clojure, Jepsen, 一致性验证, 内存分配, 分布式系统, 可用性测试, 响应大小分析, 安全测试, 性能分析, 攻击性安全, 故障注入, 数据库测试, 测试工具, 测试框架, 混沌工程, 系统验证, 网络分区