云寒YUNHAN / FIELD NOTES
← 返回文章目录
系统设计 ·

超时与重试:给分布式请求一个预算

连接超时、首字节与整轮截止时间解决不同问题;重试之前,先问操作能否重复。

不要用一个数字描述所有等待

一次 HTTP 调用包含 DNS、TCP、TLS、等待响应头和读取响应体。连接超时用于限制建连阶段;读取超时通常限制连续两次读取之间的等待,并不一定限制整个响应。流式接口可能持续输出心跳,因此永远不触发读取超时。业务层仍需要一个绝对截止时间。

从调用者向下分配预算

假设入口总预算为 10 秒,验证与响应序列化保留 1 秒,远端调用最多使用 9 秒。下游每次重试都必须使用剩余预算,而不是重新获得完整 9 秒。用单调时钟计算经过时间,避免系统时钟校准导致预算倒退。浏览器 fetch 可以使用 AbortController 实现整个调用的截止取消。

async function getWithDeadline(url, ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);
  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}

取消等待,不等于取消业务

AbortController 只能取消客户端的请求处理,不能保证服务器停止执行。对于扣款、创建订单或发送邮件,网络超时后的结果是未知,而不是失败。重试此类请求前,需要业务侧提供幂等键:同一个操作的多次提交使用同一键,服务端在事务边界内原子记录键与结果。仅在客户端保存一个随机字符串并不能产生幂等性。

重试要稀疏,也要有上限

网络故障、部分 5xx 和 429 可以考虑重试;认证失败和参数错误通常不该重试。指数退避配合 full jitter,可使用 random(0, min(cap, base × 2^attempt))。收到 Retry-After 时应遵守它,同时检查等待是否会超过总预算。多层服务同时重试会放大请求量:三层各重试三次,最坏可产生 27 次底层调用。

用实验验证而不是调大所有数字

用测试服务分别模拟建连失败、响应头延迟、响应体停顿和提交成功后断连。记录每种情况的耗时、尝试次数与业务执行次数。指标至少包含超时阶段、剩余预算和最终结果未知的计数,不要把完整请求体或认证头塞进日志。超时配置的目标是约束资源占用,而不是让错误看起来少一些。

本文为技术原理与示例说明,不代表任何实际生产环境。示例运行前请在隔离环境验证,按具体版本与业务约束调整。
继续探索其他文章 →