← 中文资源

简体中文版本

超时预算

超时预算是在请求链路上共享的截止时间,每一跳只使用剩余时间,重试也不能超过调用方的耐心。

阅读英文原文

一个截止时间,而不是许多超时

每层独立设置固定超时表面安全。五层调用各 10 秒,可能让请求存活 50 秒,远超用户早已关闭页面的时间。上层先超时,下层却继续工作,占用连接和容量,产出无人读取的结果。

超时预算反过来计算。边缘设置截止时间,例如 2 秒。处理器调用依赖前先减去已经花费的时间,并传递剩余预算。只剩 1.6 秒时,下一次调用获得 1.6 秒减去安全余量。预算耗尽时,处理器停止并返回超时响应,不再启动新工作。

export function remainingMs(deadline: number): number {
  return Math.max(0, deadline - Date.now());
}

export async function callWithinBudget<T>(
  deadline: number,
  operation: (signal: AbortSignal) => Promise<T>,
): Promise<T> {
  const budget = remainingMs(deadline);
  if (budget < 50) throw new Error('request budget exhausted');

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), budget);
  try {
    return await operation(controller.signal);
  } finally {
    clearTimeout(timer);
  }
}

截止时间应放在请求上下文中传递。Node.js 中接受调用方的 AbortSignal,再与本地定时器组合;Go 中使用派生自入站 context 的 context.WithDeadline。不要在中间层新建后台 context,否则截止时间会丢失。

重试也要消耗预算

重试不是免费的。三次重试、每次两秒,加上退避,最坏情况远超六秒。只有剩余预算能容纳时,这种策略才成立。只剩 80 毫秒时开始重试,无法在合理时间内成功,也不应执行。

预算感知的重试在每次尝试前检查截止时间,并把单次尝试超时限制在剩余时间内。它还要区分可重试失败和调用方取消。如果调用方已经取消,重试只是浪费,并可能掩盖取消。

传播取消

只让调用方停止等待不是取消。远程服务可能继续处理,客户端可能仍持有连接。应把取消信号传播到 HTTP 请求、数据库驱动和消息消费者。记录工作停止的原因:截止时间、调用方取消或关机。不同原因对应不同指标和响应。

权衡

较短预算保护延迟,但增加部分结果和重试的概率;过长预算掩盖依赖问题并占用资源。测量端到端延迟分布,从产品容忍度设定边缘预算,再为每一层分配比例并留出序列化和网络抖动余量。调用图变化时重新审视预算;新增一跳却没有传播预算,会静默破坏这个不变量。

何时使用

当请求跨多个服务或启用重试时,使用超时预算。优点是端到端延迟可预测:调用方截止时间是唯一需要关心的限制。缺点是每一层都必须接受并转发截止时间,需要共享上下文管道。与独立的逐跳超时相比,预算实现更复杂,但避免了隐藏的累加延迟。