高可用三大利器 — 熔断、限流和降级

近年来,各大厂Google、微软、阿里、腾讯等都在提高可用的概念。高可用(High Availability,简称HA)是指系统或服务在遭受故障或异常情况时仍能持续提供稳定和可靠的运行能力。

在武侠世界里,“利器”通常指的是武器中的上乘、出色之物;武器对于武者的重要性不言而喻,拥有一把优秀的武器可以让武者在战斗中更加得心应手,威力更强。

在分布式系统追求高可用的背景下,熔断、限流和降级这三个重要的策略可以称得上三大利器。

熔断(Circuit Breaker):熔断是一种防止故障扩散的策略。当一个服务出现故障或超时,熔断器会打开并快速失败,拒绝后续的请求,避免请求堆积和资源耗尽。熔断器会暂时屏蔽该服务,并在一段时间后尝试恢复。熔断器的状态变化可用于监控系统健康和提供告警信息。

限流(Rate Limiting):限流是一种控制系统请求流量的策略。通过设置一个请求速率阈值,限流可以限制每个客户端或用户在特定时间内的请求次数。这样可以防止过多的请求涌入系统,保护系统免受过载和压力冲击。限流可以平滑流量,避免系统突发流量的影响。

降级(Fallback):降级是一种在面对特殊业务或异常情况时保持系统可用的策略。当服务不可用时,降级服务会代替提供一些基本功能或返回预设的默认值,以确保系统依然能够提供有限的功能或服务;又或者某些特定活动场景(例如:双十一)下优先保障计算资源投入到 业务倾向的服务,降级边缘服务。

熔断(Circuit Breaker)

在分布式架构中,一个服务通常会与多个外部服务进行交互,这些外部服务可能是RPC接口、数据库、第三方API等。例如,在支付过程中,可能需要调用银联提供的API;而查询某个商品的价格,则可能需要进行营销活动查询。然而,除了自身服务外,依赖的外部服务的稳定性是无法绝对保证。

当依赖的第三方服务出现不稳定的情况时,例如三方服务器过载,会导致服务自身调用第三方服务的响应时间也变长,甚者形成级联效应。这样一来,服务自身的线程可能会积压,最终可能耗尽业务自身的线程池,导致服务本身变得不可用。

熔断(Circuit Breaker)就是应对这种三方服务不稳定的设计,它可以帮助系统在出现问题时保持高可用,防止故障进一步扩散,同时也能在一段时间后重新尝试恢复正常操作。避免局部不稳定因素导致整个分布式系统的雪崩。作为保护服务自身的手段,通常在客户端(调用端)进行配置。

熔断器模式(Circuit Breaker Pattern),是Michael Nygard在他的著作《Release It!》中开始推荐使用的。其可以防止应用程序反复尝试执行可能会失败的操作,使其能够继续进行而无需等待故障被修复,也无需浪费CPU周期来确定故障是否持久。Circuit Breaker模式还使应用程序能够检测故障是否已解决。如果问题似乎已经解决,应用程序可以尝试调用该操作。

注:这种设计也是典型的 快速失败原则(Fail-Fast Principle) 的应用。强调在面对错误或异常情况时,系统应该尽早地检测并快速失败,而不是继续执行可能导致更严重后果的操作。这个原则的目的是尽早发现问题并及时处理,避免故障进一步扩大,从而提高系统的稳定性和可靠性。

熔断器模式中最关键的设计在于熔断器的三种状态:

  • Closed状态:来自应用程序的请求被 Proxy 操作。Proxy 维护最近故障次数的计数,如果对操作的调用不成功,Proxy 会增加这个计数。如果在给定的时间段内最近故障的次数超过了指定的阈值,Proxy 将进入Open状态。此时,Proxy 启动一个超时计时器,当计时器到达阈值时,Proxy 将进入Half-Open状态。(这里 Proxy 代指 Resilience4j、Sentinel、Hystrix类似框架)

  • Open状态:来自应用程序的请求立即失败,并向应用程序返回异常。

  • Half-Open状态:应用程序允许有限数量的请求通过并调用操作。如果这些请求成功,假定之前导致失败的故障已经修复,将切换到Closed状态(故障计数器被重置)。如果任何请求失败,会认为故障仍然存在,因此它会回退到Open状态,并重新启动超时计时器,为系统提供进一步的时间来从故障中恢复。。

许多开源的框架都基于这三个状态进行了熔断实现的设计,比如:Resilience4j、Sentinel、Hystrix;就实际使用上推荐 Sentinel 和 Resilience4j ,因为 Hystrix 已经宣布不维护了。附上,一张网上广泛流传的对比表格 [1]

开源框架仅仅只是熔断机制的代码实现,更重要的在于结合具体业务常见来设计相关的熔断策略。这里列出一些通常具体业务设计熔断时候的考量点:

熔断异常应该如何处理:三方服务处于在熔断 Open状态下,应该如何进行服务的返回。比如:用一个默认值来替代三方服务的返回结果;返回异常页面告知用户稍后再重试;调用其他的服务来替代原来的功能等。异常往往多样的,也可以考虑不同异常下设置不同的处理方式。

应该记录详细日志:注意异常日志的记录,确保关键信息都写入日志,往往线上故障异常的多样的;好的日志格式/日志设计能够快速的定位问题、监控熔断策略符合预期。熔断状态的转换需要详细写入,方便复盘熔断策略。

是否需要诊断定时程序:当处于熔断 Open状态时,考虑是否需要来做个定时程序 测试三方服务是否恢复并转换 到 Half-Open状态,更灵活的恢复服务。

管理熔断的工具:由于异常是多样的,某些情况下意外触发了熔断;此时管理员可以通过熔断工具来恢复相关状态,应对熔断策略出现问题的情况。

注意三方服务耗时:有时候三方服务能够正常返回但耗时很长,这样可能会导致自身服务的超时;针对这种情况应该进行相关超时熔断处理,应该关注这种隐蔽的超时异常。

限流(Rate Limit)

无论服务器的硬件多么强大,总归也是有限的资源只能处理有限的请求;简单理解限流(Rate Limit)的话,在有限时间内请求数量超过服务的处理数量,自动丢弃新来的请求从而保障有限的请求高可用。

用日常例子来类比说明就很好理解,一个餐厅只有5张四人桌,理想坐满的情况也也就是20个人;而如果涌进来50个人都点单,那么每个人都无法正常的用餐。因此,要限制进来的人数是保障正常用餐。

同样的对于系统而言,一次性接受超出硬件承载的资源,就会导致资源的剧烈竞争从而 导致请求服务的延迟、异常等等,无法提供到高可用的服务。为此,对服务进行限流保护是提供高可用的重要策略了。

以下是常见的限流算法:

固定窗口计数限流算法(Fixed Window Counter):在固定的时间窗口内,限制请求的数量。例如,在1秒内最多允许处理10个请求,当窗口满时,后续请求将被拒绝。

滑动窗口计数限流算法(Sliding Window Counter):设置一个滑动时间窗口,计算在该时间窗口内的请求数量,并限制其在指定范围内。与固定窗口计数算法相比,滑动窗口算法允许更加灵活的流量控制。

令牌桶算法(Token Bucket):令牌桶算法通过将请求放入令牌桶中来控制流量。每个请求需要从令牌桶中获取令牌,如果桶中没有足够的令牌,则请求被拒绝。令牌桶算法允许突发流量一定程度的处理,并平滑了请求的速率。

  1. 令牌桶是一个固定容量的桶,它以恒定的速率产生令牌(即令牌产生速率),并将其放入桶中。
  2. 桶中最大可以保存的令牌数量为桶的容量,当桶满时,多余的令牌会被丢弃。
  3. 每当有请求到达时,如果令牌桶中有足够的令牌,该请求会获取一个令牌,并被处理。如果桶中没有令牌可用,该请求将被延迟或丢弃。
  4. 令牌桶可以应用于固定窗口计数限流算法和滑动窗口计数限流算法。在固定窗口计数限流中,令牌桶以固定速率产生令牌,而在滑动窗口计数限流中,令牌桶按照滑动时间窗口的速率产生令牌。

令牌桶算法的优点在于,它可以平滑地处理突发流量,即使在短时间内有大量请求到达,令牌桶算法仍然能够保持相对稳定的速率来处理这些请求。此外,令牌桶算法还可以允许一定程度的突发流量,因为桶中积累的令牌可以处理突发的请求。令牌桶算法的缺点是在某些情况下可能会导致请求的延迟。如果请求到达时桶中没有足够的令牌,该请求将被延迟等待令牌,可能会导致响应时间增加。

漏桶算法(Leaky Bucket):漏桶算法将请求放入一个漏桶中,请求以恒定的速率从漏桶中流出。如果漏桶已满,则多余的请求将被拒绝。漏桶算法可以用于平滑流量,防止突发请求造成的资源浪费。

  1. 漏桶是一个固定容量的桶,它有一个漏口。桶底的漏口以固定的速率(即令牌产生速率)漏水。
  2. 每个请求都会向漏桶中添加一个令牌。如果漏桶已满(即桶内令牌数量达到了最大容量),则新的令牌会被丢弃。
  3. 当请求到达时,如果漏桶中有可用的令牌,则请求被处理,且漏桶中的令牌数量减少一个。如果漏桶中没有足够的令牌,则请求被丢弃或延迟处理。

漏桶算法的优点在于,它能够以固定的速率来处理请求,从而平滑流量,防止突发请求对系统造成过大的压力。此外,漏桶算法还能够控制流出速率,避免资源的浪费;然而,漏桶算法的缺点是对于突发流量的处理相对较差。如果漏桶中的令牌数量耗尽,那么突发流量的请求会被丢弃,可能会导致某些请求的延迟。

降级(Fallback)

不知道大家是否有经历,当在楼下沙县小店吃面时,如果小店人不多的情况下 店员会端上一碟小菜给到我们;而人很多的情况,如果想吃小菜可以自己去窗口附近用小蝶装取。换个角度,这其实也可以称得上服务降级。

服务降级往往指在面对系统过载、资源不足、有计划的大型活动(双十一),有意识地降低系统的部分功能或服务质量,以保证系统的核心功能和关键服务仍能继续正常运行。

举个例子,电商系统中支持 商家对商品的价格调整 是一种非常常见的功能,但当 双十一零点的时刻 往往会和商家达成一致 不提供在零点后一段时间内商品价格的调整服务,以用来保障零点活动的高效执行。

所以降级是一种非常规应对措施,不应该成为长期的解决方案。一旦面对情况有所改变就应该恢复降级的服务,确保系统能够正常提供全部功能和服务。

降级本身的实现是根据具体业务规则来进行编码,常见的设计有:

开关硬编码: 用一个参数标识是否进行降级,在服务中用 if 来判断标识 从而进行相关服务降级的业务逻辑。如果时间很短情况下要实现降级,可能是最直接最常见的选择。

AOP拦截:通过 AOP切面面编程拦截服务请求,并更加服务降级的业务要求条件,调用降级服务请求。因为使用到了 AOP切面技术,代码侵入性小,但代码可读性差;如果没有一些注释说明,不熟悉相关业务的研发者忽略了降级拦截。

策略/工厂设计模式:在服务设计的时候采用工厂 或者 策略的设计模式,根据降级业务要求条件来进行策略/工厂的具体服务生成,从而实现服务降级逻辑。代码逻辑实现上会复杂些,可能带来更多类,可读性上对于设计模式不熟悉的研发者造成疑惑。

三者的关系

行文至此,也许有部分读者困惑比如:降级和熔断是不是一回事?熔断后执行策略就相当于降级?

纠结于这些问题的本身,还是要回到文章开头三大策略提出所面对解决的高可用问题。熔断是针对防止故障扩散所进行的策略设计,而 降级面对的是特殊场景的 服务功能/质量的调整策略。

因此,可以看到 描述降级中提到的 双十一零点前关闭商家价格调整的功能,显然 并非为了防止故障扩散的措施而是保障其他业务关注功能性能(不是熔断,主动改变);同样的,也可以举个例子当调用某个服务失败高时,切换调用到备用服务器来作为熔断处理策略,此时提供的服务功能或许都没有变化,只是启动了熔断一种技术处理策略(不属于服务降级)。

两者并非一回事。但如果考虑到 熔断发生时,处理的方式是 调整某种产品功能服务,那其实既可以算熔断也可以算降级,所以有些文章中也有提到 熔断降级 的概念。限流 与 降级呢? 熔断 与 限流呢? 在此就不一一赘述了,简而言之的话,三者都非一回事,但在某些场景下相互支撑。

熔断、限流、降级这些概念,更多是指导 在分布式系统 高可用设计上 应该考虑方面,其核心目的都是 达到系统的高可用。

转载望能 保留,欢迎关注 Java研究者 公众号

参考文献

热门相关:亿万盛宠只为你   修真界败类   锦庭娇   木工厂的那些事   年轻嫂子