Uber公布重试风暴防御方案:错误归属把链路放大倍数从8倍压到1.33倍

AI 画家正在创作中

9月17日,Uber工程博客发表技术文章《How Uber Protects Against Retry Storms》,由高级主任工程师Deepanshu Mehndiratta、首席工程师Alok Srivastava和高级软件工程师Vibhor Dhingra联合署名。文章直指微服务架构里一个被低估的失效模式:单个深层服务的故障,会通过重试配置在整条调用链上被指数级放大,最终从局部故障演变成全站事故。Uber给出的核心判断是,现有的重试配置和重试预算都是手工设置的,既看不到跨服务的放大效应,也无法区分服务产生的错误和服务只是传递的错误。

文章先用一个最简模型说明放大倍数。假设一条A到G的调用链每跳都配置重试一次,也就是一次正常请求加一次失败重试,当D节点开始报错时,请求量沿链路递增:B承接2倍于入口的请求,C承接4倍,D到G各承接8倍。把每跳的请求倍数记为R、节点深度记为d,故障传播时最深处节点承接的请求数就是R的d次方乘以入口流量N。真正的危险在于重试行为不具备上下文感知能力,系统能控制重试多少次,却控制不了什么时候重试。

引入重试预算可以显著压低这个倍数。如果每跳把重试比例限制在10%,公式变成1.1的d次方乘以N,链路深处节点承接量从8倍降到约1.33倍。Uber还算了一笔可用性账:当被调用方基础可用性为99.9%时,10%重试预算能把调用方感知的可用性拉到99.9999%;99%时拉到99.99%;95%时拉到99.75%。但当基础错误率超过10%之后,重试收益开始倒挂,80%可用性的服务加重试后感知可用性只有88%,70%的服务只有77%。

错误归属:区分病因与症状

Uber给出的解法叫错误归属(Error Ownership),核心逻辑是用症状与病因的类比界定责任。一个服务为完成一次请求调用了N个下游,如果某个下游报错导致它也返回错误,那么它返回的错误只是症状,真正的病因在那个下游;反过来,如果所有下游都没报错而它自己返回了错误,那它就是病因,也是这个错误的归属者。这套判定依赖Uber内部的服务依赖分析方案,用来把入站失败与出站失败做关联。

机制在协议层面靠错误声明头传递。决策矩阵规定了调用方的行为:被调用方声明了错误,调用方就不再重试而直接向上传递;被调用方没带声明头,第一个发现缺失的节点就把错误取消声明,从而把重试扰动的爆炸半径限制在局部,同时仍允许对出错服务本身的重试。文章用三个节点的例子说明传播规则:B重试C失败后向A返回错误时必须取消声明,A看到取消声明头就不再重试B,重试风暴因此不会继续向上蔓延。

Uber也承认这套体系并不完美。现实里存在巧合错误,某个节点自身因为缓存或数据库过载产生10%的错误率,而它的两个容错型下游也各自产生10%的错误率,两类错误同时出现时,仅靠服务依赖分析无法判断到底谁是病因。对于上下文传播不完整、或者下游根本没有接入依赖分析方案的非合作环境,Uber的策略是保守处理,宁可牺牲一部分重试机会,也不让放大效应失控。

社区讨论把这套思路和成熟实践做了对照。有开发者指出,把重试预算限制在正常流量1%的本地令牌桶就能消除长期重试风暴;也有人提到gRPC里Google的RetryInfo协议已经实现了类似的可重试提示;还有人建议给请求加截止时间预算,把剩余时间通过HTTP头传给下游。Uber没有声称错误归属是银弹,而是把它定位为共享基础设施层面的机制,让重试决策从每个服务各自配置变成基于跨服务上下文的条件判断。