- 我翻译的这篇文章参与了掘金翻译计划,选自英语优秀技术文章。
- 原文地址:Fixing retries with token buckets and circuit breakers
- 原文作者:Marc Brooker
- 译文出自:掘金翻译计划
- 本文永久链接:https://github.com/xitu/gold-miner/blob/master/article/2022/retries.md
- 译者:wangxuanni
- 校对者:Quincy-Ye timerring
使用令牌桶和熔断器进行重试
在我发表上一篇关于熔断的文章之后,有人推荐我在中断重试只使用熔断,并且不管失败率仍然发送一次正常的尝试请求。这是一个不错的方法。在客户端熔断(可能会造成资源的浪费)和重试(重试会给已经过载的下游应用增加负担)的关键问题上,这提供了一些可能解决的方案。为了看看效果如何,我们可以将它和我最喜欢的更好重试方法:令牌桶做比较。
首先,正式介绍一下待比较的对象:
- 不重试 。当客户端想发起一个请求的时候,它照常发起一个请求。如果请求失败,客户端不重试并且继续执行。
- N 次重试。当客户端想发起一个请求的时候,它照常发起一个请求。如果请求失败,客户端最多重试 N 次。
- 自适应重试 (又称令牌桶重试)。当客户端想发起一个请求的时候,它照常发起一个请求。如果请求成功,它将一部分令牌放进有大小有限的令牌桶。如果请求失败,只有当桶里有完整的令牌时就重试 N 次( N 为完整令牌的数量)。例如,每次成功请求会存储 0.1 个令牌,每次重试会消耗 1 个令牌。
- 熔断重试。 客户端想发起一个请求的时候,它照常发起一个请求。在成功或者失败时,它会更新(最近)用于记录失败率的数据。如果失败率低于阈值,则最多重试 N 次。如果它高于阈值,不进行重试。
思考
首先,来试着思考一下每种的表现。
不重试是最简单的,如果下游的失败率是 x%,失败率实际就是 x%。
N 次重试次之,如果下游的失败率是 x%,失败率实际是(1-x)N,但是会产生许多额外的工作。当失败率达到100%时,系统的工作量将是原来的 1+N 倍。
分析自适应策略有点困难,但一个大致的想法是:当失败率较低的时候,它会表现的像N次重试;当失败率较高的时候,它表现地类似于“按一定百分比的重试”。例如,如果每个成功的调用都将 10% 的令牌放入桶中,则低于 10% 的失败率的时候自适应行为像N次重试,远高于 10% 的失败率时它就像“ 0.1 次重试”。
熔断策略有些地方是相似的。低失败率(低于阈值)时,它表现的像 N 次重试。高于阈值时,它表现的像不重试。这有点复杂,因为每个客户端都不知道真实的故障率,而是根据对失败率的本地采样(可能与小型客户端的真实失败率有很大差异)做出决定。
由于整个过程是动态的,因此封闭式推理很困难。我们可以通过对服务和客户端的小型事件驱动模拟来观察效果,而不是尝试推理它。我之后会写更多关于模拟的方法,但先从一些结果开始吧。
性能模拟分析
让我们考虑一个具有单个抽象服务的模型,它处理的请求会以一定的比率随机失败。这个服务被 100 个依赖的客户端调用,每一个客户端都以某种速率开始新的尝试。我们关心两种结果:客户端看到的成功率,还有服务器从客户端看见的负载。特别是,我们关注它们如何随失败率变化。
我们可以立刻看到一些符合预期的事,和一些有意思的事。正如预期,不重试不会做额外的工作,并提供随失败率线性下降的可用性。三次重试做了很多额外的工作,提供了最佳的鲁棒性以对抗失败。熔断策略做了额外的工作,在低失败率时提供了额外的鲁棒性,但是超过阈值后下降到和不重试一样。
让我们放大一点到较低的失败率:
我们可以看到策略出现的分叉。第一个有趣的观察结果是,熔断策略比较早开始分叉,大约是预期比率的一半。这是因为每个客户端都是独立熔断的。在低失败率的情况下,自适应策略非常像三次重试,但是慢慢开始出现分叉。
客户端数量的影响
自适应和熔断器方法都依赖于每一个客户端估算的失败率。要么用熔断器失败阈值显式表示,要么用令牌桶的内容隐式表示。当客户端的数量不多的时候,针对每个客户端,可以合理地预估真实的失败率。随着大量客户端发送少量流量,估计值的差异会更大。这在云服务和基于容器的架构中尤其重要,在这种架构中,客户端可能很多但存活时间较短,每个客户端所做的工作相对较少(与多线程实例相比,单个客户端可能会看到大量线程的工作)。
我们可以模拟出在自适应和熔断器策略下客户端数量的影响。在这里,我们会在 10,100 和 1000 台客户端之间分配相同总数的请求。
有趣的是这两种方式有着截然相反的表现。熔断器策略会早早分叉,并接近不重试方法的表现。令牌桶策略(从有一个完整的桶开始)没有足够快地耗尽它的桶,接近 n 次重试的曲线。显然,在每个客户端所知有限的情况下解决重试问题是不完美的。在客户端之间共享状态的模型会改变这些结果。也会明显的增长系统的复杂度(因为客户端需要彼此发现并交流)。
哪一个更好?
选择正确的重试策略取决于我们想要实现的目标。理想的解决方案是无论服务失败率是多少,都不增加额外的负担并且达到成功率 100 %。但这显然是无法实现的,原因很简单:客户端无法知道哪些请求会成功,它们唯一的机制就是不断重试。
除了这样的理想方案,我们能做到什么?大多数应用程序想要的是在服务器失败率较低的情况下具有较高的成功率,而不是过多的额外负载。不重试达不到第一条标准,n 次重试达不到第二条标准。自适应和熔断策略在不同程度上达标。熔断器可以做到在高失败率时不增加额外的负担,但它受到某种形式的影响(它要么重试,要么不重试,并且可能在两者之间来回切换)。自适应策略不是同一种模式,它在低失败率下表现的更好,但是在失败率高的时候会增加额外的(可调节的)负担。
脚注
- 换句话说,每个客户端呈现独立的泊松过程,并且保持它自己的重试状态,这里泊松模型不是非常准确,但也没有关系,因为我们(还)没有对于高负载或者并发情况进行建模。
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。