- 测试
- 网络
【免费下载链接】toxiproxy
:alarm_clock: :fire: A TCP proxy to simulate network and system conditions for chaos and resiliency testing
Toxiproxy 是一款用于模拟网络与系统故障的 TCP 代理,专为混沌工程(Chaos Engineering)与容错性测试设计。本指南以仓库中的 _examples/tests 示例为骨架,完整演示如何在本地 Kubernetes(kind)环境中部署一个真实的 PostgreSQL 数据库,在应用与数据库之间插入 Toxiproxy 代理,并通过 Go 测试代码向连接注入延迟(latency)与连接重置(reset_peer)故障,验证应用在数据库故障下的行为。读完本文,你将掌握一套可直接复制的"真实数据库 + 故障注入"测试环境搭建流程,以及 Toxiproxy 客户端 API 的使用方法。
一、示例项目在做什么:一条真实的"代理→数据库"链路
在动手之前,先理解这个示例的整体拓扑。它不是一个抽象玩具,而是一条完整的真实链路:
Go 应用 (main.go) │ 连接 localhost:35432(经由 Toxiproxy 代理 "postgresql") ▼ Toxiproxy Server (localhost:8474,HTTP 管理 API) │ 代理 "postgresql":Listen localhost:35432 → Upstream localhost:5432 ▼ PostgreSQL (kind 集群内,NodePort 30950 → 容器 5432)- 应用通过端口
35432访问数据库,而这个端口正是 Toxiproxy 代理的监听端口; - 代理把流量转发给真实数据库
localhost:5432; - 测试代码通过 Toxiproxy 的 HTTP API(默认
8474)动态地在代理上挂载各种 Toxic(故障注入器),从而在不修改应用、不重启数据库的前提下,随心所欲地制造延迟、断连、限速等故障。
这一"代理前置"模式正是 Toxiproxy 的核心用法:故障注入对应用完全透明,应用以为自己在直连数据库,实际上每一次读写都经过了可随时"下毒"的代理。
二、环境准备与基础设施搭建(Setup)
原文档给出的 Setup 流程分为两步:安装工具链、用 kind 拉起 Kubernetes 集群并部署 PostgreSQL。完整命令如下:
$ brew install shopify/shopify/toxiproxy kind $ kind create cluster --config=cluster.yml $ kubectl --context kind-kind apply -f resources.yml $ kubectl wait deploy postgres --for condition=available --timeout=5m $ psql -h 127.0.0.1 -U postgres -c "DROP DATABASE IF EXISTS sample" $ psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE sample" $ psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE sample_test"下面逐条拆解每条命令的职责与背后配置。
1. 安装 Toxiproxy 与 kind
$ brew install shopify/shopify/toxiproxy kindtoxiproxy:由 Shopify 维护的 Homebrew tap 提供的 Toxiproxy 服务端二进制。安装后可直接以toxiproxy-server方式运行;不过本示例的测试代码选择了在测试进程内内嵌Toxiproxy Server(详见第五、六节),因此这条安装更多是提供命令行工具(如toxiproxy-cli)以备手动排查。kind(Kubernetes IN Docker):在本地 Docker 中运行单节点 Kubernetes 集群的轻量工具,用于承载示例中的 PostgreSQL。
2. 创建 kind 集群:cluster.yml 的作用
cluster.yml 是 kind 的集群定义文件,其关键设计是把集群节点的端口映射到宿主机:
kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 networking: ipFamily: ipv4 nodes: - role: control-plane extraPortMappings: # port forward 5432 on the host to 5432 on this node - containerPort: 30950 hostPort: 5432 # optional: set the bind address on the host # 0.0.0.0 is the current default listenAddress: 127.0.0.1 # optional: set the protocol to one of TCP, UDP, SCTP. # TCP is the default protocol: TCPipFamily: ipv4:显式指定集群仅使用 IPv4,避免双栈带来的寻址歧义;extraPortMappings:把宿主机127.0.0.1:5432转发到 kind 控制面节点的30950端口,而30950正是后面 resources.yml 中 PostgreSQL Service 的nodePort。这一层"hostPort → nodePort"的映射,使得宿主机上的应用和 psql 都能以127.0.0.1:5432直接访问集群内数据库;listenAddress: 127.0.0.1将端口绑定限制在本机回环地址,保证安全;protocol: TCP明确转发协议(默认即 TCP)。
3. 部署 PostgreSQL:resources.yml 的三段式清单
resources.yml 是 Kubernetes 资源清单,包含三个对象:ConfigMap、Deployment 与 Service。
ConfigMap提供数据库的环境变量配置:
apiVersion: v1 kind: ConfigMap metadata: name: postgres labels: app: postgres data: POSTGRES_DB: postgres POSTGRES_USER: postgres POSTGRES_PASSWORD: Welcome POSTGRES_HOST_AUTH_METHOD: trust注意POSTGRES_HOST_AUTH_METHOD: trust表示集群内不做密码校验,这是本地测试环境常用的简化手段,生产环境切勿如此配置。
Deployment负责拉起单个 PostgreSQL 容器:
apiVersion: apps/v1 kind: Deployment metadata: name: postgres labels: app: postgres spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres imagePullPolicy: IfNotPresent ports: - containerPort: 5432 envFrom: - configMapRef: name: postgresimage: postgres使用官方镜像,imagePullPolicy: IfNotPresent优先复用本地已有镜像以加速启动;- 容器监听
5432,环境变量整体来自 ConfigMap。
Service以 NodePort 形式暴露数据库,这正是与 cluster.yml 端口映射衔接的关键:
apiVersion: v1 kind: Service metadata: name: postgres labels: app: postgres spec: type: NodePort ports: - port: 5432 nodePort: 30950 selector: app: postgres三条命令依次执行即可完成部署与就绪等待:
$ kubectl --context kind-kind apply -f resources.yml $ kubectl wait deploy postgres --for condition=available --timeout=5m--context kind-kind指定使用 kind 创建的集群上下文;kubectl wait ... --for condition=available会阻塞直到 Deployment 的 Pod 处于可用状态,超时 5 分钟即失败退出。
4. 初始化数据库
$ psql -h 127.0.0.1 -U postgres -c "DROP DATABASE IF EXISTS sample" $ psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE sample" $ psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE sample_test"示例需要两个库:sample供go run ./运行的应用使用,sample_test供go test使用(测试通过代理连接的正是sample_test,见 db_test.go)。DROP DATABASE IF EXISTS保证了可重复执行的环境初始化。
三、运行示例应用(Run)
环境就绪后,进入示例目录直接运行:
$ go run ./程序入口在 main.go:它通过 db.go 的setupDB(":5432", "sample")直连宿主机 5432 端口的数据库(此时尚未经过代理),完成Ping后调用process执行两类查询:
db.Model(&users).Select():全量查询 User 表并逐条打印;- 一次查询同时加载 Story 及其关联作者:
db.Model(story).Relation("Author").Where("story.id = ?", 1).Select()。
数据模型定义在 models.go:User(含Emails []string)与Story(Author *User使用pg:"rel:has-one"声明一对一关联)。而 db.go 中的createSchema会为这两个模型创建临时表(CreateTableOptions{Temp: true}),seed则写入两个用户与一篇故事,保证查询有数据可返回。程序把查询到的用户与故事打印到日志,用于观察在无故障(正常)与有故障(注入 Toxic 后)两种情况下的行为差异。
四、运行容错性测试(Test)
测试同样只需一行命令:
$ go test -v . $ go test -v . -run TestMultipleToxicsgo test -v .:以详细模式运行示例包内的全部测试;go test -v . -run <正则>:按测试名正则过滤,只执行匹配的用例。原文档示例中使用了TestMultipleToxics作为演示名;在当前仓库的 db_test.go 中实际包含的两个用例是TestSlowDBConnection与TestOutageResetPeer,因此你可以用go test -v . -run TestSlowDBConnection只跑延迟注入用例,用-run TestOutageResetPeer只跑断连用例。
运行测试时无需额外启动 Toxiproxy 进程——测试代码会在init()阶段自动完成 Toxiproxy Server 的启动、代理的创建与故障的注入(见下一节)。
五、测试代码剖析:Go 测试中如何内嵌 Toxiproxy
这是整个示例最值得学习的部分:db_test.go 展示了"不启动独立服务、在测试进程内一键拉起 Toxiproxy"的完整套路,分为三个层次。
1. 启动 Toxiproxy Server(幂等探测)
func runToxiproxyServer() { timeout := 5 * time.Second // Check if there is instance run conn, err := net.DialTimeout("tcp", "localhost:8474", timeout) if err == nil { conn.Close() return } go func() { metricsContainer := toxiServer.NewMetricsContainer(prometheus.NewRegistry()) server := toxiServer.NewServer(metricsContainer, zerolog.Nop()) server.Listen("localhost:8474") }() // 最多重试 10 次等待端口就绪 for i := 0; i < 10; i += 1 { ... } }要点:
- 先探测端口 8474:如果宿主机上已运行独立的 Toxiproxy(例如通过
brew安装的服务),则直接复用,避免端口冲突; - 否则内嵌启动:通过
toxiServer.NewServer(metricsContainer, zerolog.Nop())在测试进程内以 goroutine 方式运行 Toxiproxy Server,NewMetricsContainer(prometheus.NewRegistry())挂载 Prometheus 指标,zerolog.Nop()关闭日志输出保持测试输出干净; - 启动后循环重试
DialTimeout直到端口可连,保证后续操作不会因服务未就绪而失败。
2. 创建代理(Populate)
toxi = toxiproxy.NewClient("localhost:8474") _, err = toxi.Populate([]toxiproxy.Proxy{{ Name: "postgresql", Listen: "localhost:35432", Upstream: "localhost:5432", Enabled: true, }}) proxies, err = toxi.Proxies()toxi.Populate(...)通过 HTTP API 一次性批量创建代理。代理postgresql监听localhost:35432,把流量转发到上游localhost:5432——也就是说,测试数据库的连接地址是 35432,而不是直连 5432;- 随后
toxi.Proxies()拉取代理列表并以 map 形式保存,供测试用例按名称proxies["postgresql"]快速引用; Proxy结构体的四个核心字段(Name、Listen、Upstream、Enabled)定义在 client/proxy.go,ActiveToxics字段在Populate时不可设置。
测试中数据库的连接函数也与此对应:
db, err = setupDB(":35432", "sample_test") // 测试连接走代理 func connectDB(addr string) *pg.DB { return pg.Connect(&pg.Options{ Addr: addr, User: "postgres", Database: "sample_test", }) }3. 注入故障:两个代表性测试用例
TestSlowDBConnection:注入 10 秒延迟,验证应用不报错
// Add 1s latency to 100% of downstream connections proxies["postgresql"].AddToxic("latency_down", "latency", "downstream", 1.0, toxiproxy.Attributes{ "latency": 10000, }) defer proxies["postgresql"].RemoveToxic("latency_down") err := process(db) if err != nil { t.Fatalf("got error %v, wanted no errors", err) }这段代码演示了客户端 API 的标准用法:AddToxic(name, type, stream, toxicity, attributes)。参数含义(见 client/proxy.go):
| 参数 | 示例值 | 说明 |
|---|---|---|
name | "latency_down" | Toxic 名称,省略时默认"<type>_<stream>" |
type | "latency" | Toxic 类型,在 toxics/latency.go 中注册 |
stream | "downstream" | 作用方向,省略默认downstream(客户端→上游) |
toxicity | 1.0 | 故障生效概率 0~1,传-1会归一化为1 |
attributes | {"latency": 10000} | 该 Toxic 的具体参数,单位毫秒 |
注意latency: 10000是10 秒延迟(注释中 "1s" 与代码值略有出入,以代码值为准)。延迟 Toxic 的实现细节在 toxics/latency.go:Latency与Jitter(抖动,单位为毫秒)两个字段,实际延迟为latency ± jitter(delay += rand.Int63n(jitter*2) - jitter),且延迟会扣除数据包已等待的时间,力求精确。挂载该 Toxic 后,每次数据库查询都会被拖慢 10 秒,但由于 TCP 连接并未断开,process(db)最终仍能成功完成,测试断言"不应报错"——这正是容错性测试的意义:慢,但不挂。
TestOutageResetPeer:注入 TCP 重置,验证应用感知故障
// Add broken TCP connection proxies["postgresql"].AddToxic("reset_peer_down", "reset_peer", "downstream", 1.0, toxiproxy.Attributes{ "timeout": 10, }) defer proxies["postgresql"].RemoveToxic("reset_peer_down") err := process(db) if err == nil { t.Fatalf("expect error") }reset_peer的实现见 toxics/reset_peer.go:它在收到数据后等待timeout(毫秒,这里为 10ms),然后调用stub.Close()。注释揭示了底层机制——关闭时对 socket 执行SetLinger(0),即丢弃未发送/未确认的数据并直接发送 TCP RST,而不是优雅的 FIN/ACK 关闭。因此应用侧会立即收到连接重置错误,process(db)返回错误,测试断言"必须报错"。
两个用例合在一起,恰好覆盖了容错性测试的一正一反两种断言:延迟注入不应破坏功能,断连注入必须暴露错误。这正是混沌测试的核心验证逻辑。
4. 故障的清理
两个用例都使用defer proxies["postgresql"].RemoveToxic(...)在测试结束后移除 Toxic。RemoveToxic对应 HTTP API 的DELETE /proxies/{name}/toxics/{toxicName}(见 client/proxy.go),确保每个用例从"干净连接"开始,互不污染。
六、Toxics 的工作原理:toxicity 与管道模型
理解了测试代码,再看底层机制。Toxiproxy 的 Toxic 是一个可链式串联的管道过滤器,其接口定义在 toxics/toxic.go:
Toxic v Client <-> ToxicStub <-> Upstream- Toxic 接口:只需实现
Pipe(*ToxicStub),该方法阻塞运行直到连接关闭或被中断; - ToxicStub:持有
Input/Output通道、Interrupt中断信号与State(供有状态 Toxic 使用),是每个连接的"专属工作台"; - Toxicity(生效概率):
Run()中生成rand.Float32(),当随机值小于toxicity时才执行真正的 Toxic,否则以NoopToxic(直通)代替。这就是为什么toxicity: 1.0表示"100% 故障",而0.5表示"一半连接受影响"——可用于模拟部分故障、灰度故障等更真实的场景; - 注册机制:每种 Toxic 通过
Register("typeName", toxic)注册到全局ToxicRegistry,服务器端按名称实例化(reflect.New),并可查询BufferedToxic(如 latency 的缓冲区为 1024 字节)以设置管道缓冲。
除本文用到的latency与reset_peer外,仓库还内置了其他常见故障类型,例如 toxics/bandwidth.go 的bandwidth(以 KB/s 限制速率,低速率时会将大包按 100ms 间隔切分发送)、timeout、slow_close、slicer、limit_data等,完整清单可参考 README.md 的 Toxics 章节。它们共享同一套AddToxicAPI,只需更换type与attributes即可注入不同故障,改造成本极低。
七、把示例迁移到你的项目:落地建议
- 代理即依赖:让应用连接 Toxiproxy 的监听端口(如
35432),而不是直连上游(5432)。在测试中通过Populate创建代理并保持Enabled: true,应用代码零改动。 - 复用内嵌启动模式:复制 db_test.go 中
runToxiproxyServer的"先探测、再内嵌"逻辑,可让测试同时兼容"已有独立 Toxiproxy 服务"与"纯本地测试"两种环境。 - 用
-run精准选择用例:go test -v . -run TestSlowDBConnection只跑延迟用例,-run TestOutageResetPeer只跑断连用例;配合-count=1可避免测试缓存。 - 一正一反的断言模式:可恢复类故障(延迟、限速)断言"不报错",破坏性故障(断连、重置)断言"报错",两者结合才能覆盖完整的行为边界。
- 善用
defer清理:每个用例末尾用RemoveToxic复原代理,保持用例独立性;需要并发模拟时再考虑同时挂载多个 Toxic。
八、小结
本示例虽然规模不大,却完整展示了 Toxiproxy 在真实数据库场景下的标准工作流:kind 提供基础设施、Toxiproxy 提供故障注入、Go 测试提供自动化断言。通过 cluster.yml 与 resources.yml 完成环境搭建,通过 db_test.go 掌握内嵌 Server、创建代理、注入 latency/reset_peer 故障的完整代码模式,你就可以把同样的方法扩展到 Redis、MySQL、消息队列等任何 TCP 服务,构建属于自己的混沌测试矩阵。
- 测试
- 网络
【免费下载链接】toxiproxy
:alarm_clock: :fire: A TCP proxy to simulate network and system conditions for chaos and resiliency testing
相关推荐
深度解析AgentScope多智能体框架的令牌计数与成本控制架构设计
深度解析AgentScope多智能体框架的令牌计数与成本控制架构设计 在多智能体系统开发中,精确的令牌计数与成本控制是企业级应用落地的关键技术挑战。AgentS
人工智能大模型AI Agent多智能体Agent 框架Agent 编排工具调用RAGMCP 服务语音SOAR 测试数据库搭建指南:基于 sakila 与 world_x 构建 init.sql 测试环境
SOAR 测试数据库搭建指南:基于 sakila 与 world_x 构建 init.sql 测试环境 本指南以 test/sql/README.md http
后端数据库开发工具为什么选择SPlayer Legacy?探索这款经典播放器的独特优势
为什么选择SPlayer Legacy?探索这款经典播放器的独特优势 SPlayer Legacy(射手影音)是一款经典的免费开源多媒体播放器,虽然项目已不再维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考