ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Toxiproxy 实战指南:用 Go TCP 代理在测试、CI 与开发环境中模拟网络故障

2026/10/7 17:50:20 拓冰建站 浏览量
Toxiproxy 实战指南:用 Go TCP 代理在测试、CI 与开发环境中模拟网络故障 测试网络【免费下载链接】toxiproxy:alarm_clock: :fire: A TCP proxy to simulate network and system conditions for chaos and resiliency testing项目地址https://gitcode.com/gh_mirrors/to/toxiproxy点击查看免费下载Toxiproxy 是 Shopify 开源的 TCP 代理框架专门用于在测试、CI 与开发环境中确定性或随机性地模拟网络故障例如延迟、断连、带宽受限、数据切片等。本文以官方 README 为骨架结合仓库源码proxy.go、api.go、toxics/toxic.go、cmd/server/server.go 等深入讲解其架构、安装、代理填充、Toxic 机制、HTTP API 与 CLI 实战读完后你将能独立把任意 TCP 服务接入 Toxiproxy并用测试证明你的应用不存在单点故障。什么是 ToxiproxyToxiproxy 是一个模拟网络状况的框架。它的核心是一个用 Go 编写的 TCP 代理即本仓库外加一个通过 HTTP 与代理通信的客户端库。你可以把应用的所有测试连接都改走 Toxiproxy再通过 HTTP 接口任意操控这些连接的健康状况——加延迟、断掉连接、限速、丢包从而在单元测试与集成测试中验证应用的韧性resiliency。官方 README 的定位是Toxiproxy 就是那个能让你用测试证明『你的应用没有单点故障』的工具Shopify 自 2014 年 10 月起就在所有开发与测试环境中使用它。Toxiproxy 的用法由两部分组成见 README.mdGo 编写的 TCP 代理本仓库内容监听端口、转发流量、注入故障。与代理通过 HTTP 通信的客户端你可以在任何语言里实现客户端官方提供了 Go 客户端仓库内client/目录以及社区维护的 Ruby、Python、.NET、PHP、Node、Java、Haskell、Rust、Elixir 等语言的客户端。以官方 README 中 Ruby 客户端为例给 MySQL 的下行响应加 1000ms 延迟Toxiproxy[:mysql_master].downstream(:latency, latency: 1000).apply do Shop.first # 这一行至少耗时 1s end把全部 Redis 实例都弄下线Toxiproxy[/redis/].down do Shop.first # 这一行将抛出异常 end为什么还要再造一个混沌 TCP 代理官方 README 给出的理由是现有工具无法提供集成测试和单元测试所需的动态 API。像 Linux 的nc等命令行工具不跨平台、且通常需要 root 权限在测试、开发与 CI 环境中非常不便。Toxiproxy 则以普通用户即可运行的方式提供了可通过 HTTP 随时创建/修改/删除故障注入点的能力。工作原理与请求路径从源码看Toxiproxy 的代理模型非常清晰每个 Proxy 负责接受新客户端并在客户端与上游之间建立 Link见 proxy.go 的注释Client - toxiproxy - Upstream。每个连接被拆成两条链路linkupstream方向client - server与downstream方向server - client见 proxy.goserver()循环负责Accept()新连接、拨号上游并为两个方向分别启动一条带 Toxic 的链路。Toxic 以管道pipeline方式串联在数据流中Client - ToxicStub - Upstream多个 Toxic 可以通过 channel 链式叠加见 toxics/toxic.go。安装 Toxiproxy官方 README 提供了多平台安装方式Ubuntu / Debiandpkg 包$ 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 startmacOS$ brew tap shopify/shopify $ brew install toxiproxy # 或使用 MacPorts $ port install toxiproxyWindows可在官方 Releases 页面下载toxiproxy-server-windows-amd64.exe。Docker$ docker pull ghcr.io/shopify/toxiproxy $ docker run --rm -it ghcr.io/shopify/toxiproxy若要从宿主机而非其他容器访问 Toxiproxy需要启用宿主机网络模式--nethost。也可以直接运行镜像内的 CLI$ docker run --rm --entrypoint/toxiproxy-cli -it ghcr.io/shopify/toxiproxy list从源码构建需要 Go 环境仓库根目录提供 Makefile$ make build $ ./toxiproxy-server服务器启动参数与日志服务器入口在 cmd/server/server.go支持以下启动参数参数默认值说明-hostlocalhostToxiproxy HTTP API 的监听主机-port8474Toxiproxy HTTP API 的监听端口-config空启动时加载的 JSON 代理配置文件对应 README 的-config选项-seed当前时间纳秒随机化 Toxic 的随机种子可复现混沌实验-version关闭打印服务器版本号-proxy-metrics关闭启用 toxiproxy 特有的 Prometheus 指标-runtime-metrics关闭启用 Go runtime 相关 Prometheus 指标日志级别包括panic、fatal、error、warn/warning、info、debug、trace通过环境变量LOG_LEVEL设置源码见 cmd/server/server.go使用 zerolog 实现日志带时间戳与调用位置。从 Toxiproxy 1.x 升级Toxiproxy 2.0 对 API 做了多项不兼容改动。使用 2.x 服务器时必须确保客户端库版本与之匹配。可以通过GET /version端点查看当前运行的服务器版本实现见 api.go。服务器端的详细变更记录见 CHANGELOG.md。第二步填充PopulateToxiproxy应用启动时需要先告诉 Toxiproxy哪些端点要代理到哪里。核心参数有三个name代理名称listenToxiproxy 要监听的地址upstream上游真实服务的地址。很多客户端库提供 populate 辅助方法本质就是确保列表中的每个代理都被创建。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 建立之前、尽可能早地运行官方 Rails 示例放在config/boot.rb中见下文。也可以直接用 CLI 创建代理toxiproxy-cli create -l localhost:26379 -u localhost:6379 shopify_test_redis_master官方推荐的命名规范是app_env_data store_shard如shopify_test_redis_master这样能避免多个应用共用同一个 Toxiproxy 时发生名字冲突。使用 JSON 配置文件批量填充对大型应用官方建议把 Toxiproxy 配置放到独立配置文件如config/toxiproxy.json中可传给服务器-config选项启动加载也可由应用读取后调用populate。服务器加载逻辑见 api.go 的PopulateConfig示例配置[ { 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。第三步使用 Toxiproxy 注入故障填充好代理后把应用连接改到 Toxiproxy 的监听端口即可。延续上面的例子把 Redis 客户端从直连改为走代理# 旧直连 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) # 将耗时 1s end等价地用 CLI 完成同样的事toxiproxy-cli toxic add -t latency -a latency1000 shopify_test_redis_master完整实战用单元测试证明 Redis 故障下的降级逻辑官方 README 用一个 Rails 博客的例子串起全部流程。场景文章正文存 MySQL文章的标签tags存在 Redis 的 Set 中Post类通过TagRedis操作标签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 # 返回文章标签在 Redis 中的 key def tag_key post:tags:#{self.id} end end业务上写标签增删出错可以接受但如果标签存储挂了我们应该仍能查看文章只是没标签。于是先让测试环境的所有 Redis 调用走 Toxiproxy。在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指向走代理的端口TagRedis Redis.new(port: 22222)这样测试环境的所有调用都经过 Toxiproxy。接下来写一个模拟故障的单元测试把代理临时下线down验证tags返回空数组而不是抛异常test should return empty array when tag redis is down when listing tags do post.add_tag mammals # 把 Toxiproxy 中所有 Redis 代理下线 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 挂掉时整篇博客页仍能正常加载的场景。Toxiproxy 本身与语言无关这里只是用了 Ruby 作为第一个落地场景。Toxics 详解注入故障的核心机制Toxic 是 Toxiproxy 的灵魂。它们操纵客户端与上游之间的管道可以通过 HTTP API 随时添加/移除。每个 Toxic 有各自的参数来改变对代理链路的影响。自定义 Toxic 的开发文档见 CREATING_TOXICS.md。从源码看Toxic 的核心抽象在 toxics/toxic.go一个Toxic只需实现Pipe(*ToxicStub)方法定义数据包如何流经一个ToxicStub。Toxic 以管道方式工作、可链式串联每个ToxicStub保存单条连接的状态因此同一个 Toxic 可同时作用于多条连接。注册机制见 toxics/toxic.go 的Register(typeName, toxic)每种内建 Toxic 都在各自的init()中注册如latency、bandwidth、timeout等。每个 Toxic 通用字段HTTP JSON 表示包括nameToxic 名称字符串默认type_streamtypeToxic 类型字符串stream作用的链路方向默认downstreamtoxicityToxic 应用到某条连接的概率默认1.0即 100%attributesToxic 特有参数映射。stream只能是upstream或downstream。upstream作用于client - server方向的连接downstream作用于server - client方向的连接因此可以分别篡改请求与响应。方向定义见 stream/direction.go。toxicity的实现很巧妙在 toxics/toxic.go 的ToxicStub.Run()中每次生成一个[0,1)随机数若小于toxicity则真正执行该 Toxic 的Pipe()否则执行NoopToxic直通。这就是随机混沌的机制基础。下面逐个介绍官方 README 中列出的 Toxic。latency注入延迟给所有经过代理的数据增加延迟延迟量为latency±jitter。latency毫秒jitter毫秒。源码 toxics/latency.go 的delay()为latency rand.Int63n(jitter*2) - jitter即均匀随机抖动。注意该 Toxic 是有缓冲的其GetBufferSize()返回 1024 字节toxics/latency.go。down让服务下线严格来说down不是 Toxic 实现而是通过向POST /proxies/{proxy}发送请求、把enabled字段设为false来实现的见 proxy.go关闭代理会终止其监听并关闭所有活动连接。重新置为true即可恢复。bandwidth限制带宽把连接限制为每秒最多 N 千字节。rateKB/s。源码 toxics/bandwidth.go 的实现思路是累积应睡眠的时间来平滑限速当数据包过大超过rate*100字节时还会把包拆成每 100ms 发送一批toxics/bandwidth.go避免突发吞吐。slow_close延迟关闭延迟 TCP 套接字的关闭直到delay毫秒过去。delay毫秒。源码 toxics/slow_close.go当收到数据流结束信号nil chunk时先等待delay毫秒再真正关闭。timeout数据黑洞 超时断连阻止所有数据通过并在timeout毫秒后关闭连接。若timeout为 0则连接不会关闭数据持续被丢弃直到 Toxic 被移除。timeout毫秒。源码 toxics/timeout.go 清晰地展示了两种分支timeout 0时倒计时断连否则无限期丢弃数据注释写着 Drop the data on the ground。它还实现了Cleanup()在 Toxic 被移除时主动关闭 stubtoxics/timeout.go。reset_peer模拟 TCP RST模拟对端重置连接Connection reset by peer立即或延迟timeout毫秒后关闭 stub 的输入。timeout毫秒为 0 时立即重置。源码 toxics/reset_peer.go 注释说明通过将SetLinger设为 0 来丢弃未发送/未确认的数据等效于置位 TCP RST 标志并重置连接同时丢弃数据以避免优雅关闭FIN/ACK导致的io.EOF。slicer数据切片把 TCP 数据切成许多小块可选地在每个切片包之间加延迟模拟真实网络的分包行为。average_size平均包大小字节size_variation包大小的浮动范围字节应小于average_sizedelay每个包之间的延迟微秒。源码 toxics/slicer.go 用递归二分法切分数据并在切分点叠加 ±size_variation的随机偏移保证切片大小尽量均匀发送每个切片之间等待delay微秒toxics/slicer.go。limit_data传输量限制当传输的数据量超过限制时关闭连接。bytes连接关闭前允许传输的字节数。这是唯一一个有状态的内建 Toxic源码 toxics/limit_data.go 通过LimitDataToxicState记录bytesTransmitted并实现StatefulToxic.NewState()为每条连接创建独立状态toxics/limit_data.go达到限制后截断剩余数据并关闭toxics/limit_data.go。HTTP API 参考所有客户端与 Toxiproxy 守护进程的通信都通过 HTTP 完成官方 README 完整描述了这套接口。Toxiproxy 的 HTTP 服务监听 8474 端口。所有端点均为 JSON。Proxy 字段name代理名称字符串listen监听地址字符串upstream上游地址字符串enabledtrue/false创建时默认 true。重要行为均有源码佐证修改代理名称必须删除后重建修改listen或upstream会重启代理并丢弃所有活动连接见 proxy.go 的Update()监听/上游变化时先stop再更新若listen端口填 0Toxiproxy 会挑选一个临时端口响应中的listen字段会更新为实际端口见 proxy.go 的listen()监听成功后用listener.Addr().String()回写把enabled改为false即下线代理改回true重新启用。Toxic 字段nameToxic 名称字符串默认type_streamtypeToxic 类型字符串stream作用的链路方向默认downstreamtoxicityToxic 应用到某条连接的概率默认 1.0即 100%attributesToxic 特有参数映射见上文各 Toxic 的参数。端点一览方法路径作用GET/proxies列出所有代理及其 ToxicPOST/proxies创建新代理POST/populate批量创建或替换代理列表GET/proxies/{proxy}查看单个代理及全部活动 ToxicPOST/proxies/{proxy}更新代理字段源码提示 POST 已弃用推荐 PATCHDELETE/proxies/{proxy}删除代理GET/proxies/{proxy}/toxics列出活动 ToxicPOST/proxies/{proxy}/toxics创建新 ToxicGET/proxies/{proxy}/toxics/{toxic}查看单个 Toxic 字段POST/proxies/{proxy}/toxics/{toxic}更新活动 Toxic同样推荐 PATCHDELETE/proxies/{proxy}/toxics/{toxic}移除活动 ToxicPOST/reset启用所有代理并移除全部活动 ToxicGET/version返回服务器版本号GET/metrics返回 Prometheus 兼容指标路由注册代码见 api.go全部端点均挂在 gorilla/mux 路由器上。此外api.go 定义了标准错误码请求体错误/缺字段为 400、找不到代理或 Toxic 为 404、代理或 Toxic 重名冲突为 409、stream 方向非法为 400、Toxic 类型非法为 400。Populate 语义代理可以通过/populate端点批量添加与配置只需向 Toxiproxy 传入一个 JSON 数组。如果同名代理已存在Toxiproxy 会与新的配置比较当upstream和listen不一致时才替换它实现入口 api.go 的Populate底层在ProxyCollection.PopulateJson。因此/populate非常适合在应用启动时调用以确保所需代理全部存在它也可以安全地重复调用——只要字段与现有代理一致代理就不会被改动。CLI 实战演练官方 README 给出了完整的 CLI 演练我们逐段走一遍。假设本机有 Redis 在 6379 端口先用 CLI 创建名为redis的代理监听 26379转发到 6379$ 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直接通过代理访问 Redis 是正常的$ redis-cli -p 26379 127.0.0.1:26379 SET omg pandas OK 127.0.0.1:26379 GET omg pandas给redis代理加一个 1000ms 的 latency Toxic$ toxiproxy-cli toxic add -t latency -a latency1000 redis Added downstream latency toxic latency_downstream on proxy redis注意默认创建的 Toxic 名为latency_downstream正对应type_stream的默认命名规则且作用于downstream方向。再次访问每个命令都慢 1 秒$ 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)移除该 Toxic$ 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 refusedCLI 默认连接http://localhost:8474可用-h/--host指定其他地址或设置环境变量TOXIPROXY_URL见 cmd/cli/cli.go。CLI 还内置了 Toxic 各参数与toxic add/update/delete的用法说明例如toxiproxy-cli toxic add -t latency -n myToxic -a latency100 -a jitter50 myProxy完整帮助文本见 cmd/cli/cli.go。指标MetricsToxiproxy 通过 HTTP API 的/metrics端点暴露 Prometheus 兼容指标指标类型包括代理级指标需-proxy-metrics开启与运行时指标需-runtime-metrics开启详细描述见 METRICS.md。常见问题FAQToxiproxy 有多快官方 README 声明速度很大程度上取决于硬件无 Toxic 启用时延迟可低至 100µs在 Macbook Pro 上以GOMAXPROCS4运行时吞吐约1000MB/s更高端桌面可达2400MB/s。基本可以认为 Toxiproxy 的数据搬运速度至少与应用本身相当。能随机化测试吗可以。许多 Toxic 支持随机参数如 latency 的jitter还有全局toxicity参数控制 Toxic 影响连接的概率默认 1.0。这对timeout这类 Toxic 尤其有用——可以只让 X% 的连接超时。随机种子可通过-seed参数固定实现可复现的混沌实验。为什么 MySQL 的故障注入没生效MySQL 的部分客户端在 host 为localhost时会优先使用本地 Unix 域套接字无论你传什么端口。解决办法配置 MySQL 服务器不创建套接字并把 host 设为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。开发与发布仓库根目录 Makefile 提供开发常用命令make构建当前平台的开发版二进制make all交叉编译所有平台的二进制与安装包需要支持跨编译的 Go 环境以及goreleaser来构建 Linux 包make test运行 Toxiproxy 全部测试仓库内*_test.go覆盖了各 Toxic、Proxy、HTTP API 与客户端例如 toxics/latency_test.go、api_test.go、client/client_test.go。版本发布流程详见 RELEASE.md版本号定义在 version.go。总结Toxiproxy 的核心工作流可以概括为三步安装代理 → 填充populate代理映射 → 让应用连接走代理再通过 HTTP API 或 CLI 注入故障。借助upstream/downstream双向链路、8 种内建 Toxic 以及toxicity随机概率机制你可以把网络故障从偶发事故变成测试套件中的确定性用例真正用测试证明应用没有单点故障。赞分享测试网络【免费下载链接】toxiproxy:alarm_clock: :fire: A TCP proxy to simulate network and system conditions for chaos and resiliency testing项目地址https://gitcode.com/gh_mirrors/to/toxiproxy点击查看免费下载相关推荐如何使用Godot Game Template从安装到运行的简单5步教程 如何使用Godot Game Template从安装到运行的简单5步教程 Godot Game Template 是一个专为Godot游戏引擎设计的通用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考