Skip to main content

参考:韧性模式

一份关于韧性模式的参考——超时与截止时间、带抖动和预算的重试、熔断器、舱壁、限流降载与优雅降级——以及重试为什么会放大负载。

它是什么

在分布式系统里,依赖变慢或宕机的频率,远高于调用它们的代码所假设的,而一个慢的依赖比一个死的依赖更危险:它会占住线程和连接,直到调用方也跟着失败。韧性模式把故障限制住,不让它级联:超时限定等待的时间,重试(带退避和抖动)扛过暂时性的错误,熔断器停止调用一个正在失败的东西,舱壁(bulkhead) 隔离资源,限流降载和优雅降级让系统在过载时仍然有用。

Important

核心要点: 每个调用都需要一个截止时间(deadline)。重试需要退避、抖动和预算,并且只对幂等的操作有效。用舱壁和熔断器隔离依赖,让一个慢的服务不能拖垮其余的,并且事先决定依赖消失后系统要做什么:降级,而不是失败。

何时需要它

“这个下游服务变慢或宕机时会怎样?“是几乎每个微服务设计的追问,而强的回答所说的不止”重试”。

Note

面试信号: 说出重试会放大负载,并给出算术(三层各自重试三次,在最底层就是 27 次调用),说明你有经验。再补上抖动、重试预算和”只重试幂等的调用”,就完整了。

超时与截止时间

没有超时,对一个卡住的依赖的调用会永远等下去,占住一个线程。每个调用的超时限制了它。更好的是,在边缘设置一个截止时间,并沿着调用链传播,每一跳都从同一份预算里花费,这样最后一跳就不会继续为一个第一跳早已放弃的请求干活。要根据测得的延迟(略高于 p99)来设置超时,而不是猜。

算例:一个慢依赖如何耗尽线程池。 一个服务有 100 个工作线程,它的依赖通常 100 ms 内应答,所以它的容量是 100 ÷ 0.1 = 每秒 1,000 个请求(Little 定律)。依赖变慢到 2 秒:容量降到 100 ÷ 2 = 每秒 50 个请求。如果仍以原来的每秒 1,000 个请求到达,线程池在不到一秒内就满了,每个请求都在等待,服务看起来像死了,而它其实是健康的。一个 200 ms 的超时本可以把容量保持在每秒 500 个请求,并快速失败。

重试

对暂时性的失败(超时、503、连接重置)重试,对永久性的失败(400、授权失败)绝不重试。

  • 指数退避: 每次失败后等待更长时间(例如 100 ms、200 ms、400 ms,设上限)。
  • 抖动(jitter): 让等待时间随机化(sleep = random(0, min(cap, base × 2^attempt))),这样许多一起失败的客户端不会在同一时刻重试(一个会让正在恢复的服务再次崩溃的**惊群**)。
  • 重试预算: 把重试限制在正常流量的一个比例(比如 10%),或每个请求的最大次数,这样重试不会让负载成倍增加。
  • 幂等: 一个被重试的非幂等调用(一笔支付)可能执行两次,所以只重试幂等的操作,或使用幂等键。
  • 只在一个地方重试。 如果每一层都重试,放大效应是乘法的。

算例:重试放大。 一个请求经过三层,每层对失败的调用最多总共尝试 3 次(一次尝试加两次重试)。如果最底层的服务宕机,上面一层发出 3 次调用,其中每一次又让下一层发出 3 次:3 × 3 × 3 = 27 次调用打到那个失败的服务上,而这只是因为一个用户请求。一个本来就在挣扎的服务拿到了平时 27 倍的负载,这让它一直起不来。这就是一次短暂的抖动如何变成重试风暴。

熔断器

熔断器包裹对某个依赖的调用并追踪失败。它有三种状态:

状态行为
Closed(闭合)调用放行;统计失败
Open(打开)调用立即失败(或返回一个 fallback),不去碰那个依赖,持续一个冷却期
Half-open(半开)冷却期之后,放几个试探调用过去;成功就关闭熔断器,失败就重新打开

一条典型的规则:在 10 秒窗口内错误率超过 50% 且至少有 20 个请求时打开,保持打开 30 秒,然后探测。打开的熔断器给依赖留出恢复的空间,也让调用方得到一个即时的错误或 fallback,而不是一个缓慢的超时。

舱壁

舱壁隔离资源,使一个地方的故障不能耗尽共享的池,就像船舱隔离进水一样。给每个依赖自己的线程池、连接池或并发限制。如果推荐服务卡住了,它只消耗自己的那个 20 线程的池,而拥有自己线程池的结账服务照常工作。没有舱壁,一个慢的依赖可以耗尽共享的池,让一切都停下来。

限流降载、背压与降级

  • 限流降载(load shedding): 过载时,快速拒绝多出来的请求(503 或 429),而不是把它们排队到全部超时。要区分优先级:先丢低价值或非关键的请求,保护关键路径。
  • 背压(backpressure): 把信号向上游推(一个满了就拒绝的有界队列、一个会放慢的客户端),这样过载不会悄悄地在内存里累积。
  • 优雅降级: 为每种依赖故障定义一个质量较低的响应,比如提供缓存或过期的数据、一份默认的推荐列表,或只读模式,而不是一个错误。
  • 在边缘设置超时、限流和准入控制,把过载挡在深层栈之外。

Warning

生产实践: 亚稳态故障(metastable failure)。 有些系统在触发因素消失之后仍然保持故障:一次短暂的过载造成超时,超时造成重试,重试又维持了过载,系统在没有人工干预(卸载负载、关掉重试、重启)的情况下永远无法恢复。重试预算、熔断器和限流降载,就是阻止一次短暂的事故变成一次漫长事故的东西。

如何选择

问题模式原因
调用可能卡住超时,以及传播的截止时间限制等待和线程占用
暂时性错误带指数退避、抖动和预算的重试扛过抖动而不引发风暴
依赖正在失败带 fallback 的熔断器快速失败,给它留出恢复的空间
一个慢的依赖饿死了所有东西舱壁(独立的池)把破坏限制住
过载限流降载与背压保护系统和关键路径
依赖消失了优雅降级(缓存、默认值、只读)有用胜过要么正确要么报错
一个慢副本拉高了 p99Hedged 请求在第一批响应迟迟不到时,把读发给额外的副本

Caution

常见错误: 加了重试,却没有超时、退避、抖动或幂等。没有超时的重试会永远等;没有退避会猛敲一个正在挣扎的服务;没有抖动会同步化;没有幂等会重复副作用。重试是一个负载放大器,所以要给它设预算。

相关