FunTester 一次超时如何演变成平台级故障

FunTester · 2026年07月01日 · 250 次阅读

无界重试和自动扩缩容会把轻微延迟放大成级联故障。API 可靠性必须有边界,并且要感知负载,才能避免重试风暴。

现代 API 主导架构通常围绕韧性构建。我们会加入面向瞬时故障的重试、面向持久性的复制、面向弹性的自动扩缩容,以及面向隔离的熔断器。单独看,每一种机制都在提高可用性。

问题在于,系统进入压力状态后,这些机制不再是孤立动作。它们可能同时响应同一个异常信号,把小延迟放大成大流量,把局部退化推成全局故障。

多数企业级故障并不是因为缺少容错能力,而是因为容错机制没有边界,最终一起把系统推向失稳。

我们来拆解这个过程,以及如何把可靠性设计成有界、可控、能降级的机制。

重试风暴:韧性机制放大流量

重试本来是为了应对临时故障。但一旦没有边界,重试就会直接放大负载。

下面是一段服务间调用中很常见的简化重试逻辑:

import time
import random

def downstream_service():
    # 用随机延迟模拟下游服务的响应波动
    latency = random.choice([0.1, 0.2, 0.8])
    time.sleep(latency)
    if latency > 0.7:
        raise TimeoutError("Slow response")
    return "OK"

def call_with_retries(max_attempts=3):
    for attempt in range(max_attempts):
        try:
            return downstream_service()
        except TimeoutError:
            # 每次超时都会立即再次请求下游
            print(f"Retry {attempt+1}")
    raise Exception("Failed after retries")

在正常情况下,这段代码运行良好。下游偶尔慢一次,上游再试一次,业务看起来更稳。

高负载下就不一样了。延迟升高,超时被触发,每个请求最多重试 3 次,流量可能扩大到 3 倍。后端进一步变慢,更多请求继续超时,更多重试被触发。于是,原本用来兜底的机制变成了新的压力源。

这就是重试风暴。

再把它放到 API 主导架构中看:

网关 → 体验 API → 流程 API → 系统 API → ERP/DB

如果每一层都独立重试,负载放大会从加法变成乘法。

在我参与过的一个系统中,一次下游轻微变慢在几分钟内拖垮了 3 个上游 API。直接原因不是某一层没有重试,而是每一层都有自己的重试逻辑。

有界重试模式

安全的重试至少要满足 4 个条件:有限制、带指数退避、带抖动,并且在系统压力下可以被禁用。

更安全的版本:

def call_with_bounded_retries(max_attempts=2, system_load=0.5):
    # 系统已经处于高负载时,优先快速失败,避免继续加压
    if system_load > 0.75:
        return None

    for attempt in range(max_attempts):
        try:
            return downstream_service()
        except TimeoutError:
            # 指数退避拉开重试间隔,抖动避免所有请求同步重试
            backoff = 0.2 * (2 ** attempt)
            time.sleep(backoff + random.uniform(0, 0.1))
    return None

这段逻辑的关键差异在于:

  • 降低重试上限
  • 使用指数退避
  • 通过抖动避免同步波峰
  • 基于负载快速短路

重试应该抑制不稳定,而不是放大不稳定。它要解决的是短暂抖动,不应该在系统已经吃紧时继续制造新流量。

复制扇出与协调崩塌

复制能提升持久性,但同步复制会提高协调成本。

示例:

import time

def simulate_write():
    # 模拟一次副本写入的固定耗时
    time.sleep(0.2)

def write_to_replicas(data, replicas=3):
    # 每次业务写入都会被放大为多次副本写入
    for _ in range(replicas):
        simulate_write()

突发流量下,写入量增加,每次写入又扇出到 3 个副本。副本延迟增加后,客户端可能继续重试写入,有效写入负载再次上升。此时,原本提供持久性的复制,反而变成吞吐瓶颈。

在订单处理、计费、对账这类企业集成系统中,这种模式很容易导致吞吐崩塌。问题不一定是数据丢了,而是协调开销把系统压垮了。

分层持久性策略

并不是所有写入都需要相同的保证。

def write(data, critical=True):
    if critical:
        # 关键交易走强持久性,优先保证一致性和可恢复性
        write_to_replicas(data, replicas=3)
    else:
        # 非关键事件降低复制成本,避免挤占核心链路资源
        write_to_replicas(data, replicas=1)

可以拆开处理:

  • 关键交易 → 强持久性
  • 非关键日志/事件 → 降低协调成本

可靠性必须按范围设计,而不是盲目最大化。对所有写入都使用最高规格,听起来安全,实际可能让关键链路更早进入拥塞。

自动扩缩容反馈环

自动扩缩容会根据流量指标做出反应,但流量指标不一定都代表真实需求。

如果重试抬高了请求数:

def autoscale(request_rate):
    # 只看请求速率,可能把重试流量误判为真实增长
    if request_rate > 100:
        print("Scaling up")

扩容会触发新实例初始化。新实例初始化又会访问共享 DB、缓存、配置中心或依赖服务。后端延迟继续升高,更多超时出现,重试率继续上升。于是,自动扩缩容不但没有止血,反而加速了不稳定。

更安全的扩缩容信号

扩容应该基于持续需求,而不是瞬时尖峰;应该观察延迟分布趋势、有机 RPS(排除重试)和队列增长速度。

示例:

def autoscale_safe(request_rate, sustained_load):
    # 只有确认是持续负载时,才触发扩容动作
    if sustained_load and request_rate > 120:
        print("Scaling safely")

自动扩缩容应该响应真实需求,而不是响应重试放大后的噪声。否则扩容动作本身也会加入反馈环。

真正的问题:相关性反应

重试响应延迟,复制响应写入,自动扩缩容响应流量,熔断器响应错误率。

在压力下,它们都在响应同一个信号。这种相关性会制造级联故障。分布式系统本质上像反馈系统一样工作,无界反馈环会让系统失稳。

支付对账 API 场景

考虑一个支付对账服务:

网关 → 流程 API → 计费 → ERP → 数据库

一次轻微的 ERP 变慢会发生什么?

  • ERP 延迟升到 700ms
  • 计费在 500ms 超时
  • 计费重试 3 次
  • 流程 API 重试编排
  • 网关重试客户端请求
  • 自动扩缩容响应流量尖峰
  • DB 复制延迟增加
  • DLQ 开始增长

几分钟内,一个小变慢变成了平台级事故。

根因不是某一个组件坏了,而是所有保护机制都在无边界地反应。

有界可靠性的护栏

重试预算

有效负载 = 入站 RPS × 重试次数

如果 RPS = 1,000,重试次数 = 3,那么有效负载 = 3,000。

要限制每个请求、每个服务的重试次数。更进一步,还要限制某个调用链、某个租户或某个业务场景里的总重试预算,避免局部故障吃掉全局容量。

故障分类

并非所有错误都适合重试。

错误类型 是否重试 动作
CONNECTIVITY 有界重试
TIMEOUT 退避
VALIDATION 快速失败
AUTH 告警

盲目重试是一种架构债。连接失败和短暂超时可以有限重试,参数校验失败、权限失败这类错误应该快速失败。否则,重试只是在重复提交必然失败的请求。

幂等性保障

没有幂等性的重试会造成数据破坏。

不安全:

# 每次重试都会生成新的交易 ID,可能导致重复写入
transaction_id = uuid()

安全:

# 使用请求携带的稳定 ID,让多次重试落到同一个逻辑事务
transaction_id = payload.get("transaction_id") or request.headers["correlation-id"]

每次重试都必须产生相同的逻辑结果。没有幂等键的写接口,不应该轻易开放自动重试。

带可观测性的 DLQ

需要跟踪重试比例、超时频率、DLQ 增长速度和 P95 延迟变化。这些都是早期预警信号。

这些控制都不是免费的。减少重试在某些场景下会提高错误率,限制复制也可能影响持久性保证。目标不是消灭这些机制,而是基于系统行为有意识地使用它们。


相关阅读

##### FunTester 名片|万粉千文,百无一用

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册