通过Toxiproxy从原理到实践理解混沌工程

凌晨三点,告警群被打爆。订单系统响应时间从 30ms 飙升到 30 秒,数据库连接池告警,下游支付服务超时雪崩。事后复盘发现根因平淡得令人无奈:云厂商某可用区网络抖动 200ms 。这个数字在开发环境从未出现过,开发者的笔记本到本地数据库通常 < 1ms,CI 流水线的容器之间通常 < 5ms。我们写代码时假设的"网络是可靠的",这是分布式计算 8 大谬误之首。

  1. The Network is Reliable.(网络是可靠的)
  2. Latency is Zero.(延迟为零)
  3. Bandwidth is Infinite.(带宽无限)
  4. The Network is Secure.(网络是安全的)
  5. Topology Doesn't Change.(拓扑不变)
  6. There is One Administrator.(只有一个管理员)
  7. Transport Cost is Zero.(传输成本为零)
  8. The Network is Homogeneous.(网络是同构的)

--- L. Peter Deutsch & James Gosling, Sun Microsystems, 1994

Toxiproxy 的存在意义,就是让这些"谬误"在你的开发笔记本上提前发生,对系统进行混沌工程的实践。Netflix《Principles of Chaos Engineering》定义了什么是混动工程:

Chaos Engineering is the discipline of experimenting on a system in order to build confidence in the system's capability to withstand turbulent conditions in production.

注意几个关键词:

  • experimenting(实验)--- 不是测试,是科学实验,要有假设和验证
  • build confidence(建立信心)--- 目的不是发现 bug,而是产生可信度
  • turbulent conditions(动荡条件)--- 接受混乱无法消除,只能验证容忍

混沌工程的终极目标不是构建"鲁棒"系统(能扛住故障),而是构建反脆弱系统每次故障都让系统变得更强(被识别的弱点 → 自动化测试 → 永久免疫)。

在混乱不可避免的世界,目标不是预测危机,而是构建能从危机中获益的系统。--- Nassim Nicholas Taleb

Toxiproxy 内部源码原理

理解工具的内部机制是用好工具的前提。Toxiproxy 用 Go 编写,源码精炼,值得深入。整体架构如下:
#mermaid-svg-4bM5DyW6LQaVnO73{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4bM5DyW6LQaVnO73 .error-icon{fill:#552222;}#mermaid-svg-4bM5DyW6LQaVnO73 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4bM5DyW6LQaVnO73 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4bM5DyW6LQaVnO73 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4bM5DyW6LQaVnO73 .marker.cross{stroke:#333333;}#mermaid-svg-4bM5DyW6LQaVnO73 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4bM5DyW6LQaVnO73 p{margin:0;}#mermaid-svg-4bM5DyW6LQaVnO73 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 .cluster-label text{fill:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 .cluster-label span{color:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 .cluster-label span p{background-color:transparent;}#mermaid-svg-4bM5DyW6LQaVnO73 .label text,#mermaid-svg-4bM5DyW6LQaVnO73 span{fill:#333;color:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 .node rect,#mermaid-svg-4bM5DyW6LQaVnO73 .node circle,#mermaid-svg-4bM5DyW6LQaVnO73 .node ellipse,#mermaid-svg-4bM5DyW6LQaVnO73 .node polygon,#mermaid-svg-4bM5DyW6LQaVnO73 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4bM5DyW6LQaVnO73 .rough-node .label text,#mermaid-svg-4bM5DyW6LQaVnO73 .node .label text,#mermaid-svg-4bM5DyW6LQaVnO73 .image-shape .label,#mermaid-svg-4bM5DyW6LQaVnO73 .icon-shape .label{text-anchor:middle;}#mermaid-svg-4bM5DyW6LQaVnO73 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4bM5DyW6LQaVnO73 .rough-node .label,#mermaid-svg-4bM5DyW6LQaVnO73 .node .label,#mermaid-svg-4bM5DyW6LQaVnO73 .image-shape .label,#mermaid-svg-4bM5DyW6LQaVnO73 .icon-shape .label{text-align:center;}#mermaid-svg-4bM5DyW6LQaVnO73 .node.clickable{cursor:pointer;}#mermaid-svg-4bM5DyW6LQaVnO73 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4bM5DyW6LQaVnO73 .arrowheadPath{fill:#333333;}#mermaid-svg-4bM5DyW6LQaVnO73 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4bM5DyW6LQaVnO73 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4bM5DyW6LQaVnO73 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4bM5DyW6LQaVnO73 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4bM5DyW6LQaVnO73 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4bM5DyW6LQaVnO73 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4bM5DyW6LQaVnO73 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4bM5DyW6LQaVnO73 .cluster text{fill:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 .cluster span{color:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4bM5DyW6LQaVnO73 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4bM5DyW6LQaVnO73 rect.text{fill:none;stroke-width:0;}#mermaid-svg-4bM5DyW6LQaVnO73 .icon-shape,#mermaid-svg-4bM5DyW6LQaVnO73 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4bM5DyW6LQaVnO73 .icon-shape p,#mermaid-svg-4bM5DyW6LQaVnO73 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4bM5DyW6LQaVnO73 .icon-shape .label rect,#mermaid-svg-4bM5DyW6LQaVnO73 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4bM5DyW6LQaVnO73 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4bM5DyW6LQaVnO73 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4bM5DyW6LQaVnO73 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Toxiproxy 进程
响应方向 (Downstream Stream)
请求方向 (Upstream Stream)
① 请求 TCP
② 请求转发
③ 响应 TCP
④ 响应回送
动态注入/修改
客户端App
上游服务
Listener

监听端口
Proxy 实例
ToxicCollection
Upstream Link
Toxic 链

latency → bandwidth → ...
Downstream Link
Toxic 链

latency → slicer → ...
HTTP API :8474

Proxy 核心数据结构源自 proxy.go

  • sync.Mutex 保证多 goroutine 安全访问
  • tomb.Tomb 来自 Canonical 的库,管理 goroutine 生命周期
  • ToxicCollection 是 toxic 的容器,支持运行时增删改
go 复制代码
type Proxy struct {
    sync.Mutex
    Name     string `json:"name"`
    Listen   string `json:"listen"`
    Upstream string `json:"upstream"`
    Enabled  bool   `json:"enabled"`
    listener net.Listener
    started  chan error
    tomb     tomb.Tomb              // 优雅生命周期管理
    connections ConnectionList       // 活跃连接列表
    Toxics   *ToxicCollection `json:"-"`  // toxic 集合
    apiServer *ApiServer
    Logger    *zerolog.Logger
}

每当客户端建立 TCP 连接,Toxiproxy 创建两个独立的 Link这就是为什么注入故障时要指定 stream

  • stream: upstream --- 影响请求方向(客户端发送时延迟/丢包)
  • stream: downstream --- 影响响应方向(服务器响应时延迟/丢包),通常注入响应方向更接近"服务慢"的语义。
go 复制代码
// 来自 proxy.go (https://github.com/Shopify/toxiproxy/blob/main/proxy.go#L180-L195)
name := client.RemoteAddr().String()
proxy.connections.Lock()
proxy.connections.list[name+"upstream"] = upstream
proxy.connections.list[name+"downstream"] = client
proxy.connections.Unlock()

proxy.Toxics.StartLink(proxy.apiServer, name+"upstream", client, upstream, stream.Upstream)
proxy.Toxics.StartLink(proxy.apiServer, name+"downstream", upstream, client, stream.Downstream)
graph LR
    subgraph "TCP 连接"
        Client[客户端]
        Server[上游服务]
    end

    subgraph "Toxiproxy 内部"
        UpLink[Upstream Link<br/>req 方向]
        DownLink[Downstream Link<br/>resp 方向]
    end

    Client -->|请求数据| UpLink
    UpLink -->|经过 toxic 处理| Server
    Server -->|响应数据| DownLink
    DownLink -->|经过 toxic 处理| Client

    style UpLink fill:#fcf
    style DownLink fill:#cff

Toxiproxy 不是按字节处理,而是按 StreamChunk 处理:

  • 时间戳这是延迟计算的核心。sleep := t.delay() - time.Since(c.Timestamp)定义了已经在 Toxiproxy 内停留的时间。这保证了端到端的精确延迟,无论 toxic 链有多长。
go 复制代码
// stream/io_chan.go
type StreamChunk struct {
    Data      []byte
    Timestamp time.Time  // ← 数据包进入 Toxiproxy 的时刻
}

每个 toxic 通过 Go channel 串成流水线:
#mermaid-svg-BTgxdpBZe1tqVCZo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BTgxdpBZe1tqVCZo .error-icon{fill:#552222;}#mermaid-svg-BTgxdpBZe1tqVCZo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BTgxdpBZe1tqVCZo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BTgxdpBZe1tqVCZo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BTgxdpBZe1tqVCZo .marker.cross{stroke:#333333;}#mermaid-svg-BTgxdpBZe1tqVCZo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BTgxdpBZe1tqVCZo p{margin:0;}#mermaid-svg-BTgxdpBZe1tqVCZo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo .cluster-label text{fill:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo .cluster-label span{color:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo .cluster-label span p{background-color:transparent;}#mermaid-svg-BTgxdpBZe1tqVCZo .label text,#mermaid-svg-BTgxdpBZe1tqVCZo span{fill:#333;color:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo .node rect,#mermaid-svg-BTgxdpBZe1tqVCZo .node circle,#mermaid-svg-BTgxdpBZe1tqVCZo .node ellipse,#mermaid-svg-BTgxdpBZe1tqVCZo .node polygon,#mermaid-svg-BTgxdpBZe1tqVCZo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BTgxdpBZe1tqVCZo .rough-node .label text,#mermaid-svg-BTgxdpBZe1tqVCZo .node .label text,#mermaid-svg-BTgxdpBZe1tqVCZo .image-shape .label,#mermaid-svg-BTgxdpBZe1tqVCZo .icon-shape .label{text-anchor:middle;}#mermaid-svg-BTgxdpBZe1tqVCZo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BTgxdpBZe1tqVCZo .rough-node .label,#mermaid-svg-BTgxdpBZe1tqVCZo .node .label,#mermaid-svg-BTgxdpBZe1tqVCZo .image-shape .label,#mermaid-svg-BTgxdpBZe1tqVCZo .icon-shape .label{text-align:center;}#mermaid-svg-BTgxdpBZe1tqVCZo .node.clickable{cursor:pointer;}#mermaid-svg-BTgxdpBZe1tqVCZo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BTgxdpBZe1tqVCZo .arrowheadPath{fill:#333333;}#mermaid-svg-BTgxdpBZe1tqVCZo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BTgxdpBZe1tqVCZo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BTgxdpBZe1tqVCZo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BTgxdpBZe1tqVCZo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BTgxdpBZe1tqVCZo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BTgxdpBZe1tqVCZo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BTgxdpBZe1tqVCZo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BTgxdpBZe1tqVCZo .cluster text{fill:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo .cluster span{color:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BTgxdpBZe1tqVCZo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BTgxdpBZe1tqVCZo rect.text{fill:none;stroke-width:0;}#mermaid-svg-BTgxdpBZe1tqVCZo .icon-shape,#mermaid-svg-BTgxdpBZe1tqVCZo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BTgxdpBZe1tqVCZo .icon-shape p,#mermaid-svg-BTgxdpBZe1tqVCZo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BTgxdpBZe1tqVCZo .icon-shape .label rect,#mermaid-svg-BTgxdpBZe1tqVCZo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BTgxdpBZe1tqVCZo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BTgxdpBZe1tqVCZo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BTgxdpBZe1tqVCZo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网络数据

原始字节
StreamChunk
Toxic 1: Latency

等待 1000ms
Toxic 2: Bandwidth

限速 100KB/s
Toxic 3: Slicer

切片传输
输出到下游

Toxic 实现深度解析

Latency Toxic
  1. Buffered channel :实现 GetBufferSize() int { return 1024 },避免延迟限制吞吐量
  2. 可中断 sleepselect { time.After / Interrupt } 让动态修改立即生效
  3. Jitter 实现rand.Int63n(jitter*2) - jitter[-jitter, +jitter] 均匀分布
go 复制代码
// 来自 toxics/latency.go
func (t *LatencyToxic) Pipe(stub *ToxicStub) {
    for {
        select {
        case <-stub.Interrupt:    // 收到中断信号 (toxic被修改)
            return
        case c := <-stub.Input:
            if c == nil {
                stub.Close()
                return
            }
            // 关键:精确延迟计算
            sleep := t.delay() - time.Since(c.Timestamp)
            select {
            case <-time.After(sleep):           // 等待计算出的时间
                c.Timestamp = c.Timestamp.Add(sleep)
                stub.Output <- c                // 转发到下一级
            case <-stub.Interrupt:              // 等待期间收到中断
                stub.Output <- c
                return
            }
        }
    }
}

func (t *LatencyToxic) delay() time.Duration {
    delay := t.Latency
    jitter := t.Jitter
    if jitter > 0 {
        delay += rand.Int63n(jitter*2) - jitter   // ±jitter 范围抖动
    }
    return time.Duration(delay) * time.Millisecond
}
Timeout Toxic
  • timeout=0 ≠ "无超时",而是 "接受连接但永远不响应,也不关闭"
  • 客户端表现:连接保持,但永远收不到任何数据
  • 应用必须有自己的客户端超时(如 requests.get(timeout=10)),否则永远卡住
go 复制代码
// 来自 toxics/timeout.go
func (t *TimeoutToxic) Pipe(stub *ToxicStub) {
    timeout := time.Duration(t.Timeout) * time.Millisecond
    if timeout > 0 {
        for {
            select {
            case <-time.After(timeout):
                stub.Close()      // ← 超时后强制关闭连接
                return
            case <-stub.Interrupt:
                return
            case c := <-stub.Input:
                if c == nil { stub.Close(); return }
                // ⚠️ 关键:数据被静默丢弃 ("Drop the data on the ground")
            }
        }
    } else {
        // timeout=0:永远丢弃数据,永不关闭
        for {
            select {
            case <-stub.Interrupt: return
            case c := <-stub.Input:
                if c == nil { stub.Close(); return }
                // 静默丢弃
            }
        }
    }
}
Bandwidth Toxic

将数据切成 100ms 一片的小包,用 sleep 控制每片之间的间隔,实现近似精确的带宽限制。

go 复制代码
// 来自 toxics/bandwidth.go
func (t *BandwidthToxic) Pipe(stub *ToxicStub) {
    var sleep time.Duration = 0
    for {
        select {
        case <-stub.Interrupt: return
        case p := <-stub.Input:
            if p == nil { stub.Close(); return }

            if t.Rate <= 0 {
                sleep = 0
            } else {
                // 核心公式:传输 N 字节需要的毫秒数 = N / rate(KB/s)
                sleep += time.Duration(len(p.Data)) * time.Millisecond / time.Duration(t.Rate)
            }

            // 大数据包分片:每 100ms 发送 rate*100 字节
            for int64(len(p.Data)) > t.Rate*100 {
                select {
                case <-time.After(100 * time.Millisecond):
                    stub.Output <- &stream.StreamChunk{
                        Data:      p.Data[:t.Rate*100],
                        Timestamp: p.Timestamp,
                    }
                    p.Data = p.Data[t.Rate*100:]
                    sleep -= 100 * time.Millisecond
                case <-stub.Interrupt:
                    stub.WriteOutput(p, 5*time.Second)
                    return
                }
            }

            // 剩余小包发送
            start := time.Now()
            select {
            case <-time.After(sleep):
                sleep -= time.Since(start)   // 时间补偿,提高精度
                stub.Output <- p
            case <-stub.Interrupt:
                stub.WriteOutput(p, 5*time.Second)
                return
            }
        }
    }
}
Slicer Toxic

数据包切片模拟 TCP 分片在不同 MTU 网络中的行为,测试应用是否正确处理"半包"问题(HTTP body 分多个 TCP 包到达)。

go 复制代码
// 来自 toxics/slicer.go
// 递归二分切片算法
func (t *SlicerToxic) chunk(start int, end int) []int {
    if (end-start)-t.AverageSize <= t.SizeVariation {
        return []int{start, end}
    }
    mid := start + (end-start)/2
    if t.SizeVariation > 0 {
        mid += rand.Intn(t.SizeVariation*2) - t.SizeVariation  // 随机化分割点
    }
    left := t.chunk(start, mid)
    right := t.chunk(mid, end)
    return append(left, right...)
}
Limit Data Toxic

实现了 NewState() interface{} 接口,每个连接独立的状态,避免多连接互相干扰。

go 复制代码
// 来自 toxics/limit_data.go
type LimitDataToxicState struct {
    bytesTransmitted int64    // 累计传输字节
}

func (t *LimitDataToxic) Pipe(stub *ToxicStub) {
    state := stub.State.(*LimitDataToxicState)
    bytesRemaining := t.Bytes - state.bytesTransmitted

    for {
        select {
        case <-stub.Interrupt: return
        case c := <-stub.Input:
            if c == nil { stub.Close(); return }

            // 数据截断
            if bytesRemaining < int64(len(c.Data)) {
                c = &stream.StreamChunk{
                    Timestamp: c.Timestamp,
                    Data:      c.Data[0:bytesRemaining],   // ← 只保留剩余配额
                }
            }

            stub.Output <- c
            state.bytesTransmitted += int64(len(c.Data))
            bytesRemaining = t.Bytes - state.bytesTransmitted

            if bytesRemaining <= 0 {
                stub.Close()    // ← 配额用完,关闭连接
                return
            }
        }
    }
}

Toxicity 概率参数

每个 toxic 都支持 toxicity 参数(0.0 - 1.0),表示故障发生的概率:测试间歇性故障,比"100% 故障"或"100% 正常"都更接近真实生产。

bash 复制代码
# 30% 的请求会被延迟 2 秒,70% 正常通过
curl -X POST http://localhost:8474/proxies/payment-proxy/toxics \
  -d '{
    "name":"flaky",
    "type":"latency",
    "toxicity":0.3,
    "attributes":{"latency":2000}
  }'

tenacity重试库

tenacity 是 Python 生态最流行的重试库,由 Julien Danjou 维护,用于给函数添加重试能力。它的前身是 retrying 库,2016 年因原作者维护中断而 fork 重写。核心特性如下

  • 装饰器语法,零侵入业务代码
  • 多种停止条件(次数、总时间、异常类型)
  • 多种等待策略(固定、指数退避、随机)
  • 异常分类(哪些异常重试,哪些不重试)
  • 钩子函数(重试前/后/失败时的回调)

核心三要素如下

py 复制代码
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

@retry(
    stop=stop_after_attempt(3),                            # ① 何时停止
    wait=wait_exponential(multiplier=1, min=1, max=10),    # ② 等多久再试
    retry=retry_if_exception_type((ConnectionError,))      # ③ 什么异常才重试
)
def call_api():
    return requests.get('http://api.example.com')

整体的逻辑图图下
#mermaid-svg-AEptuOSXKyGa5iPj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AEptuOSXKyGa5iPj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AEptuOSXKyGa5iPj .error-icon{fill:#552222;}#mermaid-svg-AEptuOSXKyGa5iPj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AEptuOSXKyGa5iPj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AEptuOSXKyGa5iPj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AEptuOSXKyGa5iPj .marker.cross{stroke:#333333;}#mermaid-svg-AEptuOSXKyGa5iPj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AEptuOSXKyGa5iPj p{margin:0;}#mermaid-svg-AEptuOSXKyGa5iPj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AEptuOSXKyGa5iPj .cluster-label text{fill:#333;}#mermaid-svg-AEptuOSXKyGa5iPj .cluster-label span{color:#333;}#mermaid-svg-AEptuOSXKyGa5iPj .cluster-label span p{background-color:transparent;}#mermaid-svg-AEptuOSXKyGa5iPj .label text,#mermaid-svg-AEptuOSXKyGa5iPj span{fill:#333;color:#333;}#mermaid-svg-AEptuOSXKyGa5iPj .node rect,#mermaid-svg-AEptuOSXKyGa5iPj .node circle,#mermaid-svg-AEptuOSXKyGa5iPj .node ellipse,#mermaid-svg-AEptuOSXKyGa5iPj .node polygon,#mermaid-svg-AEptuOSXKyGa5iPj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AEptuOSXKyGa5iPj .rough-node .label text,#mermaid-svg-AEptuOSXKyGa5iPj .node .label text,#mermaid-svg-AEptuOSXKyGa5iPj .image-shape .label,#mermaid-svg-AEptuOSXKyGa5iPj .icon-shape .label{text-anchor:middle;}#mermaid-svg-AEptuOSXKyGa5iPj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AEptuOSXKyGa5iPj .rough-node .label,#mermaid-svg-AEptuOSXKyGa5iPj .node .label,#mermaid-svg-AEptuOSXKyGa5iPj .image-shape .label,#mermaid-svg-AEptuOSXKyGa5iPj .icon-shape .label{text-align:center;}#mermaid-svg-AEptuOSXKyGa5iPj .node.clickable{cursor:pointer;}#mermaid-svg-AEptuOSXKyGa5iPj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AEptuOSXKyGa5iPj .arrowheadPath{fill:#333333;}#mermaid-svg-AEptuOSXKyGa5iPj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AEptuOSXKyGa5iPj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AEptuOSXKyGa5iPj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AEptuOSXKyGa5iPj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AEptuOSXKyGa5iPj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AEptuOSXKyGa5iPj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AEptuOSXKyGa5iPj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AEptuOSXKyGa5iPj .cluster text{fill:#333;}#mermaid-svg-AEptuOSXKyGa5iPj .cluster span{color:#333;}#mermaid-svg-AEptuOSXKyGa5iPj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AEptuOSXKyGa5iPj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AEptuOSXKyGa5iPj rect.text{fill:none;stroke-width:0;}#mermaid-svg-AEptuOSXKyGa5iPj .icon-shape,#mermaid-svg-AEptuOSXKyGa5iPj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AEptuOSXKyGa5iPj .icon-shape p,#mermaid-svg-AEptuOSXKyGa5iPj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AEptuOSXKyGa5iPj .icon-shape .label rect,#mermaid-svg-AEptuOSXKyGa5iPj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AEptuOSXKyGa5iPj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AEptuOSXKyGa5iPj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AEptuOSXKyGa5iPj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 成功
可重试异常
成功
可重试异常
成功
失败
不可重试异常
函数调用
尝试1
返回结果
等待 1s
尝试2
等待 2s
尝试3
抛 RetryError
直接抛异常

等待策略(wait)

python 复制代码
from tenacity import (
    wait_fixed,            # 固定等待
    wait_random,           # 随机等待
    wait_exponential,      # 指数退避
    wait_random_exponential  # 指数退避 + 抖动 (推荐)
)

# 指数退避:1s, 2s, 4s, 8s, 16s... 上限 60s
@retry(wait=wait_exponential(multiplier=1, max=60))
def call(): pass

# 指数退避 + 抖动:避免蜂群效应
@retry(wait=wait_random_exponential(multiplier=1, max=60))
def call(): pass

为什么需要抖动(jitter)呢?
服务 客户端3 客户端2 客户端1 服务 客户端3 客户端2 客户端1 #mermaid-svg-qphpWwFMbXWeBbCv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qphpWwFMbXWeBbCv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qphpWwFMbXWeBbCv .error-icon{fill:#552222;}#mermaid-svg-qphpWwFMbXWeBbCv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qphpWwFMbXWeBbCv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qphpWwFMbXWeBbCv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qphpWwFMbXWeBbCv .marker.cross{stroke:#333333;}#mermaid-svg-qphpWwFMbXWeBbCv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qphpWwFMbXWeBbCv p{margin:0;}#mermaid-svg-qphpWwFMbXWeBbCv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qphpWwFMbXWeBbCv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-qphpWwFMbXWeBbCv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qphpWwFMbXWeBbCv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-qphpWwFMbXWeBbCv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-qphpWwFMbXWeBbCv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-qphpWwFMbXWeBbCv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-qphpWwFMbXWeBbCv .sequenceNumber{fill:white;}#mermaid-svg-qphpWwFMbXWeBbCv #sequencenumber{fill:#333;}#mermaid-svg-qphpWwFMbXWeBbCv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-qphpWwFMbXWeBbCv .messageText{fill:#333;stroke:none;}#mermaid-svg-qphpWwFMbXWeBbCv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qphpWwFMbXWeBbCv .labelText,#mermaid-svg-qphpWwFMbXWeBbCv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-qphpWwFMbXWeBbCv .loopText,#mermaid-svg-qphpWwFMbXWeBbCv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-qphpWwFMbXWeBbCv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-qphpWwFMbXWeBbCv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-qphpWwFMbXWeBbCv .noteText,#mermaid-svg-qphpWwFMbXWeBbCv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-qphpWwFMbXWeBbCv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qphpWwFMbXWeBbCv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qphpWwFMbXWeBbCv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-qphpWwFMbXWeBbCv .actorPopupMenu{position:absolute;}#mermaid-svg-qphpWwFMbXWeBbCv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-qphpWwFMbXWeBbCv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-qphpWwFMbXWeBbCv .actor-man circle,#mermaid-svg-qphpWwFMbXWeBbCv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-qphpWwFMbXWeBbCv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 服务故障 大家都退避 1s par 同时重试 雷鸣群效应 同时打过来,服务再次崩溃 请求失败 请求失败 请求失败 重试 (T+1s) 重试 (T+1s) 重试 (T+1s)

加了抖动后,3 个客户端会在 0.5s, 1.5s 范围内随机分散重试,避免同步冲击。

重试条件(retry)

python 复制代码
from tenacity import retry, retry_if_exception_type, retry_if_result

# 仅特定异常重试
@retry(retry=retry_if_exception_type((ConnectionError, Timeout)))
def call(): pass

# 根据返回值决定是否重试
@retry(retry=retry_if_result(lambda r: r is None))
def call(): return None  # 返回 None 时重试

同步异常类型需要区分是否应当重试:

异常类型 是否应该重试
ConnectionError(网络问题) 应该重试
Timeout(超时) 通常重试
404 Not Found 不要重试(资源不存在)
400 Bad Request 不要重试(请求本身错误)
401 Unauthorized 不要重试(凭证问题)
500 Internal Server Error 视情况(可能是临时问题)

钩子函数

python 复制代码
from tenacity import retry, stop_after_attempt, before_sleep_log
import logging

logger = logging.getLogger(__name__)

@retry(
    stop=stop_after_attempt(3),
    before_sleep=before_sleep_log(logger, logging.WARNING)  # 重试前打印日志
)
def call_api():
    pass

在重试前会输出输出日志:

复制代码
WARNING:root:Retrying call_api in 1.0 seconds as it raised ConnectionError: ...

tenacity 应用

本文实验中订单服务的 process_payment 函数:

  • timeout=10:单次 HTTP 请求最多等 10 秒
  • stop_after_attempt(2):失败最多重试 1 次(共 2 次)
  • 总最坏时间:10s + 0.5s + 10s = 20.5s

注意:总等待时间 = 单次超时 × 重试次数 + 退避总时间,因此必须确保此值 < 上游调用方的超时,否则你重试还没完,调用方已经超时了。

python 复制代码
@retry(
    stop=stop_after_attempt(2),                        # 最多 2 次
    wait=wait_exponential(multiplier=1, min=0.5, max=2)  # 0.5s, 1s, 2s
)
def process_payment(user_id, amount):
    try:
        r = requests.post(
            f"{PAYMENT_SERVICE_URL}/api/payments/charge",
            json={'user_id': user_id, 'amount': amount},
            timeout=10  # ← 单次超时 10 秒
        )
        r.raise_for_status()
        return True
    except requests.Timeout:
        logger.error("Payment timeout")
        raise   # ← 重新抛出,触发 tenacity 重试
    except Exception as e:
        logger.warning(f"Payment failed: {e}")
        return False

环境初始化

本次测试的完整架构如下,所有跨服务的 TCP 通信都经过 Toxiproxy。
#mermaid-svg-eDFgzjrwvJIBcCyB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eDFgzjrwvJIBcCyB .error-icon{fill:#552222;}#mermaid-svg-eDFgzjrwvJIBcCyB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eDFgzjrwvJIBcCyB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eDFgzjrwvJIBcCyB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eDFgzjrwvJIBcCyB .marker.cross{stroke:#333333;}#mermaid-svg-eDFgzjrwvJIBcCyB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eDFgzjrwvJIBcCyB p{margin:0;}#mermaid-svg-eDFgzjrwvJIBcCyB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB .cluster-label text{fill:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB .cluster-label span{color:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB .cluster-label span p{background-color:transparent;}#mermaid-svg-eDFgzjrwvJIBcCyB .label text,#mermaid-svg-eDFgzjrwvJIBcCyB span{fill:#333;color:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB .node rect,#mermaid-svg-eDFgzjrwvJIBcCyB .node circle,#mermaid-svg-eDFgzjrwvJIBcCyB .node ellipse,#mermaid-svg-eDFgzjrwvJIBcCyB .node polygon,#mermaid-svg-eDFgzjrwvJIBcCyB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eDFgzjrwvJIBcCyB .rough-node .label text,#mermaid-svg-eDFgzjrwvJIBcCyB .node .label text,#mermaid-svg-eDFgzjrwvJIBcCyB .image-shape .label,#mermaid-svg-eDFgzjrwvJIBcCyB .icon-shape .label{text-anchor:middle;}#mermaid-svg-eDFgzjrwvJIBcCyB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eDFgzjrwvJIBcCyB .rough-node .label,#mermaid-svg-eDFgzjrwvJIBcCyB .node .label,#mermaid-svg-eDFgzjrwvJIBcCyB .image-shape .label,#mermaid-svg-eDFgzjrwvJIBcCyB .icon-shape .label{text-align:center;}#mermaid-svg-eDFgzjrwvJIBcCyB .node.clickable{cursor:pointer;}#mermaid-svg-eDFgzjrwvJIBcCyB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eDFgzjrwvJIBcCyB .arrowheadPath{fill:#333333;}#mermaid-svg-eDFgzjrwvJIBcCyB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eDFgzjrwvJIBcCyB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eDFgzjrwvJIBcCyB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eDFgzjrwvJIBcCyB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eDFgzjrwvJIBcCyB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eDFgzjrwvJIBcCyB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eDFgzjrwvJIBcCyB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eDFgzjrwvJIBcCyB .cluster text{fill:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB .cluster span{color:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eDFgzjrwvJIBcCyB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eDFgzjrwvJIBcCyB rect.text{fill:none;stroke-width:0;}#mermaid-svg-eDFgzjrwvJIBcCyB .icon-shape,#mermaid-svg-eDFgzjrwvJIBcCyB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eDFgzjrwvJIBcCyB .icon-shape p,#mermaid-svg-eDFgzjrwvJIBcCyB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eDFgzjrwvJIBcCyB .icon-shape .label rect,#mermaid-svg-eDFgzjrwvJIBcCyB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eDFgzjrwvJIBcCyB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eDFgzjrwvJIBcCyB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eDFgzjrwvJIBcCyB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Toxiproxy 代理层 (4个代理)
HTTP :5000
psycopg2
redis client
HTTP
HTTP
psycopg2
动态控制
动态控制
动态控制
动态控制
客户端 curl
Order Service
postgres-proxy

:5433
redis-proxy

:6380
payment-proxy

:9001
inventory-proxy

:8001
PostgreSQL
Redis
Payment Service
Inventory Service
Toxiproxy API :8474

使用docker-compose.yml部署服务,环境变量必须指向 toxiproxy。这是最容易出错的点。应用直连真实服务时,故障注入完全无效

yaml 复制代码
services:
  order-service:
    build: { context: ./webapp, dockerfile: Dockerfile }
    ports: ["5000:5000"]
    environment:
      # ↓ 所有外部依赖都指向 toxiproxy
      - DATABASE_URL=postgresql://orderuser:orderpass@toxiproxy:5433/orderdb
      - REDIS_URL=redis://toxiproxy:6380/0
      - PAYMENT_SERVICE_URL=http://toxiproxy:9001
      - INVENTORY_SERVICE_URL=http://toxiproxy:8001
    depends_on: [postgres, redis, toxiproxy, payment-service, inventory-service]
    networks: [chaos-net]

  inventory-service:
    build: { context: ./webapp, dockerfile: Dockerfile.inventory }
    ports: ["5001:5001"]
    environment:
      # ↓ 库存服务的数据库调用也走 toxiproxy
      - DATABASE_URL=postgresql://invuser:invpass@toxiproxy:5433/inventorydb
    depends_on: [postgres, toxiproxy]
    networks: [chaos-net]

  payment-service:
    build: { context: ./webapp, dockerfile: Dockerfile.payment }
    ports: ["7001:7001"]
    networks: [chaos-net]

  postgres:
    image: postgres:15
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
    networks: [chaos-net]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U superuser"]

  redis:
    image: redis:7.2-bookworm
    networks: [chaos-net]

  toxiproxy:
    image: ghcr.io/shopify/toxiproxy:2.5.0
    ports:
      - "8474:8474"     # HTTP API
      - "5433:5433"     # postgres-proxy
      - "6380:6380"     # redis-proxy
      - "9002:9001"     # payment-proxy (9001被占用映射到9002)
      - "8001:8001"     # inventory-proxy
    command: ["-host", "0.0.0.0"]
    networks: [chaos-net]

networks:
  chaos-net: { driver: bridge }
volumes:
  postgres-data:

数据库初始化

sql 复制代码
-- scripts/init-db.sql
CREATE DATABASE orderdb;
CREATE DATABASE inventorydb;
CREATE USER orderuser WITH PASSWORD 'orderpass';
CREATE USER invuser WITH PASSWORD 'invpass';
GRANT ALL PRIVILEGES ON DATABASE orderdb TO orderuser;
GRANT ALL PRIVILEGES ON DATABASE inventorydb TO invuser;

\c orderdb;
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    order_id VARCHAR(50) UNIQUE NOT NULL,
    user_id VARCHAR(50) NOT NULL,
    total_amount DECIMAL(10, 2) DEFAULT 0,
    status VARCHAR(20) DEFAULT 'created',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO orderuser;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO orderuser;

\c inventorydb;
CREATE TABLE products (
    id VARCHAR(50) PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INTEGER DEFAULT 0
);
INSERT INTO products VALUES
    ('PROD-001', '精品咖啡豆 500g', 128.00, 100),
    ('PROD-002', '手冲咖啡壶套装', 299.00, 50),
    ('PROD-003', '进口全脂牛奶 1L', 18.00, 200);
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO invuser;

Toxiproxy 代理初始化

服务启动后 Toxiproxy 是空的,没有任何代理,必须手动创建。

bash 复制代码
# 创建 4 个代理
curl -X POST http://localhost:8474/proxies \
  -d '{"name":"postgres-proxy","listen":"0.0.0.0:5433","upstream":"postgres:5432"}'

curl -X POST http://localhost:8474/proxies \
  -d '{"name":"redis-proxy","listen":"0.0.0.0:6380","upstream":"redis:6379"}'

curl -X POST http://localhost:8474/proxies \
  -d '{"name":"payment-proxy","listen":"0.0.0.0:9001","upstream":"payment-service:7001"}'

curl -X POST http://localhost:8474/proxies \
  -d '{"name":"inventory-proxy","listen":"0.0.0.0:8001","upstream":"inventory-service:5001"}'

确认创建:

bash 复制代码
$ curl -s http://localhost:8474/proxies | python3 -m json.tool
{
    "inventory-proxy": {"listen": "[::]:8001", "upstream": "inventory-service:5001", ...},
    "payment-proxy":   {"listen": "[::]:9001", "upstream": "payment-service:7001", ...},
    "postgres-proxy":  {"listen": "[::]:5433", "upstream": "postgres:5432", ...},
    "redis-proxy":     {"listen": "[::]:6380", "upstream": "redis:6379", ...}
}

应用韧性模式实现

订单服务模拟一个真实的电商下单流程,业务调用链如下,每一步都涉及不同等级的故障容忍策略
#mermaid-svg-Sb5Hs6iClyT9MOVF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sb5Hs6iClyT9MOVF .error-icon{fill:#552222;}#mermaid-svg-Sb5Hs6iClyT9MOVF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sb5Hs6iClyT9MOVF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .marker.cross{stroke:#333333;}#mermaid-svg-Sb5Hs6iClyT9MOVF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sb5Hs6iClyT9MOVF p{margin:0;}#mermaid-svg-Sb5Hs6iClyT9MOVF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .cluster-label text{fill:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .cluster-label span{color:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .cluster-label span p{background-color:transparent;}#mermaid-svg-Sb5Hs6iClyT9MOVF .label text,#mermaid-svg-Sb5Hs6iClyT9MOVF span{fill:#333;color:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .node rect,#mermaid-svg-Sb5Hs6iClyT9MOVF .node circle,#mermaid-svg-Sb5Hs6iClyT9MOVF .node ellipse,#mermaid-svg-Sb5Hs6iClyT9MOVF .node polygon,#mermaid-svg-Sb5Hs6iClyT9MOVF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .rough-node .label text,#mermaid-svg-Sb5Hs6iClyT9MOVF .node .label text,#mermaid-svg-Sb5Hs6iClyT9MOVF .image-shape .label,#mermaid-svg-Sb5Hs6iClyT9MOVF .icon-shape .label{text-anchor:middle;}#mermaid-svg-Sb5Hs6iClyT9MOVF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .rough-node .label,#mermaid-svg-Sb5Hs6iClyT9MOVF .node .label,#mermaid-svg-Sb5Hs6iClyT9MOVF .image-shape .label,#mermaid-svg-Sb5Hs6iClyT9MOVF .icon-shape .label{text-align:center;}#mermaid-svg-Sb5Hs6iClyT9MOVF .node.clickable{cursor:pointer;}#mermaid-svg-Sb5Hs6iClyT9MOVF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .arrowheadPath{fill:#333333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sb5Hs6iClyT9MOVF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Sb5Hs6iClyT9MOVF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sb5Hs6iClyT9MOVF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Sb5Hs6iClyT9MOVF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .cluster text{fill:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF .cluster span{color:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Sb5Hs6iClyT9MOVF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Sb5Hs6iClyT9MOVF rect.text{fill:none;stroke-width:0;}#mermaid-svg-Sb5Hs6iClyT9MOVF .icon-shape,#mermaid-svg-Sb5Hs6iClyT9MOVF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Sb5Hs6iClyT9MOVF .icon-shape p,#mermaid-svg-Sb5Hs6iClyT9MOVF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Sb5Hs6iClyT9MOVF .icon-shape .label rect,#mermaid-svg-Sb5Hs6iClyT9MOVF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Sb5Hs6iClyT9MOVF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Sb5Hs6iClyT9MOVF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Sb5Hs6iClyT9MOVF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST /api/orders

用户下单
① 检查库存

Inventory Service
② 处理支付

Payment Service
③ 写入订单

PostgreSQL
④ 缓存订单

Redis
返回订单号

韧性 4 大支柱示意图
#mermaid-svg-kLEbbONRdrjIRwgR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kLEbbONRdrjIRwgR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kLEbbONRdrjIRwgR .error-icon{fill:#552222;}#mermaid-svg-kLEbbONRdrjIRwgR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kLEbbONRdrjIRwgR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kLEbbONRdrjIRwgR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kLEbbONRdrjIRwgR .marker.cross{stroke:#333333;}#mermaid-svg-kLEbbONRdrjIRwgR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kLEbbONRdrjIRwgR p{margin:0;}#mermaid-svg-kLEbbONRdrjIRwgR .edge{stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .section--1 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section--1 path,#mermaid-svg-kLEbbONRdrjIRwgR .section--1 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section--1 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section--1 text{fill:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth--1{stroke-width:17;}#mermaid-svg-kLEbbONRdrjIRwgR .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-0 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-0 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-0 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-0 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-0 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-0{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-0{stroke-width:14;}#mermaid-svg-kLEbbONRdrjIRwgR .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-1 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-1 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-1 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-1 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-1 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-1{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-1{stroke-width:11;}#mermaid-svg-kLEbbONRdrjIRwgR .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-2 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-2 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-2 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-2 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-2 text{fill:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-2{stroke-width:8;}#mermaid-svg-kLEbbONRdrjIRwgR .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-3 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-3 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-3 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-3 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-3 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-3{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-3{stroke-width:5;}#mermaid-svg-kLEbbONRdrjIRwgR .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-4 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-4 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-4 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-4 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-4 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-4{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-4{stroke-width:2;}#mermaid-svg-kLEbbONRdrjIRwgR .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-5 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-5 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-5 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-5 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-5 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-5{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-5{stroke-width:-1;}#mermaid-svg-kLEbbONRdrjIRwgR .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-6 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-6 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-6 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-6 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-6 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-6{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-6{stroke-width:-4;}#mermaid-svg-kLEbbONRdrjIRwgR .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-7 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-7 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-7 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-7 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-7 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-7{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-7{stroke-width:-7;}#mermaid-svg-kLEbbONRdrjIRwgR .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-8 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-8 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-8 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-8 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-8 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-8{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-8{stroke-width:-10;}#mermaid-svg-kLEbbONRdrjIRwgR .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-9 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-9 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-9 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-9 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-9 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-9{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-9{stroke-width:-13;}#mermaid-svg-kLEbbONRdrjIRwgR .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-10 rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-10 path,#mermaid-svg-kLEbbONRdrjIRwgR .section-10 circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-10 polygon,#mermaid-svg-kLEbbONRdrjIRwgR .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-10 text{fill:black;}#mermaid-svg-kLEbbONRdrjIRwgR .node-icon-10{font-size:40px;color:black;}#mermaid-svg-kLEbbONRdrjIRwgR .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .edge-depth-10{stroke-width:-16;}#mermaid-svg-kLEbbONRdrjIRwgR .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled,#mermaid-svg-kLEbbONRdrjIRwgR .disabled circle,#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:lightgray;}#mermaid-svg-kLEbbONRdrjIRwgR .disabled text{fill:#efefef;}#mermaid-svg-kLEbbONRdrjIRwgR .section-root rect,#mermaid-svg-kLEbbONRdrjIRwgR .section-root path,#mermaid-svg-kLEbbONRdrjIRwgR .section-root circle,#mermaid-svg-kLEbbONRdrjIRwgR .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-kLEbbONRdrjIRwgR .section-root text{fill:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .section-root span{color:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .section-2 span{color:#ffffff;}#mermaid-svg-kLEbbONRdrjIRwgR .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-kLEbbONRdrjIRwgR .edge{fill:none;}#mermaid-svg-kLEbbONRdrjIRwgR .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-kLEbbONRdrjIRwgR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 韧性系统
Timeout
P99 × 3
连接超时 ≠ 读超时
上下文传播
Retry
指数退避 + 抖动
最大次数限制
幂等性前提
仅瞬态错误
Circuit Breaker
快速失败
避免雪崩
自动半开试探
Fallback
静态默认值
缓存兜底
降级响应

配置层:让流量都走 Toxiproxy,这样 Toxiproxy 才能作为"中间人"拦截到流量。

python 复制代码
# webapp/app.py
import os
import psycopg2
import redis
import requests
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://orderuser:orderpass@toxiproxy:5433/orderdb')
REDIS_URL = os.getenv('REDIS_URL', 'redis://toxiproxy:6380/0')
PAYMENT_SERVICE_URL = os.getenv('PAYMENT_SERVICE_URL', 'http://toxiproxy:9001')
INVENTORY_SERVICE_URL = os.getenv('INVENTORY_SERVICE_URL', 'http://toxiproxy:8001')

用户点击"提交订单"后,最忌讳的是白屏等待 。这里 connect_timeout=3 意味着,数据库超时设计的考量,如果 3 秒内连不上数据库,立即放弃,向用户报错而不是让浏览器转圈 30 秒。

python 复制代码
def get_db_connection():
    return psycopg2.connect(DATABASE_URL, connect_timeout=3)

常见的数据库连接延迟如下,因此3 秒是同机房场景下"绝对不可能正常但还没到雪崩"的阈值。

场景 数据库正常连接耗时 推荐 connect_timeout
同机房内网 < 5ms 1-3 秒
跨可用区 5-20ms 3-5 秒
跨地域 50-200ms 5-10 秒

库存检查:为什么"失败也算成功"

这段代码体现了业务驱动的容错决策,即库存检查在订单流程中是"软依赖"。

  • stop_after_attempt(3):库存服务允许偶发抖动,给 3 次机会
  • wait_exponential(min=1, max=10):1s → 2s → 4s,避免重试雪崩
  • retry_if_exception_type((Timeout, ConnectionError)):只对网络问题重试,HTTP 4xx 不重试(4xx 是请求本身有问题,重试也是错)
python 复制代码
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
    retry=retry_if_exception_type((requests.Timeout, requests.ConnectionError))
)
def check_inventory(items):
    try:
        r = requests.get(f"{INVENTORY_SERVICE_URL}/api/inventory", timeout=5)
        r.raise_for_status()
        return True
    except Exception as e:
        logger.warning(f"Inventory check degraded: {e}")
        return True   # ← 注意这里!失败也返回 True

**为什么库存可以降级?**因为

  • 超卖损失:100 个订单中可能 1-2 单超卖,造成几百元额外发货成本
  • 拒绝下单损失:100 个订单全部失败,损失数万元 GMV + 客户体验

绝大多数电商选择"放行 + 事后对账"。这就是为什么代码里 except 分支也返回 True
#mermaid-svg-GnHCvpg3mfq6GKhL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GnHCvpg3mfq6GKhL .error-icon{fill:#552222;}#mermaid-svg-GnHCvpg3mfq6GKhL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GnHCvpg3mfq6GKhL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GnHCvpg3mfq6GKhL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GnHCvpg3mfq6GKhL .marker.cross{stroke:#333333;}#mermaid-svg-GnHCvpg3mfq6GKhL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GnHCvpg3mfq6GKhL p{margin:0;}#mermaid-svg-GnHCvpg3mfq6GKhL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL .cluster-label text{fill:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL .cluster-label span{color:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL .cluster-label span p{background-color:transparent;}#mermaid-svg-GnHCvpg3mfq6GKhL .label text,#mermaid-svg-GnHCvpg3mfq6GKhL span{fill:#333;color:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL .node rect,#mermaid-svg-GnHCvpg3mfq6GKhL .node circle,#mermaid-svg-GnHCvpg3mfq6GKhL .node ellipse,#mermaid-svg-GnHCvpg3mfq6GKhL .node polygon,#mermaid-svg-GnHCvpg3mfq6GKhL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GnHCvpg3mfq6GKhL .rough-node .label text,#mermaid-svg-GnHCvpg3mfq6GKhL .node .label text,#mermaid-svg-GnHCvpg3mfq6GKhL .image-shape .label,#mermaid-svg-GnHCvpg3mfq6GKhL .icon-shape .label{text-anchor:middle;}#mermaid-svg-GnHCvpg3mfq6GKhL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GnHCvpg3mfq6GKhL .rough-node .label,#mermaid-svg-GnHCvpg3mfq6GKhL .node .label,#mermaid-svg-GnHCvpg3mfq6GKhL .image-shape .label,#mermaid-svg-GnHCvpg3mfq6GKhL .icon-shape .label{text-align:center;}#mermaid-svg-GnHCvpg3mfq6GKhL .node.clickable{cursor:pointer;}#mermaid-svg-GnHCvpg3mfq6GKhL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GnHCvpg3mfq6GKhL .arrowheadPath{fill:#333333;}#mermaid-svg-GnHCvpg3mfq6GKhL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GnHCvpg3mfq6GKhL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GnHCvpg3mfq6GKhL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GnHCvpg3mfq6GKhL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GnHCvpg3mfq6GKhL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GnHCvpg3mfq6GKhL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GnHCvpg3mfq6GKhL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GnHCvpg3mfq6GKhL .cluster text{fill:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL .cluster span{color:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GnHCvpg3mfq6GKhL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GnHCvpg3mfq6GKhL rect.text{fill:none;stroke-width:0;}#mermaid-svg-GnHCvpg3mfq6GKhL .icon-shape,#mermaid-svg-GnHCvpg3mfq6GKhL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GnHCvpg3mfq6GKhL .icon-shape p,#mermaid-svg-GnHCvpg3mfq6GKhL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GnHCvpg3mfq6GKhL .icon-shape .label rect,#mermaid-svg-GnHCvpg3mfq6GKhL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GnHCvpg3mfq6GKhL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GnHCvpg3mfq6GKhL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GnHCvpg3mfq6GKhL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拒绝下单
拒绝下单
拒绝下单
放行下单
放行下单
放行下单
库存服务挂了

该怎么办?
用户流失
GMV损失
库存服务变成单点
订单照常完成
⚠️ 可能少量超卖
后续异步对账可补救

支付处理:为什么"失败必须是失败"

支付是订单流程的"关键路径",任何不确定都必须报错让用户知情。

python 复制代码
@retry(stop=stop_after_attempt(2), wait=wait_exponential(min=0.5, max=2))
def process_payment(user_id, amount):
    try:
        r = requests.post(
            f"{PAYMENT_SERVICE_URL}/api/payments/charge",
            json={'user_id': user_id, 'amount': amount},
            timeout=10
        )
        r.raise_for_status()
        return True
    except requests.Timeout:
        raise          # ← 抛出,让 tenacity 接管重试
    except Exception as e:
        return False   # ← 返回失败,不能假装成功

对比库存检查的关键差异

维度 check_inventory process_payment
失败时返回 True(降级) False(如实报告)
重试次数 3 次(更宽容) 2 次(更保守)
退避时间 1-10s(慢) 0.5-2s(快)
业务影响 少量超卖 用户重复扣款风险

**为什么支付重试要保守?**超时不等于失败,有可能上游已经成功,只是响应丢了。重试就会变成重复操作。
银行 Payment Service Order Service 用户 银行 Payment Service Order Service 用户 #mermaid-svg-l4mvRK3zv8JjYnfp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-l4mvRK3zv8JjYnfp .error-icon{fill:#552222;}#mermaid-svg-l4mvRK3zv8JjYnfp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-l4mvRK3zv8JjYnfp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-l4mvRK3zv8JjYnfp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-l4mvRK3zv8JjYnfp .marker.cross{stroke:#333333;}#mermaid-svg-l4mvRK3zv8JjYnfp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-l4mvRK3zv8JjYnfp p{margin:0;}#mermaid-svg-l4mvRK3zv8JjYnfp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-l4mvRK3zv8JjYnfp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-l4mvRK3zv8JjYnfp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-l4mvRK3zv8JjYnfp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-l4mvRK3zv8JjYnfp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-l4mvRK3zv8JjYnfp .sequenceNumber{fill:white;}#mermaid-svg-l4mvRK3zv8JjYnfp #sequencenumber{fill:#333;}#mermaid-svg-l4mvRK3zv8JjYnfp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-l4mvRK3zv8JjYnfp .messageText{fill:#333;stroke:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-l4mvRK3zv8JjYnfp .labelText,#mermaid-svg-l4mvRK3zv8JjYnfp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .loopText,#mermaid-svg-l4mvRK3zv8JjYnfp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-l4mvRK3zv8JjYnfp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-l4mvRK3zv8JjYnfp .noteText,#mermaid-svg-l4mvRK3zv8JjYnfp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-l4mvRK3zv8JjYnfp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-l4mvRK3zv8JjYnfp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-l4mvRK3zv8JjYnfp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-l4mvRK3zv8JjYnfp .actorPopupMenu{position:absolute;}#mermaid-svg-l4mvRK3zv8JjYnfp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-l4mvRK3zv8JjYnfp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-l4mvRK3zv8JjYnfp .actor-man circle,#mermaid-svg-l4mvRK3zv8JjYnfp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-l4mvRK3zv8JjYnfp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 网络抖动,响应丢失 tenacity 触发重试 用户被扣款 ¥598! 下单 (¥299) 扣款 (¥299) 扣款指令 扣款成功 ⏱ Timeout 扣款 (¥299) 重试 扣款指令 ⚠️ 重复! 扣款成功

正确的做法是配合幂等性 ,支付服务收到相同的 key 时直接返回上次结果,不会重复扣款

python 复制代码
import uuid
idempotency_key = str(uuid.uuid4())   # 客户端生成,本次订单全程不变

r = requests.post(
    f"{PAYMENT_SERVICE_URL}/api/payments/charge",
    json={'user_id': user_id, 'amount': amount},
    headers={'X-Idempotency-Key': idempotency_key},   # 关键
    timeout=10
)

订单查询:缓存降级的优雅退化

订单查询是高频读操作(用户反复刷新订单页面),用 Redis 缓存提升性能。但 Redis 不是权威数据源,最终数据在 PostgreSQL。

python 复制代码
@app.route('/api/orders/<order_id>')
def get_order(order_id):
    if redis_client:
        try:
            cached = redis_client.get(f"order:{order_id}")
            if cached:
                return jsonify({'data': str(cached), 'source': 'cache'})
        except Exception as e:
            logger.warning(f"Cache read failed, falling back to DB: {e}")
            # 注意:这里没有 raise,流程继续往下走

    try:
        conn = get_db_connection()
        # ... 从 DB 查询并返回
    except Exception:
        return jsonify({'error': 'database unavailable'}), 503

降级路径的业务图景如下
#mermaid-svg-JMqlBHTIihU1MWmu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JMqlBHTIihU1MWmu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JMqlBHTIihU1MWmu .error-icon{fill:#552222;}#mermaid-svg-JMqlBHTIihU1MWmu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JMqlBHTIihU1MWmu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JMqlBHTIihU1MWmu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JMqlBHTIihU1MWmu .marker.cross{stroke:#333333;}#mermaid-svg-JMqlBHTIihU1MWmu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JMqlBHTIihU1MWmu p{margin:0;}#mermaid-svg-JMqlBHTIihU1MWmu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JMqlBHTIihU1MWmu .cluster-label text{fill:#333;}#mermaid-svg-JMqlBHTIihU1MWmu .cluster-label span{color:#333;}#mermaid-svg-JMqlBHTIihU1MWmu .cluster-label span p{background-color:transparent;}#mermaid-svg-JMqlBHTIihU1MWmu .label text,#mermaid-svg-JMqlBHTIihU1MWmu span{fill:#333;color:#333;}#mermaid-svg-JMqlBHTIihU1MWmu .node rect,#mermaid-svg-JMqlBHTIihU1MWmu .node circle,#mermaid-svg-JMqlBHTIihU1MWmu .node ellipse,#mermaid-svg-JMqlBHTIihU1MWmu .node polygon,#mermaid-svg-JMqlBHTIihU1MWmu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JMqlBHTIihU1MWmu .rough-node .label text,#mermaid-svg-JMqlBHTIihU1MWmu .node .label text,#mermaid-svg-JMqlBHTIihU1MWmu .image-shape .label,#mermaid-svg-JMqlBHTIihU1MWmu .icon-shape .label{text-anchor:middle;}#mermaid-svg-JMqlBHTIihU1MWmu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JMqlBHTIihU1MWmu .rough-node .label,#mermaid-svg-JMqlBHTIihU1MWmu .node .label,#mermaid-svg-JMqlBHTIihU1MWmu .image-shape .label,#mermaid-svg-JMqlBHTIihU1MWmu .icon-shape .label{text-align:center;}#mermaid-svg-JMqlBHTIihU1MWmu .node.clickable{cursor:pointer;}#mermaid-svg-JMqlBHTIihU1MWmu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JMqlBHTIihU1MWmu .arrowheadPath{fill:#333333;}#mermaid-svg-JMqlBHTIihU1MWmu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JMqlBHTIihU1MWmu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JMqlBHTIihU1MWmu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JMqlBHTIihU1MWmu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JMqlBHTIihU1MWmu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JMqlBHTIihU1MWmu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JMqlBHTIihU1MWmu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JMqlBHTIihU1MWmu .cluster text{fill:#333;}#mermaid-svg-JMqlBHTIihU1MWmu .cluster span{color:#333;}#mermaid-svg-JMqlBHTIihU1MWmu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JMqlBHTIihU1MWmu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JMqlBHTIihU1MWmu rect.text{fill:none;stroke-width:0;}#mermaid-svg-JMqlBHTIihU1MWmu .icon-shape,#mermaid-svg-JMqlBHTIihU1MWmu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JMqlBHTIihU1MWmu .icon-shape p,#mermaid-svg-JMqlBHTIihU1MWmu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JMqlBHTIihU1MWmu .icon-shape .label rect,#mermaid-svg-JMqlBHTIihU1MWmu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JMqlBHTIihU1MWmu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JMqlBHTIihU1MWmu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JMqlBHTIihU1MWmu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





GET /api/orders/123
Redis 可用?
读取缓存
命中?
返回 source:cache

响应时间 ~5ms
回源数据库
记录警告日志
DB 可用?
返回 source:database

响应时间 ~50ms
503 服务不可用

真正的失败

层次化的故障耐受度

故障组合 用户体验 实现
Redis 正常 + DB 正常 极快(5ms) 缓存命中
Redis 异常 + DB 正常 稍慢(50ms) 自动降级到 DB,用户无感知
Redis 正常 + DB 异常 缓存命中可用,未命中失败 部分功能
Redis 异常 + DB 异常 503 错误 真正的故障

关键设计原则永远不要让缓存的故障变成业务的故障

代码里 except Exception as e: logger.warning(...) 这一行没有 raise,因为:

  • 缓存读失败 ≠ 业务失败
  • 主流程继续走 DB 查询,用户可能感知到"慢了一点"但不会看到错误页

韧性设计的三层防御

#mermaid-svg-FEf773SnGEHoyoac{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FEf773SnGEHoyoac .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FEf773SnGEHoyoac .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FEf773SnGEHoyoac .error-icon{fill:#552222;}#mermaid-svg-FEf773SnGEHoyoac .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FEf773SnGEHoyoac .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FEf773SnGEHoyoac .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FEf773SnGEHoyoac .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FEf773SnGEHoyoac .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FEf773SnGEHoyoac .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FEf773SnGEHoyoac .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FEf773SnGEHoyoac .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FEf773SnGEHoyoac .marker.cross{stroke:#333333;}#mermaid-svg-FEf773SnGEHoyoac svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FEf773SnGEHoyoac p{margin:0;}#mermaid-svg-FEf773SnGEHoyoac .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FEf773SnGEHoyoac .cluster-label text{fill:#333;}#mermaid-svg-FEf773SnGEHoyoac .cluster-label span{color:#333;}#mermaid-svg-FEf773SnGEHoyoac .cluster-label span p{background-color:transparent;}#mermaid-svg-FEf773SnGEHoyoac .label text,#mermaid-svg-FEf773SnGEHoyoac span{fill:#333;color:#333;}#mermaid-svg-FEf773SnGEHoyoac .node rect,#mermaid-svg-FEf773SnGEHoyoac .node circle,#mermaid-svg-FEf773SnGEHoyoac .node ellipse,#mermaid-svg-FEf773SnGEHoyoac .node polygon,#mermaid-svg-FEf773SnGEHoyoac .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FEf773SnGEHoyoac .rough-node .label text,#mermaid-svg-FEf773SnGEHoyoac .node .label text,#mermaid-svg-FEf773SnGEHoyoac .image-shape .label,#mermaid-svg-FEf773SnGEHoyoac .icon-shape .label{text-anchor:middle;}#mermaid-svg-FEf773SnGEHoyoac .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FEf773SnGEHoyoac .rough-node .label,#mermaid-svg-FEf773SnGEHoyoac .node .label,#mermaid-svg-FEf773SnGEHoyoac .image-shape .label,#mermaid-svg-FEf773SnGEHoyoac .icon-shape .label{text-align:center;}#mermaid-svg-FEf773SnGEHoyoac .node.clickable{cursor:pointer;}#mermaid-svg-FEf773SnGEHoyoac .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FEf773SnGEHoyoac .arrowheadPath{fill:#333333;}#mermaid-svg-FEf773SnGEHoyoac .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FEf773SnGEHoyoac .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FEf773SnGEHoyoac .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FEf773SnGEHoyoac .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FEf773SnGEHoyoac .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FEf773SnGEHoyoac .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FEf773SnGEHoyoac .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FEf773SnGEHoyoac .cluster text{fill:#333;}#mermaid-svg-FEf773SnGEHoyoac .cluster span{color:#333;}#mermaid-svg-FEf773SnGEHoyoac div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FEf773SnGEHoyoac .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FEf773SnGEHoyoac rect.text{fill:none;stroke-width:0;}#mermaid-svg-FEf773SnGEHoyoac .icon-shape,#mermaid-svg-FEf773SnGEHoyoac .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FEf773SnGEHoyoac .icon-shape p,#mermaid-svg-FEf773SnGEHoyoac .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FEf773SnGEHoyoac .icon-shape .label rect,#mermaid-svg-FEf773SnGEHoyoac .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FEf773SnGEHoyoac .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FEf773SnGEHoyoac .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FEf773SnGEHoyoac :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 超时
重试耗尽
降级失败
成功
成功
降级响应
请求
第1层: 超时
第2层: 重试 + 退避
第3层: 降级 / 熔断
返回错误

记录告警
返回结果

故障注入

每个测试前后需要保持环境洁净,避免上一个实验残留的 toxic 干扰下一个

bash 复制代码
# 实验开始前: 清除某个代理的所有 toxic
$ curl -X DELETE http://localhost:8474/proxies/payment-proxy/toxics/<toxic_name>
# 查看所有当前注入的 toxic
$ curl -s http://localhost:8474/proxies | python3 -m json.tool | grep -A2 toxics

故障注入的通用模板如下

  • 每次注入会立即返回 toxic 配置 JSON,可作为成功凭证。注入是即时生效的,下一个 TCP 数据包就会被影响。
bash 复制代码
curl -X POST http://localhost:8474/proxies/<代理名>/toxics \
  -H "Content-Type: application/json" \
  -d '{
    "name":     "<toxic唯一名>",       # 用于后续引用/删除
    "type":     "<latency|timeout|bandwidth|...>",  # toxic 类型
    "stream":   "<upstream|downstream>",            # 默认 downstream
    "toxicity": <0.0-1.0>,                          # 默认 1.0,即 100% 触发
    "attributes": { ... }                           # 类型特定参数
  }'

基线测试

建立"无故障"性能基准,作为后续所有实验的对照组。基线保持代理完全透明。

bash 复制代码
$ time curl -X POST http://localhost:5000/api/orders \
  -H "Content-Type: application/json" \
  -d '{"user_id":"user_baseline","items":[{"product_id":"PROD-001","quantity":2,"price":128}]}'

响应(70ms):

json 复制代码
{
  "order_id": "ORD-1780121661-4456",
  "user_id": "user_baseline",
  "items": [{"price":128, "product_id":"PROD-001", "quantity":2}],
  "total": 256,
  "status": "created",
  "created_at": "2026-05-30T06:14:21.193508"
}

应用日志:

复制代码
2026-05-30 06:14:21,123 [INFO] Creating order ORD-1780121661-4456 for user user_baseline, total=256
2026-05-30 06:14:21,124 [INFO] Checking inventory via toxiproxy...
2026-05-30 06:14:21,140 [INFO] Processing payment via toxiproxy for user_baseline: 256
2026-05-30 06:14:21,180 [INFO] Order ORD-1780121661-4456 saved to PostgreSQL via toxiproxy
2026-05-30 06:14:21,193 [INFO] Order ORD-1780121661-4456 cached
2026-05-30 06:14:21,193 [INFO] Order ORD-1780121661-4456 created successfully

链路分解(共 70ms):

  • Inventory 检查:~16ms(HTTP → toxiproxy → inventory → toxiproxy → postgres)
  • Payment 处理:~40ms(HTTP → toxiproxy → payment)
  • DB 写入:~13ms(psycopg2 → toxiproxy → postgres)
  • Redis 写入:~1ms

Inventory 服务注入延迟 2 秒

模拟库存服务因 GC 暂停或慢查询导致响应慢 2 秒的真实场景,验证应用是否能在容忍范围内正常完成下单。

注入命令

bash 复制代码
$ curl -X POST http://localhost:8474/proxies/inventory-proxy/toxics \
  -d '{"name":"inv_lat","type":"latency","stream":"downstream","attributes":{"latency":2000}}'

{"attributes":{"latency":2000,"jitter":0},"name":"inv_lat",
 "type":"latency","stream":"downstream","toxicity":1}

注入了什么

字段 含义
name inv_lat toxic 唯一标识,用于后续删除
type latency toxic 类型------网络延迟
stream downstream 方向:响应方向(库存服务 → 订单服务)
latency 2000 每个数据包延迟 2000 毫秒(2秒)
jitter 0(默认) 无抖动,固定延迟
toxicity 1(默认) 100% 触发率,每次请求都被延迟

底层数据流详情

  • **选 downstream**是因为我们要模拟"服务响应慢",而不是"请求发送慢"。下行延迟模拟的是"上游处理 + 网络回程"的总耗时。

    订单服务请求 → toxiproxy:8001 (inventory-proxy)

    ① toxiproxy 立刻转发请求到 inventory-service:5001
    ② inventory-service 正常处理(~10ms)并返回响应
    ③ 响应数据进入 downstream toxic 链
    ④ LatencyToxic 让每个数据包等待 2000ms 后才输出
    ⑤ 订单服务最终收到响应,总耗时 ≈ 2000ms + 正常耗时

注入后的状态查询:

bash 复制代码
$ curl -s http://localhost:8474/proxies/inventory-proxy
{
  "name": "inventory-proxy",
  "listen": "[::]:8001",
  "upstream": "inventory-service:5001",
  "enabled": true,
  "toxics": [
    {
      "name": "inv_lat",
      "type": "latency",
      "stream": "downstream",
      "toxicity": 1,
      "attributes": {"latency": 2000, "jitter": 0}
    }
  ]
}

发起订单:

bash 复制代码
$ time curl -X POST http://localhost:5000/api/orders \
  -d '{"user_id":"user_inv","items":[{"product_id":"PROD-002","quantity":1,"price":299}]}'

{"order_id":"ORD-1780121685-8614","status":"created", ...}

响应时间:2060ms(基线 70ms + 注入 2000ms)

关键观察

指标 基线 注入后 变化
响应时间 70ms 2060ms +2000ms ≈ 注入值
业务结果 成功 成功 应用容忍了延迟
错误日志 5s timeout > 2s latency

这印证了 tenacity 配置timeout=5 大于注入延迟 2000ms,所以单次成功,不触发重试。

PostgreSQL 连接累计超时

模拟"数据库网络抖动延迟 1.5 秒",验证 connect_timeout=3 在协议握手累积下被触发的真实场景------这是云环境中最常见的故障模式之一

注入命令与参数解读:

bash 复制代码
$ curl -X POST http://localhost:8474/proxies/postgres-proxy/toxics \
  -d '{"name":"db_lat","type":"latency","stream":"downstream","attributes":{"latency":1500}}'

注入了什么

字段 含义
name db_lat toxic 标识
type latency 延迟类型
stream downstream 响应方向(PostgreSQL → 应用)
latency 1500 每个 TCP 包延迟 1.5 秒

查询订单:

bash 复制代码
$ time curl http://localhost:5000/api/orders/ORD-1780121661-4456
{"error":"database unavailable"}

real    0m3.015s

1.5 秒乍看"还能接受",但 PostgreSQL 协议不是单次往返 。它的连接建立是一个多阶段的状态机。为什么 1500ms 延迟会导致失败? 让我们看 psycopg2 的连接过程:
注意:Toxiproxy 的 toxic 工作在 TCP 连接建立之后的数据流上 。TCP 三次握手(SYN/SYN-ACK/ACK)是网络层操作,由 net.Listener.Accept()net.Dial() 完成,不经过 toxic pipelinestream: downstream 只影响连接内从 PostgreSQL 返回的协议响应数据
PostgreSQL Toxiproxy (downstream 延迟1500ms) psycopg2 PostgreSQL Toxiproxy (downstream 延迟1500ms) psycopg2 #mermaid-svg-jhGeITD964VQ8tnU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jhGeITD964VQ8tnU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jhGeITD964VQ8tnU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jhGeITD964VQ8tnU .error-icon{fill:#552222;}#mermaid-svg-jhGeITD964VQ8tnU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jhGeITD964VQ8tnU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jhGeITD964VQ8tnU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jhGeITD964VQ8tnU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jhGeITD964VQ8tnU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jhGeITD964VQ8tnU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jhGeITD964VQ8tnU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jhGeITD964VQ8tnU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jhGeITD964VQ8tnU .marker.cross{stroke:#333333;}#mermaid-svg-jhGeITD964VQ8tnU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jhGeITD964VQ8tnU p{margin:0;}#mermaid-svg-jhGeITD964VQ8tnU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jhGeITD964VQ8tnU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jhGeITD964VQ8tnU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jhGeITD964VQ8tnU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jhGeITD964VQ8tnU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jhGeITD964VQ8tnU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jhGeITD964VQ8tnU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jhGeITD964VQ8tnU .sequenceNumber{fill:white;}#mermaid-svg-jhGeITD964VQ8tnU #sequencenumber{fill:#333;}#mermaid-svg-jhGeITD964VQ8tnU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jhGeITD964VQ8tnU .messageText{fill:#333;stroke:none;}#mermaid-svg-jhGeITD964VQ8tnU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jhGeITD964VQ8tnU .labelText,#mermaid-svg-jhGeITD964VQ8tnU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jhGeITD964VQ8tnU .loopText,#mermaid-svg-jhGeITD964VQ8tnU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jhGeITD964VQ8tnU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jhGeITD964VQ8tnU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jhGeITD964VQ8tnU .noteText,#mermaid-svg-jhGeITD964VQ8tnU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jhGeITD964VQ8tnU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jhGeITD964VQ8tnU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jhGeITD964VQ8tnU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jhGeITD964VQ8tnU .actorPopupMenu{position:absolute;}#mermaid-svg-jhGeITD964VQ8tnU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jhGeITD964VQ8tnU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jhGeITD964VQ8tnU .actor-man circle,#mermaid-svg-jhGeITD964VQ8tnU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jhGeITD964VQ8tnU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① TCP 三次握手(网络层,不经过 toxic) 握手完成,~1ms,toxic 未介入 ② PostgreSQL 协议握手(downstream toxic 生效) → upstream 方向,无 toxic,瞬间到达 ⏱ downstream toxic: 延迟 1500ms 累计: 1500ms ⏱ downstream toxic: 延迟 1500ms 累计: 3000ms ⏱ 下行数据继续延迟... connect_timeout=3 已触发 psycopg2 放弃连接 TCP SYN TCP SYN TCP SYN-ACK TCP SYN-ACK StartupMessage (user=orderuser, db=orderdb) AuthenticationOk AuthenticationOk (+1500ms) PasswordMessage (md5) AuthenticationOk AuthenticationOk (+1500ms) ParameterStatus × N

根因分析

阶段 方向 是否被 toxic 影响 延迟
TCP 三次握手 双向 不经过 toxic ~1ms
StartupMessage upstream (App→PG) 无 upstream toxic ~1ms
AuthenticationOk downstream (PG→App) 被延迟 1500ms +1500ms
PasswordMessage upstream (App→PG) 无 upstream toxic ~1ms
AuthenticationOk downstream (PG→App) 被延迟 1500ms +1500ms
ParameterStatus 等 downstream (PG→App) 继续被延迟 没等到就超时了

connect_timeout=3 计的是从开始连接到收到 ReadyForQuery 的总时间。每个 PostgreSQL 返回的协议消息都 +1500ms,两次认证响应就已达 3000ms,刚好卡在超时阈值上。单次 downstream 延迟看起来"还能接受",但在 PostgreSQL 协议的多次认证往返中累积后变成致命。

Payment 服务严重延迟

注入超过应用层超时(timeout=10s)的延迟,精确触发 tenacity 重试逻辑,观察重试时间线和最终失败行为。这复现了"下游服务卡死,重试反而让用户等更久"的痛点场景。

注入命令与参数解读:

bash 复制代码
$ curl -X POST http://localhost:8474/proxies/payment-proxy/toxics \
  -d '{"name":"payment_long","type":"latency","attributes":{"latency":15000}}'

注入了什么

字段 含义
name payment_long toxic 标识
type latency 延迟类型
stream (未指定,默认 downstream 响应方向
latency 15000 延迟 15 秒

为什么选 15 秒?这是精心设计的实验值:

复制代码
应用配置: timeout=10s (process_payment 函数)
注入延迟: 15s
关系: 15s > 10s
结果: 必然触发 ReadTimeout,然后被 tenacity 接住

底层数据流示意图如下:
Payment Service Toxiproxy (latency:15000) Order Service Payment Service Toxiproxy (latency:15000) Order Service #mermaid-svg-mOkX5BJ1rBGkhu85{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mOkX5BJ1rBGkhu85 .error-icon{fill:#552222;}#mermaid-svg-mOkX5BJ1rBGkhu85 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mOkX5BJ1rBGkhu85 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mOkX5BJ1rBGkhu85 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mOkX5BJ1rBGkhu85 .marker.cross{stroke:#333333;}#mermaid-svg-mOkX5BJ1rBGkhu85 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mOkX5BJ1rBGkhu85 p{margin:0;}#mermaid-svg-mOkX5BJ1rBGkhu85 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mOkX5BJ1rBGkhu85 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mOkX5BJ1rBGkhu85 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-mOkX5BJ1rBGkhu85 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-mOkX5BJ1rBGkhu85 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-mOkX5BJ1rBGkhu85 .sequenceNumber{fill:white;}#mermaid-svg-mOkX5BJ1rBGkhu85 #sequencenumber{fill:#333;}#mermaid-svg-mOkX5BJ1rBGkhu85 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-mOkX5BJ1rBGkhu85 .messageText{fill:#333;stroke:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mOkX5BJ1rBGkhu85 .labelText,#mermaid-svg-mOkX5BJ1rBGkhu85 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .loopText,#mermaid-svg-mOkX5BJ1rBGkhu85 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mOkX5BJ1rBGkhu85 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-mOkX5BJ1rBGkhu85 .noteText,#mermaid-svg-mOkX5BJ1rBGkhu85 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-mOkX5BJ1rBGkhu85 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mOkX5BJ1rBGkhu85 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mOkX5BJ1rBGkhu85 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mOkX5BJ1rBGkhu85 .actorPopupMenu{position:absolute;}#mermaid-svg-mOkX5BJ1rBGkhu85 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-mOkX5BJ1rBGkhu85 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mOkX5BJ1rBGkhu85 .actor-man circle,#mermaid-svg-mOkX5BJ1rBGkhu85 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-mOkX5BJ1rBGkhu85 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ⏱ LatencyToxic 持有响应数据 等待 15000ms 10秒后,requests库 ReadTimeout tenacity 接住异常,触发重试 5秒后才会真正放行响应 但客户端已断开,数据被丢弃 POST /charge (建立TCP连接) 转发请求 响应(<10ms 完成) ⏱ 等待中... 主动关闭 TCP

发起订单,请求失败,实践为21 秒

bash 复制代码
$ time curl -X POST http://localhost:5000/api/orders \
  -d '{"user_id":"user_retry","items":[{"product_id":"PROD-001","quantity":1,"price":128}]}'

{"error":"RetryError[<Future at 0x7f8641affe30 state=finished raised ReadTimeout>]"}

real    0m21.032s

完整重试时间线(应用日志):

  1. tenacity 配置 stop_after_attempt(2) 表示总共 2 次(不是"额外重试 2 次")

  2. 总耗时 = 单次超时 × 尝试次数 + 退避总和

  3. 用户感知:21 秒后才看到失败------这往往比快速失败更糟糕

    06:14:00.000 [INFO] Creating order ORD-... for user user_retry
    06:14:00.005 [INFO] Checking inventory via toxiproxy... [16ms - inventory正常]
    06:14:00.021 [INFO] Processing payment via toxiproxy for user_retry: 128
    06:14:10.025 [ERROR] Payment timeout ← ① 第1次超时(10s)
    06:14:10.526 [INFO] Processing payment via toxiproxy for user_retry: 128 ← ② 退避0.5s后重试
    06:14:20.530 [ERROR] Payment timeout ← ③ 第2次超时(10s)
    06:14:20.531 [ERROR] Order creation failed: RetryError[...] ← ④ 重试耗尽

Timeout Toxic vs Latency Toxic

timeout toxic 不是"无限延迟",而是完全不同的故障语义。理解它能避免混沌实验设计错误。

注入命令与参数解读

bash 复制代码
$ curl -X POST http://localhost:8474/proxies/payment-proxy/toxics \
  -d '{"name":"payment_to","type":"timeout","attributes":{"timeout":0}}'

注入了什么

字段 含义
name payment_to toxic 标识
type timeout 超时丢包类型(注意:不是 latency)
timeout 0 特殊值:永远不触发主动关闭

关键源码回顾 (来自 3.6 节 timeout.go):

go 复制代码
if timeout > 0 {
    // 模式A: 等 timeout 毫秒后强制关闭连接
} else {
    // 模式B: timeout=0,进入"永远丢弃数据"模式
    for {
        select {
        case <-stub.Interrupt: return
        case c := <-stub.Input:
            if c == nil { stub.Close(); return }
            // 静默丢弃所有数据,永不响应,永不主动关闭
        }
    }
}

timeout=0 实际行为

  • 建立TCP连接不受影响,是因为Toxics.StartLink() 在 TCP 握手之后 才被调用。Toxic Pipeline 工作在已建立的 TCP 连接之上 ,处理的是连接内的数据流,而不是连接建立过程本身。

Payment Service Toxiproxy (timeout:0) App Payment Service Toxiproxy (timeout:0) App #mermaid-svg-KnMLZXRSbqlB8qLb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KnMLZXRSbqlB8qLb .error-icon{fill:#552222;}#mermaid-svg-KnMLZXRSbqlB8qLb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KnMLZXRSbqlB8qLb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KnMLZXRSbqlB8qLb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KnMLZXRSbqlB8qLb .marker.cross{stroke:#333333;}#mermaid-svg-KnMLZXRSbqlB8qLb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KnMLZXRSbqlB8qLb p{margin:0;}#mermaid-svg-KnMLZXRSbqlB8qLb .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KnMLZXRSbqlB8qLb text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KnMLZXRSbqlB8qLb .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-KnMLZXRSbqlB8qLb .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-KnMLZXRSbqlB8qLb #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-KnMLZXRSbqlB8qLb .sequenceNumber{fill:white;}#mermaid-svg-KnMLZXRSbqlB8qLb #sequencenumber{fill:#333;}#mermaid-svg-KnMLZXRSbqlB8qLb #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-KnMLZXRSbqlB8qLb .messageText{fill:#333;stroke:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KnMLZXRSbqlB8qLb .labelText,#mermaid-svg-KnMLZXRSbqlB8qLb .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .loopText,#mermaid-svg-KnMLZXRSbqlB8qLb .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-KnMLZXRSbqlB8qLb .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-KnMLZXRSbqlB8qLb .noteText,#mermaid-svg-KnMLZXRSbqlB8qLb .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-KnMLZXRSbqlB8qLb .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KnMLZXRSbqlB8qLb .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KnMLZXRSbqlB8qLb .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-KnMLZXRSbqlB8qLb .actorPopupMenu{position:absolute;}#mermaid-svg-KnMLZXRSbqlB8qLb .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-KnMLZXRSbqlB8qLb .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-KnMLZXRSbqlB8qLb .actor-man circle,#mermaid-svg-KnMLZXRSbqlB8qLb line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-KnMLZXRSbqlB8qLb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 连接建立成功(toxic不影响TCP握手) 🗑️ 数据被丢弃 (Drop on the ground) 请求永远不会到达 Payment Service 应用等待响应... TCP 层因双方都不发数据 OS 触发 RST 或 keepalive 超时 ⚡ 立即收到 RemoteDisconnected 不到 30ms ① 建立 TCP 连接 ✅ ② 发送 HTTP 请求数据 TCP RST / Connection Reset

对比三种"超时"故障模式

注入方式 应用看到的 触发时间 应用日志特征
latency: 99999999(接近无限延迟) ReadTimeout 应用层 timeout 触发(如 10s) "Timeout while reading"
timeout: 0(本实验) ConnectionError TCP RST 立即返回(~30ms) "RemoteDisconnected"
timeout: 5000 先发数据,5s 后被强制断 5 秒 "Connection aborted"

生产场景对应

  • latency 极大值 → 模拟"服务僵死,慢得像挂了"
  • timeout: 0 → 模拟"防火墙黑洞、负载均衡器丢包"
  • timeout: N → 模拟"主动断连,如 Nginx upstream timeout"

结果:

bash 复制代码
$ time curl -X POST http://localhost:5000/api/orders ...
{"error":"payment failed"}
real    0m0.036s

应用日志:

  • timeout=0 让 toxic 进入"永远丢弃数据"模式

  • 但 toxic 自己不主动关闭连接

  • 客户端发请求 → 数据被丢 → TCP 层最终因为 keepalive 或 buffer 满而 reset

  • 表现为 RemoteDisconnected不触发 ReadTimeout

    06:15:00.438 [INFO] Creating order ORD-...
    06:15:00.460 [WARNING] Payment failed:
    ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

对比表

Toxic 应用看到的错误 触发时间 tenacity 是否重试
timeout ConnectionError / RemoteDisconnected 立即 (~30ms) 是(ConnectionError 在重试列表)
latency: 15000 Timeout / ReadTimeout 单次超时 (10s) 是(Timeout 在重试列表)

Toxiproxy API 速查

bash 复制代码
# 代理管理
GET    /proxies                         # 列出所有代理
POST   /proxies                         # 创建代理
GET    /proxies/{name}                  # 查看代理
POST   /proxies/{name}/enable           # 启用
POST   /proxies/{name}/disable          # 禁用
DELETE /proxies/{name}                  # 删除

# Toxic 管理
POST   /proxies/{name}/toxics           # 添加 toxic
GET    /proxies/{name}/toxics           # 列出 toxics
POST   /proxies/{name}/toxics/{tname}   # 修改 toxic
DELETE /proxies/{name}/toxics/{tname}   # 删除 toxic

# Toxic 通用参数
{
  "name": "my_toxic",          # 唯一标识
  "type": "latency",           # toxic 类型
  "stream": "downstream",      # 方向 upstream/downstream
  "toxicity": 1.0,             # 触发概率 0.0-1.0
  "attributes": {...}          # toxic 特定参数
}
相关推荐
雪之下雪乃的代码日记1 小时前
认识Java中集合框架
java·开发语言·笔记
少司府1 小时前
C++进阶:继承
c语言·开发语言·c++·继承·组合·虚继承
郝学胜-神的一滴1 小时前
CMake 012:Linux 下动态库与可执行程序的单文件构建
linux·服务器·开发语言·c++·软件构建·cmake
江屿风1 小时前
C++图的基本概念流食般投喂-竞赛编
开发语言·数据结构·c++·笔记·算法·图论
独自破碎E1 小时前
SLKJ笔试题解析
java·开发语言
之歆1 小时前
Day23_Bootstrap 前端框架完全指南:从栅格系统到组件化开发
开发语言·前端·javascript·前端框架·bootstrap·ecmascript·less
AI玫瑰助手10 小时前
Python函数:默认参数的定义与注意事项
开发语言·python·信息可视化
油炸自行车10 小时前
Claude Code 错误:API Error: 400 Failed to deserialize the JSON body into the
开发语言·javascript·json·trae·claude code·api error 400
肩上风骋10 小时前
C++14特性
开发语言·c++·c++14特性