Toxiproxy 是一个用于模拟网络状况的框架。它专门为在测试、CI 和开发环境中工作而设计,支持对连接进行确定性的篡改,同时也支持随机混沌和自定义。Toxiproxy 是你需要的工具,用来通过测试证明你的应用不存在单点故障。 自 2014 年 10 月起,我们已在 Shopify 的所有开发和测试环境中成功使用它。有关弹性的更多信息,请参阅我们的[博客文章][blog]。
Toxiproxy 的使用由两部分组成。一个用 Go 编写的 TCP 代理(即本仓库所包含的内容),以及一个通过 HTTP 与代理通信的客户端。你将应用配置为让所有测试连接都经过 Toxiproxy,然后就可以通过 HTTP 操纵它们的健康状况。有关如何设置你的项目,请参阅下方的用法。
例如,要使用 Ruby 客户端为 MySQL 的响应添加 1000ms 的延迟:
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)。
我们找到的现有工具无法提供集成测试和单元测试所需的动态 API。像 nc 等 Linux 工具不具备跨平台性,并且需要 root 权限,这在测试、开发和 CI 环境中会带来问题。
让我们通过一个 Rails 应用程序的示例来演示。请注意,Toxiproxy 与 Ruby 没有任何绑定关系,它只是我们最初的用例。你可以在 sirupsen/toxiproxy-rails-example 查看完整示例。 如果想立即开始,请跳转到用法。
对于我们那个受欢迎的博客,出于某种原因,我们将文章的标签存储在
Redis 中,而文章本身存储在 MySQL 中。我们可能有一个 Post 类,
其中包含一些在 Redis 集合中操作标签的方法:
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。
将项目配置为使用 Toxiproxy 包含三个步骤:
Linux
请参阅 Releases 获取适用于你架构的最新二进制文件和系统软件包。
Ubuntu
$ wget -O toxiproxy-2.13.0.deb https://github.com/Shopify/toxiproxy/releases/download/v2.13.0/toxiproxy_2.13.0_linux_amd64.deb
$ sudo dpkg -i toxiproxy-2.13.0.deb
$ sudo service toxiproxy start
OS X
使用 Homebrew:
$ brew tap shopify/shopify
$ brew install toxiproxy
或使用 MacPorts:
$ port install toxiproxy
Windows
适用于 Windows 的 Toxiproxy 可从 https://github.com/Shopify/toxiproxy/releases/latest/download/toxiproxy-server-windows-amd64.exe 下载, 配套的 CLI 位于 https://github.com/Shopify/toxiproxy/releases/latest/download/toxiproxy-cli-windows-amd64.exe
Docker
Toxiproxy 可在 Github 容器镜像仓库 获取。
旧版本 <= 2.1.4 可在 Docker Hub 上获取。
$ docker pull ghcr.io/shopify/toxiproxy
$ docker run --rm -it ghcr.io/shopify/toxiproxy
如果从宿主机而非其他容器使用 Toxiproxy,请使用 --net=host 启用主机网络。
该镜像将 API 绑定到 0.0.0.0,因此在主机网络模式下,可通过每个主机接口访问它;
如果其他机器能够访问该主机,请传入 -e TOXIPROXY_AUTH_TOKEN=<token>(参见 HTTP API)。
$ docker run --rm --entrypoint="/toxiproxy-cli" -it ghcr.io/shopify/toxiproxy list
源码
如果你已安装 Go,可以使用 make 文件从源码构建 Toxiproxy:
$ make build
$ ./dist/toxiproxy-server
在 Toxiproxy 2.0 中,API 进行了若干更改,使其与 1.x 版本不兼容。
要使用 2.x 版本的 Toxiproxy 服务器,你需要确保你的客户端库支持相同的版本。你可以通过查看 /version 端点来检查你正在运行的 Toxiproxy 版本。
有关特定客户端库的更改,请参阅其文档。Toxiproxy 服务器的详细更改可在 CHANGELOG.md 中找到。
当你的应用程序启动时,它需要确保 Toxiproxy 知道哪些端点代理到哪里。主要参数包括:名称、Toxiproxy 要监听的地址以及上游地址。
一些客户端库为此任务提供了辅助方法,其本质只是确保列表中的每个代理都被创建。以下是 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 建立连接。请查阅你的客户端库文档,了解 population 辅助函数的相关说明。
或者使用 CLI 创建代理,例如:
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 示例: