服务治理——熔断

熔断是兜底杀手锏之一,在一个远程调用框架中,一个服务接口被调用时,如果出错应该优先采取重试的方案,如果多次超时——表明服务确实不可用之后——才会考虑熔断,以避免“灾害”进一步的扩大。
熔断本身没有特别难的算法,但是需要考虑比较多的细节。

一、熔断器的实现原理

常见的实现如:

Dubbo

Dubbo 中的熔断机制比较简单,总的来说,就是在正常调用失败后,直接调用由用户指定的一个回调方法来计算一个默认值。

Hystrix

Hystrix 中的实现相对来说会更复杂一些,主要包括降级和恢复两个过程,原理和 Martin Fowler 的文章中的描述比较接近。

Resilience4j

TODO

Sentinel

TODO

二、熔断器的应用

熔断粒度

服务一般是复杂的、有许多接口的,而且调用不同接口形成的链路结构可能并不一样,当某个接口出现故障时,很有可能并不影响其他接口的可用性,比如对下面两个链路:

  • 服务 A 接口 1 -> 服务 B 接口 1
  • 服务 A 接口 2 -> 服务 C 接口 1

如果服务 B 的接口 1(以下使用 B1 简称)不可用了,会导致 A1 不可用,但不影响 A2。
因此以接口作为熔断粒度更合适,服务熔断其实熔断的是服务中有问题的那部分接口。

添加熔断的位置——上游 Or 下游

正如之前所示,一条服务链路可能是复杂的、多级的,如下图所示:
链路示例

如上图所示,熔断规则的添加方式可以有以下 3 种:

  1. 仅在 A1 和 A2 上添加熔断规则。如果 B1 不可用了,会触发 A1 的降级,但是 A2 并不知道,于是又会对 B 发起无用的调用。其实当下游的某个服务不可用之后,以该服务为下游的所有链路其实都报废掉了,所以这里在 A1 已经试出 B1 的问题后 A2 再去请求已经没有意义了。
  2. 仅在 B1 上添加熔断规则。此时如果 A1 不可用了,仍然会导致请求超时的现象,影响用户体验。
  3. 同时都加上熔断规则。这样其实会在熔断恢复时引起一点小麻烦:如果 A1 率先进入 Recovering 状态,此时 B1 仍不可用,反过来又会把 A1 带回到降级状态,直到 B1 进入 Recovering 状态;如果 B1 率先进入 Recovering 状态则不会有这样的问题。

总而言之,在为上下游服务接口添加熔断规则时,能都加则都加,如果不能则尽量在容易出错的接口处添加熔断规则。

熔断与服务发现是一对搭档——下游有多个实例的情况

在分布式系统里,我们默认网络是不稳定的、容易发生抖动的,有时候请求一时不能正常地打到目标服务器上,但是可以通过重试来达到目的,一般这样的情况会比较少,所以我们会定一个调用成功率来决定是否开启熔断模式将这条链路降级,那么这个成功率应该怎么设定呢?
我们先考虑最简单的情况——下游服务只有一个实例,如果这个实例挂掉,那么对其的请求就都不好使了,因此调用成功率可以设置得比较大,比如约80% ~ 90% / min的一个数。
下游多实例的链路
但是服务为了保证可用性,怎么可能只有一个副本?如上图所示,下游服务有多个副本的情况下,如果其中有一个副本出了问题,经过服务发现组件的刷新(Watch 机制),A 能够“感知”到下游服务结构的变动、忽略掉已经不可用的实例,当然这个“感知”有一定的延迟时间,如果我们容忍这样的延迟,那么调用成功率的值也可以相对设置得大胆一些,比如约60% ~ 70% / min;反过来说,如果我们希望熔断器更严格,则会导致熔断器将整个下游服务的流量阻断掉,这是一个需要权衡的设计问题。

异步请求 Or 同步请求

服务接口可以分为同步的和异步的,我们说的服务不可用其实都发生在同步调用中,而异步请求一般都是非阻塞的,不可能出现超时现象。

异步请求一般通过消息队列来实现,消息队列可以持久化消息,并通过不断重拾来实现至少一次的消费,因此异步请求不会影响链路的可用性。
异步和非阻塞没有必然关联,异步表达的是一种“被动的”通信模式(由操作系统来通知应用程序请求已返回),非阻塞指的是调用方法会立刻返回,换句话说,我们完全可以实现一种异步阻塞的 IO 模型。

开关熔断

开关是实现熔断的方式之一,主要包括版本切换开关、调参配置开关、灰度流量开关。

  • 版本切换开关
    新版本上线,上线如果发生问题,一个解决方法是:回滚代码。线上服务由多台机器组成,滚动回滚是需要较长的时间的。一般来说需要几分钟到几十分钟不等。更有效的方法是在编码阶段对于改动都设置开关,出现问题立即切换到老版本。
    需要注意消除临时代码,临时代码会带来维护上的困难,一般运行两周待版本确认稳定后要将切换开关及原来的老版本代码下线。
  • 调参配置开关
    举一个场景:mysql 数据库经常是被认为非常稳定的基础设施,甚至有的团队在做架构设计的时候原则是:消息中间件挂了,我们需要直连降级;缓存挂了,我们降级直接走持久层;但是如果 mysql 挂了,就是真的挂了。
    mysql 挂的场景确实不多见,常见的情况是我们自己没有用好。比如:容量没有做合理预估、建立物理连接时长不合理。
    随着线上服务器功能增加、QPS 升高,对数据库压力增大,有可能造成预估或者测算出合理的参数不再合理,为了应对突发问题,最好将数据库连接配置放到动态配置管理中。
  • 灰度流量开关
    大功能上线一般需要灰度,以免不符合预期造成较大损失。一个建议的策略是如果本身 QPS 较高,那么可以按照 SLA(Service-Level Agreement 服务等级协议)可允许的错误预算来设置灰度粒度。比如,系统从年初一直运行良好,没有出现过问题,这次要上线了,对外承诺 SLA3 个 9。那么第一次灰度的流量可以按系统 0.1%来灰度,那么就算出现问题了,三天内可以恢复,也可以保证我们的 SLA。

重试

请求出错除了因为服务器不可用外,大部分情况下都是由于网络引起的,现实中网络情况受很多条件影响,比如用网高峰期带宽被占用、运营商升级系统等,重试可以减少网络抖动的影响,减少服务出错被误报的可能性。
立即重试可能不太合适,这样会在短时间内对依赖服务产生大量请求,可能本来只是网络抖动,结果疯狂重试造成了服务器崩溃,得不偿失。增加暂停时间将有助于缓解这种情景,有下面几种计算等待时间的策略:

  • 指数: 基数 * 2^尝试次数
  • 全抖动: 休眠时间 = rand(0 , 基数 * 2^尝试次数)
  • 等抖动: 临时 = 基数 * 2^尝试次数; 休眠时间 = 临时 / 2 + rand(0, 临时 / 2)
  • 不相关抖动: 休眠时间 = rand(基数, 休眠时间 * 3);

重试策略

参考

  1. 漫画:什么是服务熔断?
  2. CircuitBreaker - Martin Fowler