Toxiproxy 是一个用于模拟网络条件的框架。它专门为测试、持续集成和开发环境而设计,支持对连接进行确定性篡改,同时也支持随机故障注入和自定义场景。Toxiproxy 是你需要的工具,可以通过测试证明你的应用不存在单点故障。 自 2014 年 10 月以来,我们已在 Shopify 的所有开发和测试环境中成功使用它。更多信息请参阅我们关于弹性(resiliency)的[博客文章][blog]。
Toxiproxy 的使用由两部分组成:一个用 Go 编写的 TCP 代理(即本仓库的内容),以及一个通过 HTTP 与该代理通信的客户端。你可以将应用配置为让所有测试连接都经过 Toxiproxy,然后通过 HTTP 操控它们的健康状态。有关如何配置项目的说明,请参阅下面的用法。
例如,要为来自 Ruby 客户端 的 MySQL 响应添加 1000ms 延迟:```ruby Toxiproxy[:mysql_master].downstream(:latency, latency: 1000).apply do Shop.first # this takes at least 1s end
要关闭所有 Redis 实例:```ruby
Toxiproxy[/redis/].down do
Shop.first # this will throw an exception
end
虽然本 README 中的示例目前使用的是 Ruby,但这并不妨碍你用任何其他语言创建客户端 (参见 Clients)。
我们找到的现有方案没有提供我们在集成测试和单元测试中所需要的
动态 API。像 nc 等 Linux 工具并不是跨平台的,而且需要 root 权限,
这使得它们在测试、开发和 CI 环境中存在问题。
让我们以一个 Rails 应用程序为例来演示。请注意,Toxiproxy 与 Ruby 并无任何绑定关系, Ruby 只是我们的第一个使用场景。你可以在 sirupsen/toxiproxy-rails-example 查看完整示例。 如果想立即开始,请跳到 用法。
以我们那个受欢迎的博客为例,出于某种原因,我们将文章的标签存储在
Redis 中,而文章本身存储在 MySQL 中。我们可能有一个 Post 类,
其中包含一些用于操作 Redis 集合 标签的方法:```ruby
class Post < ActiveRecord::Base
def tags TagRedis.smembers(tag_key) end
def add_tag(tag) TagRedis.sadd(tag_key, tag) end
def remove_tag(tag) TagRedis.srem(tag_key, tag) end
def tag_key "post:tags:#{self.id}" end end
我们决定在写入标签数据存储(添加/移除)时出错是可以接受的。然而,如果标签数据存储不可用,我们应该能够看到无标签的帖子。我们可以在 `tags` 方法中围绕 `SMEMBERS` Redis 调用简单地捕获 `Redis::CannotConnectError`。让我们使用 Toxiproxy 来测试这一点。
由于我们已经安装了 Toxiproxy 并且它正在我们的机器上运行,我们可以跳到第 2 步。这一步我们需要确保 Toxiproxy 具有 Redis 标签的映射。在 `config/boot.rb`(在任何连接建立之前)我们添加:```ruby
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 客户端:```ruby
TagRedis = Redis.new(port: 22222)
测试环境中的所有调用现在都通过 Toxiproxy。这意味着我们可以
添加一个单元测试来模拟故障:```ruby
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
方法,使其更具弹性:```ruby
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**```bash
$ 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:```bash $ brew tap shopify/shopify $ brew install toxiproxy
或者使用 [MacPorts](https://www.macports.org/):```bash
$ port install toxiproxy
Windows
适用于 Windows 的 Toxiproxy 可从 https://github.com/Shopify/toxiproxy/releases/download/v2.1.4/toxiproxy-server-windows-amd64.exe 下载
Docker
Toxiproxy 可在 Github 容器注册表 上获取。
旧版本 <= 2.1.4 可在 Docker Hub 上获取。```bash
$ docker pull ghcr.io/shopify/toxiproxy
$ docker run --rm -it ghcr.io/shopify/toxiproxy
如果从主机而不是其他容器使用 Toxiproxy,请使用 `--net=host` 启用主机网络。```shell
$ docker run --rm --entrypoint="/toxiproxy-cli" -it ghcr.io/shopify/toxiproxy list
如果你已经安装了 Go,你可以使用 Makefile 从源代码构建 Toxiproxy:```bash $ make build $ ./toxiproxy-server
#### 从 Toxiproxy 1.x 升级
在 Toxiproxy 2.0 中,API 发生了若干变化,使其与 1.x 版本不兼容。
为了使用 Toxiproxy 服务器 2.x 版本,你需要确保你的客户端
库支持相同的版本。你可以通过查看 `/version` 端点来检查你运行的 Toxiproxy 版本。
有关具体的库变更,请参阅你的客户端库的文档。Toxiproxy 服务器的详细变更
可在 [CHANGELOG.md](https://github.com/shopify/toxiproxy/blob/HEAD/CHANGELOG.md) 中找到。
### 2. 填充 Toxiproxy
当你的应用程序启动时,它需要确保 Toxiproxy 知道哪些
端点要代理到哪里。主要参数有:名称、Toxiproxy 要**监听**的地址以及上游的地址。
一些客户端库为此任务提供了辅助函数,这本质上就是
确保列表中的每个代理都被创建。来自 Ruby 客户端的示例:```ruby
# Make sure `shopify_test_redis_master` and `shopify_test_mysql_master` are
# present in 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 创建代理,例如:```bash toxiproxy-cli create -l localhost:26379 -u localhost:6379 shopify_test_redis_master
我们建议采用如上所述的命名方式:`<app>_<env>_<data store>_<shard>`。
这可以确保使用同一个 Toxiproxy 的应用程序之间不会发生冲突。
对于大型应用程序,我们建议将 Toxiproxy 配置存储在单独的配置文件中。我们使用 `config/toxiproxy.json`。此文件可以通过 `-config` 选项传递给服务器,或由应用程序加载以配合 `populate` 函数使用。
一个示例 `config/toxiproxy.json`:```json
[
{
"name": "web_dev_frontend_1",
"listen": "[::]:https://raw.githubusercontent.com/shopify/toxiproxy/HEAD/18080%22,
"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。
要使用 Toxiproxy,你现在需要将应用程序配置为通过 Toxiproxy 连接。继续我们第二步中的示例,我们可以将 Redis 客户端配置为通过 Toxiproxy 连接:```ruby
redis = Redis.new(port: 6380)
redis = Redis.new(port: 22220)
现在你可以通过 Toxiproxy API 对其进行干扰。在 Ruby 中:```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:```bash 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](https://github.com/shopify/toxiproxy/blob/HEAD/CREATING_TOXICS.md)
#### latency
为所有通过代理的数据添加延迟。延迟等于 `latency` +/- `jitter`。
Attributes:
- `latency`: 时间(毫秒)
- `jitter`: 时间(毫秒)
#### down
在 Toxiproxy 的实现中,使服务下线在技术上并不是一种 toxic。这是通过对 `/proxies/{proxy}` 执行 `POST` 并将 `enabled` 字段设置为 `false` 来完成的。
#### bandwidth
将连接限制为每秒最大千字节数。
Attributes:
- `rate`: 以 KB/s 为单位的速率
#### slow_close
延迟 TCP 套接字的关闭,直到经过 `delay` 时间。
Attributes:
- `delay`: 时间(毫秒)
#### timeout
阻止所有数据通过,并在 `timeout` 后关闭连接。如果 `timeout` 为 0,连接不会关闭,数据将被丢弃,直到移除该 toxic。
Attributes:
- `timeout`: 时间(毫秒)
#### reset_peer
通过立即或经过 `timeout` 后关闭 stub Input,在连接上模拟 TCP RESET(对端重置连接)。
Attributes:
- `timeout`: 时间(毫秒)
#### slicer
将 TCP 数据切分成小块,并可以选择在每个切分后的“数据包”之间添加延迟。
Attributes:
- `average_size`: 平均数据包的大小(字节)
- `size_variation`: 平均数据包的字节变化量(应小于 average_size)
- `delay`: 每个数据包延迟的时间(微秒)
#### limit_data
当传输的数据超过限制时关闭连接。
- `bytes`: 在连接关闭之前应传输的字节数
#### packet_loss
随机丢弃通过代理的数据块,模拟不稳定的 Wi-Fi、移动网络或卫星网络条件。
Attributes:
- `loss_rate`: 数据块被丢弃的概率 [0.0-1.0](默认 0.0)
- `correlation`: 当上一个数据块被丢弃时额外的丢弃概率,用于建模突发丢包(默认 0.0)
### HTTP API
客户端与 Toxiproxy 守护进程的所有通信都通过 HTTP 接口进行,接口描述如下。
Toxiproxy 在端口 **8474** 上监听 HTTP。
#### 代理字段:
- `name`: 代理名称(字符串)
- `listen`: 监听地址(字符串)
- `upstream`: 代理上游地址(字符串)
- `enabled`: true/false(创建时默认为 true)
要更改代理名称,必须删除并重新创建代理。
更改 `listen` 或 `upstream` 字段将重启代理并断开所有活动连接。
如果 `listen` 指定的端口为 0,toxiproxy 将选择一个临时端口。响应中的 `listen` 字段将更新为实际端口。
如果将 `enabled` 改为 `false`,代理将下线。你可以将其切换回 `true` 以重新启用。
#### Toxic 字段:
- `name`: toxic 名称(字符串,默认为 `<type>_<stream>`)
- `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 兼容的指标
#### 填充代理
可以使用 `/populate` 端点批量添加和配置代理。这是通过向 toxiproxy 传递一个代理的 json 数组来完成的。如果已存在同名的代理,则会将其与新代理进行比较,如果 `upstream` 和 `listen` 地址不匹配,则替换该代理。
例如,可以在应用程序启动时包含 `/populate` 调用,以确保所有必需的代理存在。多次调用此操作是安全的,因为只要代理的字段与新数据一致,代理就不会被修改。
### CLI 示例```bash
$ 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 <proxyName>`
未收到任何输入内容。请在“INPUT:”之后提供英文Markdown内容,我将继续以目标语言(zh)进行翻译。```bash $ redis-cli -p 26379 127.0.0.1:26379> SET omg pandas OK 127.0.0.1:26379> GET omg "pandas"
I don't see any source content to translate — the input after "INPUT:" is empty. Please provide the actual chunk 43 text, and I'll translate it into zh according to your rules.```bash
$ toxiproxy-cli toxic add -t latency -a latency=1000 redis
Added downstream latency toxic 'latency_downstream' on proxy 'redis'
The input appears to be empty — no Markdown content was provided after "INPUT:". There is nothing to translate in this chunk.```bash $ 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)
I'm ready to translate chunk 47 of 55 from English to Chinese, but the input you provided appears to be empty—there is no actual Markdown content after "INPUT:".
Please provide the chunk's content, and I'll translate it immediately following all the rules.```bash
$ toxiproxy-cli toxic remove -n latency_downstream redis
Removed toxic 'latency_downstream' on proxy 'redis'
The INPUT section is empty. Please provide the content you want me to translate.```bash $ redis-cli -p 26379 127.0.0.1:26379> GET omg (nil)
请提供需要翻译的 Markdown 内容。```bash
$ toxiproxy-cli delete redis
Deleted proxy redis
I don't see any content after "INPUT:" in your message. There's no text provided for me to translate.
Please paste the Markdown content you'd like translated from English to Chinese, and I'll translate it according to the rules you've specified.```bash $ redis-cli -p 26379 Could not connect to Redis at 127.0.0.1:26379: Connection refused
### 指标
Toxiproxy 通过其 HTTP API 在 /metrics 暴露兼容 Prometheus 的指标。
完整描述请参阅 [METRICS.md](https://github.com/shopify/toxiproxy/blob/HEAD/METRICS.md)
### 常见问题
**Toxiproxy 有多快?** Toxiproxy 的速度在很大程度上取决于你的硬件,
但在未启用任何 toxic 时,你可以预期延迟为 *< 100µs*。在
使用 `GOMAXPROCS=4` 在 Macbook Pro 上运行时,我们实现了 *~1000MB/s* 的吞吐量,而在
更高端的台式机上则高达 *2400MB/s*。基本上,你可以预期 Toxiproxy 移动数据的
速度至少与你正在测试的应用一样快。
**Toxiproxy 可以进行随机化测试吗?** 许多可用的 toxic 都可以配置为
具有随机性,例如 `latency` toxic 中的 `jitter`。此外,还有一个全局的
`toxicity` 参数,用于指定一种 toxic 将影响的连接百分比
。这对于 `timeout` 之类的 toxic 最为有用,
它可以让 X% 的连接超时。
**我没有看到我的 Toxiproxy 操作对 MySQL 生效**。MySQL 会优先
为某些客户端选择本地 Unix 域套接字,无论你传入哪个端口,
只要主机设置为 `localhost`。请将 MySQL 服务器配置为不创建
套接字,并使用 `127.0.0.1` 作为主机。请记得在重启服务器后
删除旧的套接字。
**Toxiproxy 会导致间歇性连接失败**。使用临时端口范围
之外的端口,以避免随机的端口冲突。在 Linux 上,默认范围是 `32,768` 到 `61,000`
,参见 `/proc/sys/net/ipv4/ip_local_port_range`。
**我应该为每个应用程序运行一个 Toxiproxy 吗?** 不,我们建议所有应用程序共用
同一个 Toxiproxy。为了区分不同服务,我们
建议按以下模式命名你的代理:`<app>_<env>_<data store>_<shard>`。
例如,`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](https://github.com/shopify/toxiproxy/blob/HEAD/RELEASE.md)
[blog]: https://shopify.engineering/building-and-testing-resilient-ruby-on-rails-applications