熔断、限流、降级

342 阅读14分钟

熔断

熔断主要针对具体某个接口出现问题时,对这个接口采用的临时方案,它站在接口或者服务的层面来考虑问题。防止因为某个接口出现问题,导致问题蔓延,直至整个系统不可用。例如:某个接口的提供方宕机,导致接口调用方频繁超时,调用方的上游受到影响,进而也会超时,最终整个链路都会变得超时,如果超时时间很长的话,会导致客户端系统资源浪费(一些池化的资源,长时间得不到释放)。此时可以在接口的调用方进行对该接口的熔断,具体做法,可以直接返回一个默认值,防止频繁的调用超时。同时通过不断的对该接口进行可用性探测,当检测到该接口可用时,对熔断逻 辑进行撤销。

熔断的实现

当远程服务被调用时,熔断器将监视这个调用,如调用时间太长,熔断器将会介入并中断调用。此外,熔断器将监视所有对远程资源的调用,如对某一个远程资源的调用失败次数足够多,那么熔断器会出现并采取快速失败,阻止将来调用此远程资源的请求。

熔断器的状态

熔断器(Circuit Breaker)有三种状态:

  • 关闭(Closed)
    • 熔断器关闭时,允许所有请求正常访问后端服务。熔断器会记录请求失败、超时、异常等情形,当失败率达到一定阈值时,会触发熔断器打开。
  • 打开(Open)
    • 熔断器打开时,所有请求会快速失败,不再访问后端服务。这避免了大量请求在故障服务上等待造成阻塞。打开状态会维持一定时间,让后端服务有恢复的时间。
  • 半开(Half-Open)
    • 熔断器打开一段时间后,会进入半开状态。这时会释放一定量的请求到后端服务,并统计成功率。如果请求成功率达到预设值,认为服务已恢复,熔断器会关闭;否则继续保持打开状态。

状态转换逻辑:

  • 关闭 -> 打开:失败率达阈值
  • 打开 -> 半开:打开一段时间后
  • 半开 -> 关闭:请求成功率达标
  • 半开 -> 打开:请求成功率未达标

熔断器的三状态转换,可以实现对后端服务故障的快速检测、故障隔离和自动恢复。

熔断原理

熔断器的工作原理如下:

  • 默认关闭状态,请求可以通过,同时记录请求失败率
  • 失败率达到阈值时,熔断器开启,请求快速失败
  • 进入熔断状态的服务在一段时间后会进入半开状态
  • 在半开状态会放部分流量请求通过,测试服务是否恢复
  • 如果请求成功率达标,熔断器切回关闭状态,服务恢复正常

使用熔断的好处:

  • 快速失败,不会拖垮依赖服务
  • 防止故障蔓延的连锁反应
  • 自动恢复服务调用,无需人工介入

熔断需要设置合理的失败率阈值,并与重试、限流等机制配合使用,可以有效提高系统故障容错能力。

降级

降级是系统应对突发流量的解决方案,它站在系统层面来考虑问题,目的是为了保证整个系统的可用性。因为受系统自身资源的限制,可以处理的流量是是有限的,不能对突发流量进行完全处理,那么此时就应该对流量进行有选择的处理,让系统只处理优先级高的流量,优先级低的流量不进行处理,直接返回默认值(或者其他处理策略),也就是对部分低优先级流量进行舍弃,例如,双十一期间,很多电商网站的评论,收藏功能变得不可用,这里就是系统为了保证网站核心功能可用,对评论和收藏功能进行了降级,将系统资源用在核心业务上,正所谓"好钢用在刀刃上"

降级策略与手段

降级策略有很多种,可以从下面3个维度分为5种策略:

  • 自动化维度 包括:自动开关降级、人工开关降级。
  • 功能维度 包括:读服务降级、写服务降级。
  • 系统层次维度 包括:多级降级。

降级的手段主要可以分为以下几种:

  • 限流降级:自动根据系统负载情况限制流量,如关闭非核心业务,降低锁粒度等。
  • 资源隔离: Priority 区分核心和非核心资源,保证核心服务资源优先。
  • 负载控制:停止非核心任务,自动增删服务实例等。
  • 熔断降级:遇到调用故障时快速失败,避免级联故障。
  • 数据降级:返回缓存或默认值,而不是直接失败。
  • 函数降级:切换到简单的业务逻辑,暂时不执行复杂逻辑。

降级模式包括预发降级(事前)、应急降级(事中)、最少降级(事后)等。

需要注意的是要区分核心流量和非核心流量,避免过度降级影响核心业务。降级是一个保护系统的“自我牺牲”行为,要谨慎使用。

自动开关降级

系统在运行时根据运行状态自动触发降级,例如:

  • 超时
    • 调用远程非核心服务时,响应过慢后自动降级,先不调了。需要配置好超时时间和超时重试次数。
  • 统计失败次数
    • 有的服务可能不太稳定,例如外部的机票服务,当调用失败次数超过容忍度后就自动降级。可以通过异步线程去探测服务是否恢复了,可用后自动取消降级。
  • 故障
    • 比如远程服务挂掉了,就自动降级,可以使用默认值、提前准备的内容、之前的缓存数据。
  • 限流
    • 例如秒杀系统,通常会使用限流机制进行保护,当达到限流阈值时,后续请求就自动被降级,例如将用户导流到排队页面过会儿重试或直接告诉用户没货了。

人工开关降级

自动降级是根据系统出现的一些问题进行响应,但在系统还没有出现问题时,我就想降级某些服务,比如促销马上开始了,可以预知访问量很大,我们提前就把推荐服务关掉;再比如新功能想上线进行灰度测试,当新功能不符合预期时我想马上切换回老服务。

  • 这类需求就需要使用可以人工控制的开关来实现。
  • 开关可以存放到配置文件、数据库、Redis/Zookeeper等位置,定期同步开关数据。
  • 分布式系统中通常会创建一个配置中心,对整个系统中的配置开关进行集中管理,提供 Web UI 界面进行便捷操作。目前有一些开源方案可以选择,例如 ZooKeeper、Diamond、Etcd 3、Consul。

读服务降级

  • 从读取数据 的角度考虑降级,例如商品详情页,其中有非常多的内容,比如商家信息、推荐信息、配送至信息、相关分类、热销榜等等,这些都不是核心数据,所以在出现异常时可以进行降级处理。

还以商品详情页为例,在促销活动之前,可以将整个页面切换为静态化,最大程度的降级读服务。

写服务降级

写服务都是很关键的,降级思路基本就是同步写转异步写。

例如扣减库存的操作,正常情况下的设计一般是:

  • 数据库中扣减,成功后更新 Redis 缓存。
  • 先扣减 Redis 缓存,同步扣减数据库,如果失败则回滚 Redis 缓存。

当数据库性能跟不上时,就需要采用异步方式了:

先扣减 Redis 缓存,同时向队列中发送一条扣减数据库库存的消息,异步进行数据库扣减,实现最终一致性。 再比如用户评价,如果量太大,就可以把评价从同步写转为异步写,还有评价后会给一些奖励,也可以异步。

多级降级

从距离用户远近的角度,可以分为以下3个层面:

  • 页面JS降级开关
    • 主要控制页面功能的降级,在页面中通过JS脚本部署来控制在适当时机开启/关闭开关。
  • 接入层降级开关
    • 在请求入口进行控制,请求进入后,会首先进入接入层,例如 Nginx,在其中根据实际情况进行自动/人工降级。
    • 接入层的控制功能是很强大的,可以实现秒级切换、增量式切换(按照机器组开启)、细粒度服务开关、超时自动降级。
  • 应用层降级开关
    • 在应用中配置相应的功能开关,根据实际情况进行自动/人工降级。

限流

限流和降级所做的事情相似,也是站在系统层面来考虑问题,都是在系统遇到突发大流量所做的应急方案。为了保证系统不被大流量冲垮,降级所做的事情是对流量进行有选择的处理,让系统处理总流量中的一部分。这里的有选择,主要是按照流量的优先级进行选择的。而限流则是通过对流量进行采样,例如1000个请求,通过80%采样后,系统只处理其中的800个,剩下的200个,通过限流策略进行过滤,这里的过滤更强调无差别的过滤。

限流的实现

计数器

计数器限流是限流算法的一种,它的工作原理如下:

  1. 设置限流阈值,比如 QPS 不超过 100。
  2. 使用一个计数器统计单位时间内的请求数。可以使用 Redis、数据库或内存计数器。
  3. 收到请求时,检查计数器的值是否达到阈值:
    • 如果未达到阈值,计数器加 1,请求继续执行。
    • 如果达到阈值,直接返回限流错误。
  4. 在统计窗口结束时,重置计数器,开始新一轮的统计。

计数器限流的优点是:

  • 实现简单,只需要一个计数器。
  • 精确控制每秒钟处理的请求数。

缺点:

  • 无法处理突发请求。
  • 计数器可能成为性能瓶颈。

此外,可以通过漏桶算法处理突发请求,并通过令牌桶控制整体速率。计数器限流易于实现,能有效应对持续大流量,常被用作第一道防线。更复杂的限流可以基于计数器来实现。

固定窗口

固定窗口限流,我们可以这样子来理解,我们假设规定1s可以放入3个请求,那么窗口的大小就是1s,然后在1s内对请求计数,如果超过3那么就限流,过了这一秒后计数清空。 image.png

缺点

  • 比如如上述窗口大小为1秒,一个窗口允许1000个请求,某一个窗口的前100ms没有请求,在最后的100ms突然接收1000个请求,这个窗口的下一个窗口在前100ms也接收到1000个请求,这样就是短短的200ms接收了2000个请求,它的缺点与计数器类似。

滑动窗口

改进上述的固定窗口,我们可以把1s以200ms分割成了5个小窗口,然后每个小窗口会记录这段时间内的请求数量,如果总和已经超过了最大请求量那么就拒绝请求,然后每隔200ms 我们往右边滑动,顺便减去左边被淘汰出去的窗口的请求数,我们可以用下图来理解(1s三个请求)

image.png

滑动窗口限流算法的工作原理是:

  1. 设置限流时间窗口,例如1秒钟。

  2. 设置限流阈值,例如每窗口最多处理100个请求。

  3. 使用一个队列记录最近窗口时间内的请求。

  4. 收到一个请求时:

    • 从队列头部删除窗口之前的请求计数。
    • 计算队列中的请求数量。
    • 如果请求数量超过阈值,则拒绝,否则接受。
    • 将该请求计入队列。
  5. 窗口每滑动一次,重复步骤4。

滑动窗口限流的优点:

  • 可以限制突发流量,平滑处理。
  • 计算窗口请求数简单高效。
  • 时间和空间复杂度为 O(1)。

缺点是队列长度需要考虑窗口时间和限流精度。

滑动窗口限流易于实现,通过平滑突发流量peak(高峰)来提供稳定的吞吐量。是一种精准、高性能的限流算法。

漏桶

a46c51128c1d4c71acb6a7244cae1a35.png

它的工作原理如下:

  1. 有一个固定容量的桶,用于存储要发送的数据包。
  2. 桶中以一定的速率r(漏 Rate)流出数据包。当桶满时,新进入的数据包会被丢弃。
  3. 外部的数据以可变速率 R(输入 Rate)流入桶中。
  4. 如果 R > r,则数据包会先进入桶排队,然后按固定速率 r 流出。当桶满时丢包。
  5. 如果 R < r,则所有数据包都直接从桶流出。

漏桶算法特点:

  • 对突发流量平滑,避免网络拥塞。
  • 可以控制数据流输出的上限速率。
  • 需要选择适当的桶容量和输出速率。
  • 会造成一定的时延。

漏桶算法易于实现,通过最小容量缓冲突发流量,并控制平均输出速率,适用于需要平滑发送速率的场景。

令牌桶

令牌桶能够在限制请求平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。该算法以一定的速率往桶中放入令牌。每次请求需要先获取到桶中的令牌才能继续执行,否则等待可用的令牌,或者直接拒绝。放令牌的动作是持续不断进行的,如果桶中令牌数达到上限,则丢弃令牌,因此桶中可能一直持有大量的可用令牌。此时请求进来可以直接拿到令牌执行。比如设置qps为100,那么限流器初始化完成1秒后,桶中就已经有100个令牌了,如果此前还没有请求过来,这时突然来了100个请求,该限流器可以抵挡瞬时的100个请求。由此可见,只有桶中没有令牌时,请求才会进行等待,最终表现的效果即为以一定的速率执行 1.png

它的工作原理如下:

  1. 有一个桶,用来存放固定数量的令牌。
  2. 令牌以一定的速率 r 定期添加到桶中,这是令牌生成速率。
  3. 当一个数据包需要发送时,需要先从桶中获取一个令牌。如果桶中有令牌,可以发送,否则需要等待令牌生成。
  4. 桶中令牌数目存在上限,如果桶满时还有新令牌生成,就丢弃。
  5. 数据发送速率不能超过令牌生成速率 r。

令牌桶算法的特点:

  • 可以限制平均发送速率。
  • 允许短时间的高发送速率(突发特性)。
  • 需要设置适当的桶容量和令牌生成速率。

令牌桶算法可以有效控制发送流量的Expodential Weighted Moving Average(指数加权移动平均)速率,常用于流量整形和速率限制。

比较

image.png