news 2026/10/7 15:31:58

Toxiproxy 数据库容错性测试实战:基于 kind + PostgreSQL 搭建混沌测试环境

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Toxiproxy 数据库容错性测试实战:基于 kind + PostgreSQL 搭建混沌测试环境
  • 测试
  • 网络

【免费下载链接】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 是一款用于模拟网络与系统故障的 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 kind
  • toxiproxy:由 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: TCP
  • ipFamily: 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: postgres
  • image: 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执行两类查询:

  1. db.Model(&users).Select():全量查询 User 表并逐条打印;
  2. 一次查询同时加载 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 TestMultipleToxics
  • go 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(客户端→上游)
toxicity1.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即可注入不同故障,改造成本极低。

七、把示例迁移到你的项目:落地建议

  1. 代理即依赖:让应用连接 Toxiproxy 的监听端口(如35432),而不是直连上游(5432)。在测试中通过Populate创建代理并保持Enabled: true,应用代码零改动。
  2. 复用内嵌启动模式:复制 db_test.go 中runToxiproxyServer的"先探测、再内嵌"逻辑,可让测试同时兼容"已有独立 Toxiproxy 服务"与"纯本地测试"两种环境。
  3. 用-run精准选择用例:go test -v . -run TestSlowDBConnection只跑延迟用例,-run TestOutageResetPeer只跑断连用例;配合-count=1可避免测试缓存。
  4. 一正一反的断言模式:可恢复类故障(延迟、限速)断言"不报错",破坏性故障(断连、重置)断言"报错",两者结合才能覆盖完整的行为边界。
  5. 善用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

项目地址:https://gitcode.com/gh_mirrors/to/toxiproxy
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 15:30:58

西源江河颂

遥溯江源雪岳雄&#xff0c;灵豹巡岩倚昊空。 来瞻金城波澹荡&#xff0c;沧流润彻皋兰崇。 滩头稚子嬉沙渚&#xff0c;筏上游人趁晚风。 画舫凌澜观浩渺&#xff0c;共揽烟波意自融。 奔流历涧携沙逝&#xff0c;远泻铺畴厚壤隆。 江河共铸神州野&#xff0c;沃壤丰穰四海雍。…

作者头像 李华
网站建设 2026/10/7 15:29:38

【专知智库】让数据变成零件,让零件变成资产,让资产驱动增长

让数据变成零件&#xff0c;让零件变成资产&#xff0c;让资产驱动增长专知智库&#xff0c;上市公司研发价值增长操作系统一、数据&#xff0c;为什么没有变成资产&#xff1f;每家公司都有大量数据。研发数据、专利数据、软著数据、合规数据、实验记录、技术方案、人员工时、…

作者头像 李华
网站建设 2026/10/7 15:29:10

挖到一款AI漫剧生成新项目|个人实测体验,一起来玩

最近一直在体验市面上各类AI视频漫剧工具&#xff0c;偶然拿到了新项目的小规模内测机会。 ⏰内测时间&#xff1a;10月13日‑10月19日 项目暂未对外公开&#xff0c;进入平台需要邀请码。 &#x1f449;扫码进群即可领取内测邀请码&#xff1a;扫码进群即可获得邀请码 http…

作者头像 李华
网站建设 2026/10/7 15:28:47

Caveman式极简编码代理:proxy、endpoint与token管理实战

1. 从“caveman”说起&#xff1a;一个被低估的编码代理思路第一次看到“caveman”这个词被拿来命名一个跟 coding agents 相关的东西&#xff0c;我脑子里蹦出来的画面是《疯狂原始人》里那种抡着骨头棒子、不管三七二十一先砸下去再说的场景。后来仔细琢磨了一下这个命名背后…

作者头像 李华
网站建设 2026/10/7 15:28:42

Text2SQL 训练数据合成:利用大模型批量构建黄金问答对与 SQL 校验

Text2SQL 训练数据合成&#xff1a;利用大模型批量构建黄金问答对与 SQL 校验上个季度为了优化公司内部垂直领域的 Text2SQL 表现&#xff0c;团队采购了一批开源基础模型打算进行微调&#xff08;SFT&#xff09;。训练集构建的任务分发到业务线后&#xff0c;动员了六位数据分…

作者头像 李华