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/main/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/main/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。