超时、重试、限流与熔断:依赖变慢时别把自己拖垮

一个慢支付接口可能占住所有请求线程和连接,让原本不依赖支付的查询也失败。可靠性设计不是保证外部永远正常,而是限制坏依赖的影响,并让调用结果仍然可解释。

1. 给等待设置总预算

连接超时、读取超时和整个操作的 deadline 不是同一件事。某些读取超时限制的是相邻数据之间的等待,服务端一直慢慢返回内容就可能持续很久。端到端总时限需要单独考虑。

例如入口给一个读取操作 800ms,其中连接和首次尝试最多 300ms、重试等待最多 100ms、第二次尝试最多 300ms,剩余留给编码与网络。数值是练习假设,不是通用生产推荐。下游拿到的 deadline 应递减,而不是每一层都重新获得完整 800ms。

2. 只有适合的失败才重试

短时网络故障、部分过载响应可能值得重试;参数错误、权限错误和确定的业务拒绝通常不应重复发送。写操作在结果未知时先考虑幂等键和状态查询。

指数退避让连续失败后的等待变长,jitter 加入随机性,减少大量客户端同时再次冲击服务。下面只计算 full jitter 等待值,可运行,不执行网络请求。

Python
import random import math def retry_delay(attempt: int, base: float = 0.1, cap: float = 2.0) -> float: if (type(attempt) is not int or not 0 <= attempt <= 20 or type(base) not in (int, float) or type(cap) not in (int, float) or not math.isfinite(base) or not math.isfinite(cap) or base <= 0 or cap <= 0): raise ValueError("invalid retry policy") return random.uniform(0.0, min(cap, base * (2 ** attempt))) assert 0.0 <= retry_delay(3) <= 0.8

这里明确拒绝布尔值、小数次数和 NaN/无穷等待参数;Python 类型提示本身不会执行这些校验。attempt=0 表示第一次重试的等待区间,调用者负责统一计数约定。

真正执行时还要限制总次数、总耗时,处理 Retry-After 等协议提示并支持取消。若三层各做三次尝试,一次用户操作可能放大成 27 次底层调用。因此约定由哪一层负责重试,避免叠加放大。

3. 四种措施分别控制什么

措施 控制对象 类比 常见误区
限流 一段时间内的请求速率或配额 门口售票限额 限速不等于限制在途数
并发限制 同时进行的工作数量 只有十张工作台 无界等待队列仍会耗尽内存
熔断 持续失败依赖的调用尝试 暂时关掉坏线路 半开探测也要限制数量
隔离 不同工作共享的资源 分开的工作区域 所有请求共用一个池仍会互相拖累

令牌桶允许一定突发,滑动窗口有另一组精度与成本权衡。分布式限流要明确按用户、租户、IP 还是全局配额,并考虑多节点计数一致性。限流存储自身失效时放行还是拒绝,应由业务影响决定。

4. 降级要诚实

推荐列表可以返回缓存或空列表;订单付款结果不应编造成功。区分“数据稍旧”“正在处理”“服务暂时不可用”,让界面有合适行为。熔断器只能快速拒绝调用,业务层仍要说明拒绝后的结果。

测试时把依赖延迟提高、随机断开连接,再观察其他接口的 p95、活动连接和排队量。只有在这些指标下仍能维持边界,才说明隔离有实际作用。

参考资料

一次请求的预算之外,还要看整体重试量

单次请求最多重试两次,看似有上限;如果所有请求同时遇到过载,下游流量仍可能被放大。除了每次请求的次数和总时限,还可以限制一段时间内重试占全部调用的比例,超过预算就把失败返回上游。

练习中让依赖持续拒绝请求,分别记录原始请求数、实际尝试数和排队长度,再关闭多层重复重试进行比较。Google SRE 给出了每请求和每客户端预算的案例;文中的具体阈值属于其系统经验,不是所有服务应照抄的默认参数。

本节的核验依据

2026-10-25 查阅。下面的链接支持本节的机制说明;练习场景和选型建议是本教程的教学设计。