Shopify/toxiproxy
GitHub: Shopify/toxiproxy
一款用于在开发和测试环境中模拟网络故障和系统延迟的 TCP 代理框架,旨在帮助验证分布式系统的容错与弹性能力。
Stars: 12005 | Forks: 499
# Toxiproxy
[](https://github.com/Shopify/toxiproxy/releases/latest)
[](https://github.com/Shopify/toxiproxy/actions/workflows/test.yml)

Toxiproxy 是一个用于模拟网络状况的框架。它专为在测试、CI 和开发环境中工作而设计,支持对连接进行确定性的篡改,同时也支持随机混沌和自定义操作。**Toxiproxy 是你需要的工具,用于通过测试证明你的应用程序没有单点故障。** 自 2014 年 10 月以来,我们一直在 Shopify 的所有开发和测试环境中成功使用它。有关更多信息,请参阅我们关于弹性的[博客文章][blog]。
Toxiproxy 的使用由两部分组成。一个是用 Go 编写的 TCP 代理(也就是本仓库包含的内容),另一个是通过 HTTP 与代理通信的客户端。你将应用程序配置为使所有测试连接都通过 Toxiproxy,然后就可以通过 HTTP 操纵它们的健康状态。有关如何设置项目的信息,请参见下方的[用法](#usage)。
例如,使用 [Ruby 客户端](https://github.com/Shopify/toxiproxy-ruby)为 MySQL 的响应增加 1000 毫秒的延迟:
```
Toxiproxy[:mysql_master].downstream(:latency, latency: 1000).apply do
Shop.first # this takes at least 1s
end
```
关闭所有 Redis 实例:
```
Toxiproxy[/redis/].down do
Shop.first # this will throw an exception
end
```
虽然本 README 中的示例目前是 Ruby 的,但这并不妨碍你使用任何其他语言创建客户端(参见[客户端](#clients))。
## 目录
- [Toxiproxy](#toxiproxy)
- [目录](#table-of-contents)
- [为什么还要另一个混沌 TCP 代理?](#why-yet-another-chaotic-tcp-proxy)
- [客户端](#clients)
- [示例](#example)
- [用法](#usage)
- [1. 安装 Toxiproxy](#1-installing-toxiproxy)
- [从 Toxiproxy 1.x 升级](#upgrading-from-toxiproxy-1x)
- [2. 填充 Toxiproxy](#2-populating-toxiproxy)
- [3. 使用 Toxiproxy](#3-using-toxiproxy)
- [4. 日志](#4-logging)
- [Toxics](#toxics)
- [latency](#latency)
- [down](#down)
- [bandwidth](#bandwidth)
- [slow_close](#slow_close)
- [timeout](#timeout)
- [reset_peer](#reset_peer)
- [slicer](#slicer)
- [limit_data](#limit_data)
- [HTTP API](#http-api)
- [Proxy 字段:](#proxy-fields)
- [Toxic 字段:](#toxic-fields)
- [端点](#endpoints)
- [填充 Proxies](#populating-proxies)
- [CLI 示例](#cli-example)
- [指标](#metrics)
- [常见问题](#frequently-asked-questions)
- [开发](#development)
- [发布](#release)
## 为什么还要另一个混沌 TCP 代理?
我们发现现有的工具并没有提供我们在集成和单元测试中所需的那种动态 API。像 `nc` 等 Linux 工具不能跨平台,而且需要 root 权限,这使得它们在测试、开发和 CI 环境中存在问题。
## 客户端
* [toxiproxy-ruby](https://github.com/Shopify/toxiproxy-ruby)
* [toxiproxy-go](https://github.com/Shopify/toxiproxy/tree/main/client)
* [toxiproxy-python](https://github.com/douglas/toxiproxy-python)
* [toxiproxy.net](https://github.com/mdevilliers/Toxiproxy.Net)
* [toxiproxy-php-client](https://github.com/ihsw/toxiproxy-php-client)
* [toxiproxy-node-client](https://github.com/ihsw/toxiproxy-node-client)
* [toxiproxy-java](https://github.com/trekawek/toxiproxy-java)
* [toxiproxy-haskell](https://github.com/jpittis/toxiproxy-haskell)
* [toxiproxy-rust](https://github.com/itarato/toxiproxy_rust)
* [toxiproxy-elixir](https://github.com/Jcambass/toxiproxy_ex)
## 示例
让我们通过一个 Rails 应用程序来演练一个示例。请注意,Toxiproxy 绝不与 Ruby 绑定,这只是我们的第一个用例。你可以在 [sirupsen/toxiproxy-rails-example](https://github.com/sirupsen/toxiproxy-rails-example) 查看完整示例。
要立即开始,请跳至[用法](#usage)。
对于我们热门的博客,出于某种原因,我们将文章的标签存储在 Redis 中,而文章本身存储在 MySQL 中。我们可能有一个 `Post` 类,其中包含一些在 [Redis set](http://redis.io/commands#set) 中操作标签的方法:
```
class Post < ActiveRecord::Base
# Return an Array of all the tags.
def tags
TagRedis.smembers(tag_key)
end
# Add a tag to the post.
def add_tag(tag)
TagRedis.sadd(tag_key, tag)
end
# Remove a tag from the post.
def remove_tag(tag)
TagRedis.srem(tag_key, tag)
end
# Return the key in Redis for the set of tags for the post.
def tag_key
"post:tags:#{self.id}"
end
end
```
我们已经确定,在向标签数据存储写入数据(添加/删除)时出错是可以接受的。但是,如果标签数据存储宕机,我们应该能够看到没有标签的文章。我们可以简单地在 `tags` 方法中的 `SMEMBERS` Redis 调用周围救援 `Redis::CannotConnectError`。让我们使用 Toxiproxy 来测试这一点。
由于我们已经安装了 Toxiproxy 并且它正在我们的机器上运行,我们可以跳到第 2 步。在这一步中,我们需要确保 Toxiproxy 有一个用于 Redis 标签的映射。我们在 `config/boot.rb` 中(在任何连接建立之前)添加:
```
require 'toxiproxy'
Toxiproxy.populate([
{
name: "toxiproxy_test_redis_tags",
listen: "127.0.0.1:22222",
upstream: "127.0.0.1:6379"
}
])
```
然后在 `config/environments/test.rb` 中,我们通过添加以下行将 `TagRedis` 设置为通过 Toxiproxy 连接到 Redis 的 Redis 客户端:
```
TagRedis = Redis.new(port: 22222)
```
现在,测试环境中的所有调用都通过 Toxiproxy。这意味着我们可以添加一个单元测试来模拟故障:
```
test "should return empty array when tag redis is down when listing tags" do
@post.add_tag "mammals"
# Take down all Redises in Toxiproxy
Toxiproxy[/redis/].down do
assert_equal [], @post.tags
end
end
```
测试因 `Redis::CannotConnectError` 而失败。完美!Toxiproxy 在该闭包执行期间成功关闭了 Redis。让我们修复 `tags` 方法以增加弹性:
```
def tags
TagRedis.smembers(tag_key)
rescue Redis::CannotConnectError
[]
end
```
测试通过了!我们现在有了一个单元测试,证明在 Redis 宕机时获取标签会返回一个空数组,而不是抛出异常。为了实现完全覆盖,你还应该编写一个集成测试,用于测试在 Redis 宕机时获取整个博客文章页面的情况。
完整的示例应用程序位于 [sirupsen/toxiproxy-rails-example](https://github.com/sirupsen/toxiproxy-rails-example)。
## 用法
配置项目以使用 Toxiproxy 包含三个步骤:
1. 安装 Toxiproxy
2. 填充 Toxiproxy
3. 使用 Toxiproxy
### 1. 安装 Toxiproxy
**Linux**
请参阅 [`Releases`](https://github.com/Shopify/toxiproxy/releases) 获取适用于你的架构的最新二进制文件和系统软件包。
**Ubuntu**
```
$ wget -O toxiproxy-2.1.4.deb https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy_2.1.4_amd64.deb
$ sudo dpkg -i toxiproxy-2.1.4.deb
$ sudo service toxiproxy start
```
**OS X**
使用 [Homebrew](https://brew.sh/):
```
$ brew tap shopify/shopify
$ brew install toxiproxy
```
或者使用 [MacPorts](https://www.macports.org/):
```
$ port install toxiproxy
```
**Windows**
可在 https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-windows-amd64.exe 下载 Windows 版的 Toxiproxy。
**Docker**
Toxiproxy 可在 [Github container registry](https://github.com/Shopify/toxiproxy/pkgs/container/toxiproxy) 上找到。
`<= 2.1.4` 的旧版本可在 [Docker Hub](https://hub.docker.com/r/shopify/toxiproxy/) 上找到。
```
$ docker pull ghcr.io/shopify/toxiproxy
$ docker run --rm -it ghcr.io/shopify/toxiproxy
```
如果从宿主机而不是其他容器使用 Toxiproxy,请使用 `--net=host` 启用 host 网络。
```
$ docker run --rm --entrypoint="/toxiproxy-cli" -it ghcr.io/shopify/toxiproxy list
```
**源码**
如果你安装了 Go,可以使用 make 文件从源码构建 Toxiproxy:
```
$ make build
$ ./toxiproxy-server
```
#### 从 Toxiproxy 1.x 升级
在 Toxiproxy 2.0 中,API 进行了一些更改,使其与 1.x 版本不兼容。为了使用 2.x 版本的 Toxiproxy 服务器,你需要确保你的客户端库支持相同的版本。你可以通过查看 `/version` 端点来检查你正在运行的 Toxiproxy 版本。有关特定库的更改,请参阅你的客户端库的文档。Toxiproxy 服务器的详细更改可以在 [CHANGELOG.md](./CHANGELOG.md) 中找到。
### 2. 填充 Toxiproxy
当你的应用程序启动时,它需要确保 Toxiproxy 知道将哪些端点代理到哪里。主要参数是:名称、Toxiproxy **监听**的地址和上游地址。
一些客户端库为此任务提供了辅助工具,其本质上是确保列表中的每个代理都被创建。来自 Ruby 客户端的示例:
```
# 确保 `shopify_test_redis_master` 和 `shopify_test_mysql_master` 在
# Toxiproxy 中存在
Toxiproxy.populate([
{
name: "shopify_test_redis_master",
listen: "127.0.0.1:22220",
upstream: "127.0.0.1:6379"
},
{
name: "shopify_test_mysql_master",
listen: "127.0.0.1:24220",
upstream: "127.0.0.1:3306"
}
])
```
这段代码需要在启动时尽早运行,在任何代码建立通过 Toxiproxy 的连接之前。请查看你的客户端库以获取有关填充辅助工具的文档。
或者使用 CLI 创建代理,例如:
```
toxiproxy-cli create -l localhost:26379 -u localhost:6379 shopify_test_redis_master
```
我们建议使用如上所示的命名方式:`___`。这确保了使用相同 Toxiproxy 的应用程序之间不会发生冲突。
对于大型应用程序,我们建议将 Toxiproxy 配置存储在单独的配置文件中。我们使用 `config/toxiproxy.json`。可以使用 `-config` 选项将此文件传递给服务器,或者由应用程序加载以与 `populate` 函数一起使用。
一个 `config/toxiproxy.json` 示例:
```
[
{
"name": "web_dev_frontend_1",
"listen": "[::]:18080",
"upstream": "webapp.domain:8080",
"enabled": true
},
{
"name": "web_dev_mysql_1",
"listen": "[::]:13306",
"upstream": "database.domain:3306",
"enabled": true
}
]
```
请使用临时端口范围之外的端口,以避免随机端口冲突。在 Linux 上,默认为 `32,768` 到 `61,000`,请参阅 `/proc/sys/net/ipv4/ip_local_port_range`。
### 3. 使用 Toxiproxy
要使用 Toxiproxy,你现在需要将你的应用程序配置为通过 Toxiproxy 进行连接。继续我们在第二步中的示例,我们可以将我们的 Redis 客户端配置为通过 Toxiproxy 进行连接:
```
# 旧版直连 Redis
redis = Redis.new(port: 6380)
# 新版通过 Toxiproxy
redis = Redis.new(port: 22220)
```
现在你可以通过 Toxiproxy API 对其进行篡改。在 Ruby 中:
```
redis = Redis.new(port: 22220)
Toxiproxy[:shopify_test_redis_master].downstream(:latency, latency: 1000).apply do
redis.get("test") # will take 1s
end
```
或者通过 CLI:
```
toxiproxy-cli toxic add -t latency -a latency=1000 shopify_test_redis_master
```
有关用法,请查阅你相应的客户端库。
### 4. 日志
包含以下日志级别:panic、fatal、error、warn 或 warning、info、debug 和 trace。
可以通过环境变量 `LOG_LEVEL` 更新级别。
### Toxics
Toxics 会操纵客户端和上游之间的管道。可以使用 [HTTP api](#http-api) 在代理中添加和删除它们。每个 toxic 都有自己的参数,用于改变它影响代理链接的方式。
有关实现自定义 toxics 的文档,请参阅 [CREATING_TOXICS.md](./CREATING_TOXICS.md)
#### latency
为通过代理的所有数据添加延迟。延迟等于 `latency` +/- `jitter`。
属性:
- `latency`:时间,以毫秒为单位
- `jitter`:时间,以毫秒为单位
#### down
在 Toxiproxy 的实现中,关闭服务在技术上并不是一个 toxic。这是通过向 `/proxies/{proxy}` 发送 `POST` 请求并将 `enabled` 字段设置为 `false` 来完成的。
#### bandwidth
将连接限制为每秒最大千字节数。
属性:
- `rate`:速率,以 KB/s 为单位
#### slow_close
延迟 TCP 套接字的关闭,直到 `delay` 时间耗尽。
属性:
- `delay`:时间,以毫秒为单位
#### timeout
阻止所有数据通过,并在 `timeout` 后关闭连接。如果 `timeout` 为 0,连接将不会关闭,并且数据将被丢弃,直到移除该 toxic。
属性:
- `timeout`:时间,以毫秒为单位
#### reset_peer
通过立即或在 `timeout` 后关闭 stub Input,模拟连接上的 TCP RESET (Connection reset by peer)。
属性:
- `timeout`:时间,以毫秒为单位
#### slicer
将 TCP 数据切成小段,可选在每个切片“数据包”之间添加延迟。
属性:
- `average_size`:平均数据包的大小,以字节为单位
- `size_variation`:平均数据包大小的变动范围(以字节为单位,应小于 average_size)
- `delay`:每个数据包延迟的时间,以微秒为单位
#### limit_data
当传输的数据超过限制时关闭连接。
- `bytes`:在连接关闭前应传输的字节数
### HTTP API
客户端与 Toxiproxy 守护进程之间的所有通信都通过 HTTP 接口进行,此处对此进行了描述。
Toxiproxy 在端口 **8474** 上监听 HTTP。
#### Proxy 字段:
- `name`:代理名称(字符串)
- `listen`:监听地址(字符串)
- `upstream`:代理上游地址(字符串)
- `enabled`:true/false(创建时默认为 true)
要更改代理的名称,必须将其删除并重新创建。
更改 `listen` 或 `upstream` 字段将重新启动代理并断开所有活动连接。
如果 `listen` 指定的端口为 0,toxiproxy 将选择一个临时端口。响应中的 `listen` 字段将使用实际端口进行更新。
如果你将 `enabled` 更改为 `false`,它将关闭代理。你可以将其切换回 `true` 以重新启用它。
#### Toxic段:
- `name`:toxic 名称(字符串,默认为 `_`)
- `type`:toxic 类型(字符串)
- `stream`:要影响的链接方向(默认为 `downstream`)
- `toxicity`:将 toxic 应用于链接的概率(默认为 1.0,即 100%)
- `attributes`:特定 toxic 属性的映射
有关特定 toxic 的属性,请参阅 [Toxics](#toxics)。
`stream` 方向必须是 `upstream` 或 `downstream`。`upstream` 将 toxic 应用于 `client -> server` 连接,而 `downstream` 将 toxic 应用于 `server -> client` 连接。这可用于分别修改请求和响应。
#### 端点
所有端点均使用 JSON 格式。
- **GET /proxies** - 列出现有的代理及其 toxics
- **POST /proxies** - 创建一个新代理
- **POST /populate** - 创建或替换代理列表
- **GET /proxies/{proxy}** - 显示代理及其所有活动的 toxics
- **POST /proxies/{proxy}** - 更新代理的字段
- **DELETE /proxies/{proxy}** - 删除一个现有代理
- **GET /proxies/{proxy}/toxics** - 列出活动的 toxics
- **POST /proxies/{proxy}/toxics** - 创建一个新的 toxic
- **GET /proxies/{proxy}/toxics/{toxic}** - 获取一个活动 toxic 的字段
- **POST /proxies/{proxy}/toxics/{toxic}** - 更新一个活动 toxic
- **DELETE /proxies/{proxy}/toxics/{toxic}** - 移除一个活动 toxic
- **POST /reset** - 启用所有代理并移除所有活动的 toxics
- **GET /version** - 返回服务器版本号
- **GET /metrics** - 返回兼容 Prometheus 的指标
#### 填充 Proxies
可以使用 `/populate` 端点批量添加和配置代理。这是通过向 toxiproxy 传递一个代理的 json 数组来完成的。如果已存在同名代理,它将与新代理进行比较,如果 `upstream` 和 `listen` 地址不匹配,则会被替换。
例如,可以在应用程序启动时包含 `/populate` 调用,以确保所有必需的代理都存在。多次进行此调用是安全的,因为只要代理的字段与新数据一致,它们就不会被触碰。
### CLI 示例
```
$ toxiproxy-cli create -l localhost:26379 -u localhost:6379 redis
Created new proxy redis
$ toxiproxy-cli list
Listen Upstream Name Enabled Toxics
======================================================================
127.0.0.1:26379 localhost:6379 redis true None
Hint: inspect toxics with `toxiproxy-client inspect `
```
```
$ redis-cli -p 26379
127.0.0.1:26379> SET omg pandas
OK
127.0.0.1:26379> GET omg
"pandas"
```
```
$ toxiproxy-cli toxic add -t latency -a latency=1000 redis
Added downstream latency toxic 'latency_downstream' on proxy 'redis'
```
```
$ redis-cli -p 26379
127.0.0.1:26379> GET omg
"pandas"
(1.00s)
127.0.0.1:26379> DEL omg
(integer) 1
(1.00s)
```
```
$ toxiproxy-cli toxic remove -n latency_downstream redis
Removed toxic 'latency_downstream' on proxy 'redis'
```
```
$ redis-cli -p 26379
127.0.0.1:26379> GET omg
(nil)
```
```
$ toxiproxy-cli delete redis
Deleted proxy redis
```
```
$ redis-cli -p 26379
Could not connect to Redis at 127.0.0.1:26379: Connection refused
```
### 指标
Toxiproxy 通过其 HTTP API 在 /metrics 路径暴露兼容 Prometheus 的指标。
有关完整说明,请参阅 [METRICS.md](./METRICS.md)
### 常见问题
**Toxiproxy 有多快?** Toxiproxy 的速度很大程度上取决于你的硬件,但是在未启用 toxics 时,你可以预期延迟 *< 100µs*。在 Macbook Pro 上使用 `GOMAXPROCS=4` 运行时,我们达到了 *~1000MB/s* 的吞吐量,而在高端台式机上甚至高达 *2400MB/s*。基本上,你可以预期 Toxiproxy 移动数据的速度至少与你正在测试的应用程序一样快。
**Toxiproxy 可以进行随机测试吗?** 许多可用的 toxics 可以配置为具有随机性,例如 `latency` toxic 中的 `jitter`。还有一个全局 `toxicity` 参数,用于指定 toxic 将影响的连接百分比。这对于 `timeout` 这样的 toxic 最有用,它允许 X% 的连接超时。
**我没有看到我的 Toxiproxy 操作在 MySQL 上生效。** 对于某些客户端,如果主机设置为 `localhost`,MySQL 会优先使用本地 Unix 域套接字,无论你向它传递什么端口。将你的 MySQL 服务器配置为不创建套接字,并使用 `127.0.0.1` 作为主机。请记住在重启服务器后删除旧的套接字。
**Toxiproxy 导致间歇性连接失败。** 请使用临时端口范围之外的端口,以避免随机端口冲突。在 Linux 上,默认为 `32,768` 到 `61,000`,请参阅 `/proc/sys/net/ipv4/ip_local_port_range`。
**我应该为每个应用程序运行一个 Toxiproxy 吗?** 不,我们建议所有应用程序使用相同的 Toxiproxy。为了区分服务,我们建议使用以下方案命名你的代理:`___`。例如,`shopify_test_redis_master` 或 `shopify_development_mysql_1`。
### 开发
* `make`。为当前平台构建 toxiproxy 开发二进制文件。
* `make all`。为所有平台构建 Toxiproxy 二进制文件和包。需要在 Linux 和 Darwin (amd64) 上安装启用了交叉编译的 Go,以及在 `$PATH` 中包含 [`goreleaser`](https://goreleaser.com/) 以构建 Linux 软件包的二进制文件。
* `make test`。运行 Toxiproxy 测试。
### 发布
请参阅 [RELEASE.md](./RELEASE.md)
标签:EVTX分析, Go语言, Python脚本, Shopify开源, TCP代理, TCP连接操纵, Toxiproxy, 弹性测试, 故障注入, 数据包丢失, 文档结构分析, 日志审计, 测试工具, 混沌工程, 程序破解, 系统条件模拟, 网络中断, 网络延迟, 网络模拟, 网络测试, 韧性测试, 高可用性