调用下游时只设一个 3 秒超时,问题往往还没解决:请求可能先在本地队列等了 2.8 秒,拿到连接后又重新获得完整的 3 秒,最后把上游的截止时间和工作线程一起耗光。
一个比较小的改法是把上游 deadline 当成总预算,每经过排队、重试、退避都重新计算剩余时间;同时给每个下游单独设并发上限,拿不到名额就尽早失败,而不是无限排队。伪代码大致这样:
left = deadline - now
if left < 200ms: return budget_exhausted
acquire downstream_slot with timeout min(100ms, left / 5)
if not acquired: return dependency_busy
left = deadline - now
call downstream with timeout left - 100ms
预留的 100ms 用来编码响应和释放资源,具体值按接口尾延迟调整。并发上限也不要只看线程数,最好按下游分别配置;一个慢依赖占满自己的名额时,不应拖住其他依赖。代价是高峰时会更早返回失败,但比请求堆积到全站雪崩更容易恢复。
重试也必须吃同一份预算,只对可安全重试的调用开放。第一次失败后重新计算 left,剩余时间不足一次最小尝试加退避就直接结束,不能每次重试都创建新的完整超时。
验收时可以让一个 stub 分别延迟 50ms、500ms 和持续不返回,再并发压入超过门禁上限的请求。观察队列等待时间、在途数、剩余预算和各类失败数:在途数不能突破上限,超载请求应在门禁等待上限内返回,任何下游调用都不能越过上游 deadline。这样故障隔离是不是生效,基本能从指标和时间边界直接看出来。