☰
自定义 Toxiproxy Toxic 实战指南:从 DebugToxic 到 HTTP 响应注入
2026/10/7 20:04:29 网站建设 项目流程
  • 测试
  • 网络

【免费下载链接】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 代理,其核心扩展点在于Toxic(毒药)——一段附着在代理链路上、可以改写数据流向与内容的小插件。本指南以仓库 _examples/toxics 中的两个完整示例为主线,带你从零实现一个自定义 Toxic:先用DebugToxic把经过代理的字节流以十六进制打印出来,再通过HttpToxic拦截并改写 HTTP 响应头。读完本文,你将掌握Toxic接口、ToxicStub管道模型、stream包的读写抽象以及自定义服务端组装方式,能够直接在自己的项目中编写、注册并验证新的 Toxic。

一、示例概览:为什么需要自定义 Toxic

Toxiproxy 内置了latency、bandwidth、timeout、reset_peer、limit_data等常用故障注入 Toxic(见 toxics 目录),但真实场景中的故障形态往往是业务特有的:可能需要在特定协议字段上做篡改、按业务规则丢弃数据、或者把原始流量打印出来用于调试。此时就需要自定义 Toxic。

仓库在 _examples/toxics 中提供了两个可直接go run的示例:

  • debug_toxic.go:实现debugToxic,将经过代理的数据以十六进制(hex)格式打印到日志;
  • http_toxic.go:实现httpToxic,解析经过代理的 HTTP 响应并改写其中的响应头。

两者都是独立可运行的完整程序——它们不依赖官方toxiproxy-server二进制,而是自己组装一个 Toxiproxy 服务端(监听0.0.0.0:8484),再通过toxiproxy-cli把自定义 Toxic 挂到代理上。这正体现了 CREATING_TOXICS.md 所说的设计哲学:用toxics.Register注册自定义类型,拷贝 cmd/server/server.go 组装自己的二进制,即可在不 fork 整个项目的前提下扩展 Toxic。

二、自定义 Toxic 的核心接口与运行机制

在深入两个示例之前,先理解自定义 Toxic 的运行模型。定义在 toxics/toxic.go 中的核心接口只有一个:

type Toxic interface { // Defines how packets flow through a ToxicStub. // Pipe() blocks until the link is closed or interrupted. Pipe(*ToxicStub) }

Pipe()函数定义了数据如何流经 Toxic,它阻塞运行直到链路关闭或被中断。传入的ToxicStub结构体(同样定义于 toxics/toxic.go)承载了该 Toxic 在每条连接上的全部上下文:

type ToxicStub struct { Input <-chan *stream.StreamChunk // 输入通道 Output chan<- *stream.StreamChunk // 输出通道 State interface{} // 每条连接独立的有状态数据 Interrupt chan struct{} // 中断通道,用于暂停/替换 Toxic }

Toxic 以**管道(pipeline)**方式串联:Client <-> ToxicStub <-> Upstream,多个 Toxic 可以通过 channel 链在一起。通道里流动的不是裸字节,而是stream.StreamChunk(定义见 stream/io_chan.go):

type StreamChunk struct { Data []byte Timestamp time.Time // 代理从客户端/服务端收到该数据的时刻 }

带上时间戳的原因在于:latency这类 Toxic 需要知道一段数据已在代理中等待了多久。你可以把StreamChunk理解为「网络包」级别的抽象。

注册自定义 Toxic 则通过全局注册表完成(toxics/toxic.go 中的Register/New/Count),内置 Toxic 也在各自文件的init()中注册,例如 toxics/noop.go 里的Register("noop", new(NoopToxic))。自定义 Toxic 的注册方式完全一致(见下文示例的main())。

与官方服务端的差异

官方toxiproxy-server(cmd/server/server.go)默认把 HTTP API 监听在localhost:8474,并且只包含内置 Toxic。示例程序则使用server.Listen("0.0.0.0:8484")启动 API,因此后续toxiproxy-cli命令都需要通过--host "http://localhost:8484"显式指定端点,这正是原文档中每条 CLI 命令都带--host的原因。

三、DebugToxic:把经过代理的字节流打印成十六进制

3.1 完整复现步骤

按 _examples/toxics/README.md 的流程,依次执行:

第一步,运行自定义 toxiproxy 服务端(内含 DebugToxic):

$ go run debug_toxic.go

第二步,在另一个终端启动一个真实的 Redis 服务作为上游:

$ redis-server

第三步,通过 CLI 创建代理并挂上debugToxic:

$ toxiproxy-cli --host "http://localhost:8484" create -l :16379 -u localhost:6379 redis $ toxiproxy-cli --host "http://localhost:8484" toxic add --type debug redis

这里create的三个参数含义分别是:-l :16379为代理监听端口,-u localhost:6379为上游地址,redis为代理名。

第四步,通过代理访问 Redis,触发流量:

$ redis-cli -p 16379 keys '*'

(等价写法redis-cli -p 16379 "keys" "*"亦可。)此时回到运行go run debug_toxic.go的终端,自定义 Toxiproxy 会把keys命令的请求字节和响应字节以十六进制格式打印出来。

3.2 源码级原理剖析

完整的DebugToxic实现在 _examples/toxics/debug_toxic.go,其核心是Pipe()中的主循环:

func (t *DebugToxic) Pipe(stub *toxics.ToxicStub) { buf := make([]byte, 32*1024) writer := stream.NewChanWriter(stub.Output) reader := stream.NewChanReader(stub.Input) reader.SetInterrupt(stub.Interrupt) for { n, err := reader.Read(buf) log.Printf("-- [DebugToxic] Processed %d bytes\n", n) if err == stream.ErrInterrupted { writer.Write(buf[:n]) return } else if err == io.EOF { stub.Close() return } t.PrintHex(buf[:n]) writer.Write(buf[:n]) } }

这里用到的是 stream/io_chan.go 提供的两个适配器:

  • stream.NewChanReader(stub.Input)把<-chan *StreamChunk包装成标准io.Reader;
  • stream.NewChanWriter(stub.Output)把chan<- *StreamChunk包装成io.WriteCloser;
  • reader.SetInterrupt(stub.Interrupt)让阻塞中的Read可以被中断通道唤醒。

循环的三种退出/分支情况对应着 Toxic 必须遵守的中断协议(这也是 CREATING_TOXICS.md 反复强调的要点):

  1. stream.ErrInterrupted:Toxic 被 API 更新或移除时,InterruptToxic()(见 toxics/toxic.go)会向Interrupt通道发送信号。此时 Toxic 应把尚未写出的"在途数据"(buf[:n])写完再返回,绝不能丢弃任何已从Input读出的字节,否则流会缺字节而损坏;
  2. io.EOF:上游关闭,代表数据流结束,应调用stub.Close()关闭输出通道并返回;
  3. 正常路径:打印十六进制后原样writer.Write(buf[:n])转发,实现"只看不碰"的透明调试。

十六进制打印本身由PrintHex完成(_examples/toxics/debug_toxic.go):按每行 4 组、每组 8 字节的排版输出% x格式。注意其边界处理——当剩余字节不足 8 时按实际长度截断,避免越界打印。

关于 Toxic 被中断时的行为,可以对照 toxics/latency.go 中LatencyToxic的做法:它在select中同时监听stub.Interrupt和time.After,收到中断时同样先把数据stub.Output <- c再返回,即"不把任何数据丢在地上"是自定义 Toxic 的一条黄金准则。

四、HttpToxic:拦截并改写 HTTP 响应头

4.1 完整复现步骤

第一步,运行自定义 toxiproxy 服务端(内含 HttpToxic):

$ go run http_toxic.go

第二步,创建代理并挂上httpToxic,上游指向example.com:80:

$ toxiproxy-cli --host "http://localhost:8484" create -l :18080 -u example.com:80 example $ toxiproxy-cli --host "http://localhost:8484" toxic add --type http example

第三步,通过代理发起请求并观察响应:

$ curl -v localhost:18080/hello

预期输出中会出现关键两行:

... < HTTP/1.1 404 Not Found < Location: https://github.com/Shopify/toxiproxy

即请求经代理转发到example.com后,返回了 404 状态码;而Location响应头已被自定义 Toxic 改写为 Toxiproxy 项目地址——这正是 HttpToxic 注入生效的直接证据。

4.2 源码级原理剖析

完整实现见 _examples/toxics/http_toxic.go。与 DebugToxic 不同,HttpToxic 需要把字节流按 HTTP 协议解析再改写,因此借助了 Go 标准库的net/http:

func (t *HttpToxic) Pipe(stub *toxics.ToxicStub) { buffer := bytes.NewBuffer(make([]byte, 0, 32*1024)) writer := stream.NewChanWriter(stub.Output) reader := stream.NewChanReader(stub.Input) reader.SetInterrupt(stub.Interrupt) for { tee := io.TeeReader(reader, buffer) resp, err := http.ReadResponse(bufio.NewReader(tee), nil) if err == stream.ErrInterrupted { buffer.WriteTo(writer) return } else if err == io.EOF { stub.Close() return } if err != nil { buffer.WriteTo(writer) } else { t.ModifyResponse(resp) resp.Write(writer) } buffer.Reset() } }

实现的关键技巧:

  1. io.TeeReader旁路缓存:TeeReader(reader, buffer)在读取的同时把所有字节镜像写入buffer。一旦http.ReadResponse解析失败(例如上游返回的并不是完整 HTTP 响应、或中途被中断),就把buffer里已缓存的原始字节原样WriteTo(writer),保证数据不丢;
  2. http.ReadResponse流式解析:从bufio.NewReader(tee)中读出一个完整*http.Response对象。注意这里的 reader 是io.Reader接口(由 ChanReader 实现),因此天然兼容标准库的解析逻辑——这体现了 stream 包把 channel 抽象成io.Reader/io.Writer的设计价值:任何能操作io.Reader的 Go 代码都能直接处理流经 Toxic 的数据;
  3. 改写与回写:解析成功则调用自定义的ModifyResponse:
func (t *HttpToxic) ModifyResponse(resp *http.Response) { resp.Header.Set("Location", "https://github.com/Shopify/toxiproxy") }

随后resp.Write(writer)把(被改写的)HTTP 响应重新序列化并写回输出通道,buffer.Reset()清空旁路缓存进入下一轮循环。

值得说明的是:HttpToxic 只在一个方向上解析 HTTP(响应方向),且依赖上游以标准 HTTP/1.x 文本格式返回。若你的代理场景需要双向解析或 HTTP/2,需要在此基础上扩展,但这并不影响它作为"在 TCP 流上做协议级篡改"的范本价值——CREATING_TOXICS.md 明确指出该示例正是"使用 stream 包配合 Go http 包"的完整范例。

五、让自定义 Toxic 更专业:配置、缓冲与状态

两个示例足以让你跑通自定义 Toxic 全流程;若要写出可复用的生产级 Toxic,CREATING_TOXICS.md 还给出了三个进阶机制,其实现均可对照仓库源码:

5.1 通过 JSON 字段暴露配置

Toxic 结构体中的公开字段会被 API 自动 JSON 编解码,因此天然成为 CLI/HTTP 可配置的参数。参照 toxics/latency.go:

type LatencyToxic struct { // Times in milliseconds Latency int64 `json:"latency"` Jitter int64 `json:"jitter"` }

这样toxiproxy-cli toxic add --type latency --attribute latency=1000即可在运行时注入参数。注意:结构体字段不应在Pipe()中被写入——每个连接都有独立的 Toxic 实例,且 API 更新时会替换实例;需要跨连接保存状态时应使用下面的 StatefulToxic,或退而求其次使用 Pipe 顶部的局部变量(局部变量在中断期间也无法持久化,但至少不会跨连接串扰)。

5.2 用 BufferedToxic 避免阻塞整个管道

默认情况下 Toxic 无缓冲:stub.Output的写入会阻塞,直到对端或其他 Toxic 取走数据。由于 Toxic 是链式串联的,不读Input会连带阻塞其他 Toxic 与上游写入。若希望 Toxic 具备缓冲,实现BufferedToxic接口即可(见 toxics/toxic.go 与 toxics/latency.go):

func (t *LatencyToxic) GetBufferSize() int { return 1024 }

返回值的单位是StreamChunk个数,而单个 chunk 通常为 1 字节到 32KB 不等,规划内存时要心中有数。toxics.New()(toxics/toxic.go)会在创建 Toxic 时读取该值并写入ToxicWrapper.BufferSize。

5.3 用 StatefulToxic 保存每条连接的状态

如果 Toxic 需要按连接累积数据(例如统计已传输字节数),实现StatefulToxic接口的NewState()即可(见 toxics/toxic.go)。参照 toxics/limit_data.go:

func (t *LimitDataToxic) NewState() interface{} { return new(LimitDataToxicState) }

状态对象会存放在ToxicStub.State上,在Pipe()中取出使用:

state := stub.State.(*LimitDataToxicState)

LimitDataToxic正是这样一边转发一边累计state.bytesTransmitted,达到Bytes限额后stub.Close()截断连接的。

5.4 中断协议自查清单

无论实现哪种进阶特性,编写Pipe()时都应自查(依据 CREATING_TOXICS.md 与 toxics/toxic.go 的InterruptToxic):

  • 所有阻塞操作(含sleep)是否都可被stub.Interrupt中断?
  • 收到中断后是否把在途数据全部写回stub.Output再返回?
  • 读到nilchunk /io.EOF时是否调用stub.Close()并返回?
  • 是否有数据被吞掉导致流损坏?

满足这些约束,自定义 Toxic 才能与 Toxiproxy 的"更新/移除 Toxic 时安全切换"机制(InterruptToxic会等待运行中的 Toxic 退出,见 toxics/toxic.go)正确配合。

六、从示例走向你自己的二进制

两个示例的main()展示了组装自定义服务端的完整模板:

func main() { toxics.Register("debug", new(DebugToxic)) logger := zerolog.New(os.Stderr).With().Caller().Timestamp().Logger() metrics := toxiproxy.NewMetricsContainer(prometheus.NewRegistry()) server := toxiproxy.NewServer(metrics, logger) server.Listen("0.0.0.0:8484") }

要点拆解:

  • toxics.Register("debug", new(DebugToxic)):把类型名debug与 Toxic 工厂注册进全局注册表。内置 Toxic 各自在init()中注册(如 toxics/limit_data.go 的Register("limit_data", ...)),而示例选择在main()里注册,效果等价;
  • toxiproxy.NewServer(metrics, logger):创建 API 服务器(HTTP 端点实现在 api.go,包含/proxies、/proxies/{proxy}/toxics等完整 REST 路由),之后toxiproxy-cli的所有命令都是对这些端点的封装;
  • server.Listen("0.0.0.0:8484"):监听地址;相比官方默认端口8474(见 cmd/server/server.go 的-port参数),示例固定使用8484,因此 CLI 需要--host "http://localhost:8484"。

要把自定义 Toxic 投入实际使用,CREATING_TOXICS.md 建议的做法是:拷贝 cmd/server/server.go 到自己的项目,保留其-host、-port、-config、-seed、-runtime-metrics、-proxy-metrics等启动参数,在main()中注册自己的 Toxic 后编译成独立二进制。这样你既拥有完整的 Toxiproxy 能力(代理管理、内置 Toxic、配置热加载、Prometheus 指标),又能注入业务专属的故障类型,无需 fork 整个仓库。若你编写的 Toxic 具有通用价值,还可以按 CREATING_TOXICS.md 的建议通过 Pull Request 回馈社区。

结语

从DebugToxic的"透明旁观"到HttpToxic的"协议级篡改",本文完整还原了 _examples/toxics/README.md 的两条实操链路,并下沉到Toxic接口、ToxicStub中断协议、StreamChunk时间戳、ChanReader/ChanWriter的io.Reader抽象,以及BufferedToxic、StatefulToxic等进阶机制。掌握这些,你便能在混沌测试中随心所欲地制造"专属故障"——无论是打印 Redis 协议流量用于排障,还是篡改 HTTP 响应验证客户端容错,自定义 Toxic 都是把 Toxiproxy 从"拿来即用的工具箱"升级为"可编程的混沌平台"的关键一步。

  • 测试
  • 网络

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询