超时、重试、限流与熔断:依赖变慢时别把自己拖垮
一个慢支付接口可能占住所有请求线程和连接,让原本不依赖支付的查询也失败。可靠性设计不是保证外部永远正常,而是限制坏依赖的影响,并让调用结果仍然可解释。
1. 给等待设置总预算
连接超时、读取超时和整个操作的 deadline 不是同一件事。某些读取超时限制的是相邻数据之间的等待,服务端一直慢慢返回内容就可能持续很久。端到端总时限需要单独考虑。
例如入口给一个读取操作 800ms,其中连接和首次尝试最多 300ms、重试等待最多 100ms、第二次尝试最多 300ms,剩余留给编码与网络。数值是练习假设,不是通用生产推荐。下游拿到的 deadline 应递减,而不是每一层都重新获得完整 800ms。
2. 只有适合的失败才重试
短时网络故障、部分过载响应可能值得重试;参数错误、权限错误和确定的业务拒绝通常不应重复发送。写操作在结果未知时先考虑幂等键和状态查询。
指数退避让连续失败后的等待变长,jitter 加入随机性,减少大量客户端同时再次冲击服务。下面只计算 full jitter 等待值,可运行,不执行网络请求。
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、活动连接和排队量。只有在这些指标下仍能维持边界,才说明隔离有实际作用。
参考资料
- AWS Builders Library:Timeouts, retries, backoff and jitter
- Microsoft:Circuit Breaker
- Microsoft:Bulkhead
一次请求的预算之外,还要看整体重试量
单次请求最多重试两次,看似有上限;如果所有请求同时遇到过载,下游流量仍可能被放大。除了每次请求的次数和总时限,还可以限制一段时间内重试占全部调用的比例,超过预算就把失败返回上游。
练习中让依赖持续拒绝请求,分别记录原始请求数、实际尝试数和排队长度,再关闭多层重复重试进行比较。Google SRE 给出了每请求和每客户端预算的案例;文中的具体阈值属于其系统经验,不是所有服务应照抄的默认参数。
本节的核验依据
2026-10-25 查阅。下面的链接支持本节的机制说明;练习场景和选型建议是本教程的教学设计。