熔断、限流、降级、扩容

为什么需要

在云时代,随着业务的不断发展和扩大,分布式的系统已经是大部分企业的首选,然而分布式也会带来一个都会遇到的问题,任何一个节点或者组件出现问题都可能会导致整个服务的雪崩。

对于服务雪崩的情况主要有两个解决办法,一个是快速的失败减少系统的复杂,也就是熔断、限流、降级,另一种是快速的扩容,增加服务的负载能力。需要注意的是熔断是被动感知故障的,并且熔断肯定会导致系统的抖动。

熔断

生活中就有一些熔断的实例,譬如电器中的熔断器,它的作用就是在电压出现问题的时候及时切开电路,保护电器不受伤害。计算机中的熔断就是借鉴了这种模式。当服务连续出现错误时,系统会自动停止对该服务的调用,防止错误扩散,保护系统的稳定性。

电器中的熔断器只有断开和链接,而在计算机中有三个状态:

  1. 闭合状态: 请求正常进行,熔断器会监控请求的错误率,如果达到了阈值就会切换开打开状态;
  2. 打开状态:请求直接被拒绝,防止服务进一步崩溃,在打开状态之后会经过一段时间的冷却期,这个时间主要是用于服务自我恢复的,过了冷却期会进入半开状态。
  3. 半开状态:请求会有一部分被正常处理,熔断器会通过这些请求判断服务是否已经恢复,如果恢复将进入闭合状态,如果错误率依旧很高将会进入打开状态。

实现熔断关键在于对于请求失败的监控,在监控失败的请求时一定要分区应用层的错误,譬如密码错误、余额不足这种失败是需要忽略的。

监控失败常见的模式下面几种

  1. 错误次数计数:在一定时间窗口内计数错误。如果错误次数超过预设的阈值,熔断器就会打开。优点在于实现起来非常简单,缺点是如果请求量突增,肯可能系统的错误率并没有上升,但是错误次已经达到阈值,可能导致熔断器错误的打开。
  2. 错误率:在一定时间窗口内统计服务的错误率,当错误率达到指定阈值时,触发熔断机制,优点适用于请求量大且变化较大的场景,因为它可以更好地处理错误的随机性。缺点在请求量很少的情况下错误率可能会受到影响。
  3. 请求延迟:统计服务在一定时间窗口的响应时间,当响应时间超过指定阈值时,触发熔断机制。有点是在服务开始变慢,但还没有完全失败时就提前介入,防止慢服务导致的问题。缺点在于很难去去设置一个合理的响应阈值。

在.net 中 Polly 库提供了两种熔断的策略 CircuitBreaker 基于错误次数的熔断器。AdvancedCircuitBreaker 基于错误率的熔断器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 基于错误次数的熔断器
var circuitBreakerPolicy = Policy
.Handle<Exception>() // 指定要处理的异常类型
.CircuitBreaker( // 创建熔断器策略
exceptionsAllowedBeforeBreaking: 3, // 错误次数阈值
durationOfBreak: TimeSpan.FromMinutes(1), // 熔断时间
onBreak: (ex, breakDelay) => { /* 处理熔断事件的代码 */ },
onReset: () => { /* 处理熔断器重置事件的代码 */ },
onHalfOpen: () => { /* 处理熔断器半开事件的代码 */ });

var advancedCircuitBreakerPolicy = Policy
.Handle<Exception>() // 指定要处理的异常类型
.AdvancedCircuitBreaker( // 创建高级熔断器策略
failureThreshold: 0.5, // 错误率阈值
samplingDuration: TimeSpan.FromMinutes(1), // 时间窗口
minimumThroughput: 10, // 最小吞吐量
durationOfBreak: TimeSpan.FromMinutes(1), // 熔断时间
onBreak: (ex, breakDelay) => { /* 处理熔断事件的代码 */ },
onReset: () => { /* 处理熔断器重置事件的代码 */ },
onHalfOpen: () => { /* 处理熔断器半开事件的代码 */ });

需要注意的是 CircuitBreaker 没有设置错误检测的时间窗口,它只关注连续的错误数量。这意味着如果错误并非连续发生,CircuitBreaker 不会触发。

最后需要注意一下熔断级别,从微观的某个实例单一操作到宏观的实例或整个系统的熔断,可以细分为下面几种:

  1. 实例的Api:这种熔断的效果是非常好的,影响也是最小的;
  2. Api:这种可能会存在误删,譬如10台机器,只有一台机器的接口出现了问题,其他的机器都是正常的,显然直接熔断接口错误率是非常高的;
  3. 实例:对实例进行熔断会导致实例上正常的接口也被下线,误伤范围大;
  4. 服务:整个服务熔断会导致整个服务的不可用,通常不会服务进行熔断;

大部分请求下熔断的级别或者说熔断的粒度越小越好。

限流

限流是一种控制请求速率的方式,防止过多的请求同时到达系统,导致系统无法处理并可能崩溃。通过限制进入系统的请求速率,限流可以保证系统在高负载情况下的稳定性和可用性。

限流最核心的限流算法,根据不用同的场景有固定窗口、滑动窗口、漏桶、和令牌桶四种算法。

固定窗口

窗口就是时间, 固定窗口把时间划分为固定的大小窗口,每个窗口允许的最大请求数量固定。譬如10秒值允许接受100个请求,在10秒如果超过请求阈值100就拒绝请求,如果没有就进入下一个窗口。

优点是实现简单,占用资源少。

缺点也非常明显,第一点是边界效应,譬如在窗口切换的瞬间,可能会出现突发的大量请求,这可能会导致系统的瞬时负载超过预期。例如,如果限制每秒100个请求,那么在第一秒的最后一刻和第二秒的第一刻,可能会连续发生200个请求。

第二点是面对流量突增时窗口粒度不好控制,如果窗口太大,那么限流的精度就会降低,如果窗口太小,那么可能会频繁地拒绝请求。譬如前10ms就已经接受了100个请求。那么剩余的窗口时间就无法接受新的请求了,会导致资源利用不高。

第三点是公平性问题,所有的请求都被平等地限制,没有考虑到请求的优先级或来源。这可能会导致优先级高的请求被拒绝,或者恶意的请求占用了大量的请求配额。

滑动窗口

滑动窗口限流算法的基本思想是将时间分割为多个相等的小窗口,并对每个窗口内的请求进行计数。当一个新的请求来临时,会在当前窗口的计数器上增加1。然后,系统会计算在当前窗口以及前面的所有窗口中的请求总数。如果这个总数超过了预设的阈值,那么新的请求就会被拒绝。比如 3 秒的固定窗口阈值是 10,切分为 3 个 1 秒的滑动窗口,当请求进来找到当前窗口,并在当前窗口计数器 +1,之后会计算在当前窗口以及前面的所有窗口中的请求总数。如果这个总数超过了 10,那么这个请求就会被拒绝。

滑动窗口的粒度过小 限流的统计就会更加精,但是也会占用资源。滑动窗口对比固定窗口解决了边界的问题。滑动窗口在面对流量突增时依旧存在和固定窗口一样的问题。

漏桶

想象一个装满水的漏桶,水以一定的速率从桶底的孔洞中流出,当水流入的速率超过水流出的速率时,多余的水会溢出桶外。同样地,如果请求进入系统的速率超过系统处理的速率,多余的请求会被拒绝。
实现起来也比较简单,可以通过一个队列来实现。每个请求都会被添加到队列的末尾。然后,有一个固定速度的处理器从队列的头部取出请求并处理。如果队列已满,新来的请求就会被拒绝。

漏桶限流算法的主要优点是能够控制数据的传输速率,保证数据的传输速率不超过系统的处理能力。此外,漏桶限流算法还能够缓冲突发流量,防止系统因为瞬间的大流量而崩溃。

漏桶限流算法也有一些缺点。首先,漏桶的大小是固定的,如果突发流量超过漏桶的容量,多余的请求会被立即拒绝,而不是排队等待。其次,系统处理请求的速率是恒定的,无法根据系统的实际负载动态调整。

所以漏桶限流比较适合流量整形的场景。【流量整形(Traffic Shaping)是一种网络流量管理技术,用于控制数据包的发送速率,以防止网络拥堵和确保满足特定的服务质量(QoS)要求。流量整形的主要目标是使数据流更加平滑,避免突发流量导致的网络拥堵。它可以控制数据包的发送速率、延迟、抖动等参数,以满足不同的服务质量要求。】

令牌桶

令牌桶算法的核心思想是以一个恒定的速率往桶中添加令牌,这个速率通常称为令牌发放速率。桶的容量有一个上限,这意味着即使没有数据包要发送,桶也不会无限制地累积令牌。没一个请求会消耗一个令牌,如果桶中有令牌那么请求被正常执行,如果没有请求将会被拒绝或等待。

主要优势是它允许一定程度的突发流量。因为桶有一定的容量,所以在流量低于平均水平时,令牌可以在桶中累积。当突发流量到来时,累积的令牌可以立即使用,允许数据包以超过平均速率的速度传输,直到令牌用完为止。

相对于漏桶提高了系统的资源利用率。但是放弃了一些流量整型的能力。上游流量的抖动会扩散到下游服务。

降级

降级的核心思想是通过牺牲非核心服务来保证核心服务的稳定性。 虽然熔断和限流可以在一定程度上保护系统,但在某些情况下,它们可能无法阻止系统过载。例如,如果所有的请求都是合法的,并且每个请求都需要消耗大量的资源,即使有了熔断和限流,系统也可能会因为资源耗尽而崩溃。

降级在过载后一定程度上可以提高用户的体验,在服务过载后,返回一个简化的结果,用一些巧妙的方式告知用户,例如友好的错误提示页面或者有趣的页面加载动画等。

降级的难点在于对核心业务的识别,已经服务的等级的区分,譬如:

服务等级 服务名 影响
p0 用户、支付 账号、支付等核心功能无法使用
p1 活动、榜单 活动、榜单等功能无法使用
p3 聊天 世界聊天、私聊等功能无法使用
p4 推送 系统相关通知无法送法

当对服务进行了分级和影响后,根据服务的等级和依赖关系来为非核心业务建立降级机制。

扩容

熔断、限流、降级都是从系统底线、核心服务保障、非核心业务牺牲三个角度出发去保障系统核心业务的运行。它们都会放弃一部分的用户体验和可用性。

扩容可以保证用户体验不受影响的情况下处理系统过载。当系统过载或者到了资源到了一定的阈值的情况下,自动增加系统资源保证服务的稳定运行。扩容将是解决系统过载问题最常用的方法。

在 K8S 中,扩容分为 orizontal Pod Autoscaler( HPA )对应水平扩展,Vertical Pod Autoscaler( VPA )对应垂直扩展,

水平扩容就是增加服务的 pod 数量,垂直扩展就是升级机器的硬件资源。

水平扩容不受单机硬件的限制,非常适用于没有状态的服务, 但是对于有状态服务,在水平扩容的时候,会涉及数据迁移。如果这个有状态服务,对数据的自动迁移原生支持不好的话,会给系统增加复杂度,这时选择垂直扩容会比较好。

扩容的难点在于估量出需要扩容的服务,以及服务需要扩容到什么规模, 对于一些运营活动和一些计划内的内容,这种这种都会有一些历史数据和经验来评估。

而对于线上突发流量的这种情况,就需要监控来捕捉即将过载的服务后进行扩容。

最后

在 DotNet 中限流可以使用官方的中间件实现,文档地址


熔断、限流、降级、扩容
http://example.com/posts/37676.html
作者
她微笑的脸y
发布于
2023年11月24日
许可协议