服务容错设计:流量控制、服务熔断、服务降级

一、 为什么需要容错设计:

        在分布式系统中,因为使用的机器和服务非常多,所以故障发生的频率会比传统的单体应用更大。只不过,单体应用的故障影响面很大,而分布式系统中由于故障的影响面可以被隔离,所以影响面较小,但是因为机器和服务多,出故障的频率也很多。不过我们需要明白下面这些道理:

  • 出现故障不可怕,故障影响面过大才可怕
  • 出现故障不可怕,故障恢复时间过长才可怕

        另一方面,因为分布式系统的架构复杂,为了有效对分布式架构进行运维管理,我们会在系统中添加各种监控指标,方便出问题时快速定位。因此有些公司拼命地添加各种监控指标,有的甚至能添加出几万个监控指标,但这样做却给人一种”使蛮力“的感觉,一方面,信息太多就等于没有信息,另一方面,SLA 要求我们定义出核心关键指标。如果只是拼命添加各种监控指标而不定义出关键指标,这其实是一种思维上的惰性。

        另外,上述的措施都是在 “救火阶段” 而不是在 “防火阶段”,所谓 “防火胜于救火”,所以我们更要考虑如何进行防火,这就要求我们在设计或者运维时都要为可能发生的故障考虑,即所谓 “Design for Failure”,面向失败设计,在设计时要考虑如何减轻故障,如果无法避免,也要使用自动化的方式恢复故障,减少故障影响面。而容错设计就是面向失败设计中一个非常重要的环节,常见的容错设计有:

(1)流控:即流量控制,根据流量、并发线程数、响应时间等指标,把随机到来的流量调整成合适的形状,即流量塑性,保证系统在流量突增情况下的可用性,避免系统被瞬时的流量高峰冲垮,一旦达到阈值则进行拒绝服务、排队等降级操作。

(2)熔断:当下游服务发生一定数量的失败后,打开熔断器,后续请求就快速失败。一段时间过后再判断下游服务是否已恢复正常,从而决定是否重置熔断器。

(3)降级:当访问量剧增、服务出现异常或者非核心服务影响到核心流程时,暂时牺牲掉一些东西,以保障整个系统的平稳运行。

(4)隔离:将系统或资源分隔开,保证系统故障时,能限定传播范围和影响范围,防止滚雪球效应,保证只有出问题的服务不可用,服务间的相互不影响。常见的隔离手段:资源隔离、线程隔离、进程隔离、集群隔离、机房隔离、读写隔离、快慢隔离、动静隔离等。

(5)超时:相当多的服务不可用问题,都是客户端超时机制不合理导致的,当服务端发生抖动时,如果超时时间过长,客户端一直处于占用连接等待响应的阶段,耗尽服务端资源,最终导致服务端集群雪崩;如果超时时间设置过短又会造成调用服务未完成而返回,所以一个健康的服务,一定要有超时机制,根据业务场景选择恰当的超时阈值。

(6)幂等:当用户多次请求同一事件时,得到的结果永远是同一个。

二、流控设计:

       限流的目的是通过限制并发访问数或者时间窗口内的请求数,保证系统在流量突增情况下的稳定性,使系统不至于被高流量击垮,一旦达到限制阈值就可以拒绝服务(告知没有资源了或定向到错误页)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。而一般高并发系统常见的限流方式有:

  • 限制单位时间的调用量
  • 限制系统的总并发数,比如数据库连接池、线程池
  • 限制时间窗口内的并发数,比如漏桶和令牌桶

下面我们就讨论各种限流方式的设计与具体应用场景:

1、限制单位时间内的调用量:

        从字面上很容易理解,就是通过一个计数器来统计单位时间内某个服务的访问量。如果超过了阙值,则该单位时间段那不允许继续访问,或者把请求放入队列中等下一个时间段继续访问。需要注意的是计数器在进入下一个单位时间段时需要先清零。

        对于单位时间的设置,第一不能太长,太长将导致限流效果不够“敏感”;第二不能设置得太短,越短的单位时间段将导致阈值越难设置,比如1秒钟,因为高峰期的1秒钟和低峰期的1秒钟的流量有可能相差百倍甚至千倍。最优的单位时间片段应该以阈值设置的难易程度为标准,比如我们的监控系统统计的是服务每分钟的调用量,那我们自然可以选择1分钟作为时间片段,因为我们很容易评估每个服务在高峰期和低峰期的分钟调用量,并可以通过服务在每分钟的平均耗时和异常量来评估服务在不同单位时间段的服务质量。

        当单位时间段和阈值已经确定下来了,接下来就该考虑计数器的实现了,在 Java 中最常用的就是 AtomicLong,每次服务调用时,我们可以通过 AtomicLong.incrementAndGet() 方法来给计数器加1并返回最新值,并通过这个最新值和阈值来进行比较来看该服务单位时间段内是否超过了阈值。对于限制单位时间段内调用量的这种限流方式,实现简单,适用于大多数场景,但是也有一定的局限性,如果设定的阀值在单位时间段内的前几秒就被流量突刺消耗完了,将导致该时间段剩余的时间内该服务“拒绝服务”,这种现象被称为“突刺消耗”。

2、限制系统的总并发数:

        这种方式通过严格限制某服务的并发访问速度,也就限制了该服务单位时间段内的访问量。相比于第一种方案,它有着更严格的限制边界,因为如果采用第一种限流方案,如果大量的服务在极短的时间产生,仍然会压垮系统,甚至雪崩。并发限流一般用于服务资源有严格的限制的场景,比如连接数,线程数等。

        在 Java 中实现,并发限流也非常简单,比如可以使用信号量 Semaphore,在服务调用的入口调用非阻塞方法 Semaphore.tryAcquire() 来尝试获取一个信号量;如果获取失败,则直接返回或将调用放入到某个队列中;当服务执行完成,则使用 Semaphore.release() 来释放资源。

        但这种方式仍然可以造成流量尖刺,即每台服务器上该服务的并发量从 0 上升到阀值是没有任何阻力的,因为并发量考虑的只是服务能力边界的问题。      

3、通过漏桶进行限流:

        漏桶算法有点像我们生活中用到的漏斗,液体倒进去漏斗后从下端小口中以固定速率流出。漏桶算法就是这样,不管流量有多大,漏桶都保证了流量的常速率输出,由于调用的消费速率已经固定,那么当桶的容量堆满时,就只能丢弃了。示意图如下:

         漏桶算法是一种悲观策略,它严格地限制了系统的吞吐量,从某种角度上来说,它的效果和并发量限流很类似。漏桶算法可以用于大多数场景,但是由于它对于服务吞吐量有着严格限制,可能导致某些服务称为瓶颈。

        在 Java 中想要实现一个漏桶算法,可以准备一个队列,当作桶的容量,另外通过一个计划线程池(ScheduledExecutorService)来定期从队列中获取并执行请求调用,可以一次拿多个请求,然后并发执行。

4、通过令牌桶进行限流:

        令牌桶算法可以看成是漏桶算法的一种改进,漏桶算法能够强行限制请求调用的速率,而令牌桶算法能够在限制平均调用速率的同时,允许一定程度的突发调用,实现平滑限流。令牌桶算法中,桶中会有一定数量的令牌,每次请求调用需要去桶中拿取令牌,拿到令牌后才有资格执行,否则必须等待。

        看到这里或许有些疑问,如果把令牌比喻成信号量,那么好像和并发限流没什么区别。其实不是,令牌桶算法的精髓在于拿令牌放令牌的方式,这和单纯的并发限流有明显的区别:

  • 每次请求时需要获取的令牌数不是固定的,比如当桶中的令牌比较多时,每次调用只需要获取一个令牌,随着令牌数的逐渐减少,当令牌使用率(使用中的令牌数/令牌总数)达到某个比率时,可能一次需要获取两个令牌,获取令牌的个数可能还会升高。

  • 归还令牌有两种方法,第一种是直接放回,第二种是什么也不做,由另一个额外的令牌生成步骤将令牌允许放回桶中。

         前面讲过,令牌桶允许一定程度的突发调用,那么关于令牌桶处理数据报文的方式,RFC 中定义了两种令牌桶算法:

  • 单速率三色标记(single rate three color marker,srTCM,RFC2697 定义,或称为单速双桶算法)算法,主要关注报文尺寸的突发。
  • 双速率三色标记(two rate three color marker,trTCM,RFC2698 定义,或称为双速双桶算法)算法,主要关注速率的突发。

        两种算法都是为报文打上红、黄、绿三种颜色的标记,所以称为“三色标记”。 QoS 会根据报文的颜色,设置报文的丢弃优先级,两种算法都可工作于色盲模式和非色盲模式。对于单速率三色标记算法和双速率三色标记算法感兴趣的读者,可以阅读这篇文章:https://zhuanlan.zhihu.com/p/164503398

小结:令牌桶和漏桶算法区别:

(1)内容上:令牌桶算法是按固定速率生成令牌,请求能从桶中拿到令牌就执行,否则执行失败。漏桶算法是任务进桶速率不限,但是出桶的速率是固定的,超出桶大小的的任务丢弃,也就是执行失败。

(2)突发流量适应性上:令牌桶限制的是流入的速率且灵活,允许一次取出多个 token,只要有令牌就可以处理任务,在桶容量上允许处理突发流量。而漏桶算法出桶的速率固定,有突发流量也只能按流出的速率处理任务,所以漏桶算法是平滑流入的速率,限制流出速率。

5、四种限流算法的比较与小结:

        下面给出在某种特定场景和特定参数下四种限流方式对服务并发能力影响的折线图,其中X轴表示当前并发调用数,而Y轴表示某服务在不同并发调用程度下采取限流后的实际并发调用数:

        在不同场景不同参数下,服务采用所述四种限流方式都会有不同的效果,没有哪种限流算法能保证在所有场景下都是最优限流算法,因为这需要从服务资源特性、限流策略(参数)配置难度、开发难度和效果检测难度等多方面因素来考虑。但是相比于其他三种限流方式来说,令牌桶算法限流无疑是最为灵活的,因为它有着众多可配置的参数来直接影响限流的效果,比如 Google 的 Guava 包的 RateLimiter 提供了令牌桶算法的实现,感兴趣的读者可以自行百度。

        最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,或者是其他限流算法,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量,这是合理的,如果因为极少部分流量需要保证的话,那么就可能导致系统达到极限而挂掉,得不偿失

参考文章:https://www.iteye.com/blog/manzhizhen-2311691

三、熔断设计:

1、什么是熔断机制:

        熔断机制可以快速地拒绝可能导致异常的调用,防止应用程序不断执行可能失败的操作,当感知到下游服务的资源出现不稳定状态(调用超时或异常比例升高时),暂时切断对下游服务的调用,而不是一直阻塞等待服务响应,阻止级联失败导致的雪崩效应,保证系统的可用性;尤其是后端太忙的时候,使用熔断设计可以保护后端不会过载。另外,对于服务间的调用一般都会设置超时与重试机制,但如果错误太多,或是在短时间内得不到修复,那么再多的重试也没有任何意义了,这时也需要使用熔断机制快速返回结果。

        另外,开启熔断之后,也应该不断检测下游服务的健康情况,当检测到该节点的服务调用响应正常后,则恢复调用链路。这就要求熔断器具备感知异常的能力以及对关键逻辑实现开关控制,因此,熔断的设计有两个关键点:

  • ① 判断何时熔断:客户端对每次请求服务端的正常、异常(失败、拒绝、超时)返回计数,当异常返回次数超过设定阈值时进行熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。
  • ② 何时从熔断状态恢复:处于熔断状态时,客户端每隔一段时间(比如5秒),允许部分请求通过,若这部分请求正常返回,就恢复熔断。

2、熔断机制的实现:

(1)以 Hystrix 为例,Hystrix 设计了三种状态:

  • ① 熔断关闭状态(Closed): 服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制。
  • ② 熔断开启状态(Open): 在固定时间内,接口调用出错比率达到一个阈值,会进入熔断开启状态。进入熔断状态后, 后续对该服务接口的调用不再经过网络,直接执行本地的 fallback 方法。
  • ③ 半熔断状态(Half-Open): 在进入熔断开启状态一段时间之后,熔断器会进入半熔断状态。所谓半熔断就是尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。

(2)阿里研发的 Sentinel 内部熔断机制的实现其实也是熔断器的思想。Sentinel 底层会通过优化的滑动窗口数据结构来实时统计所有资源的调用数据(通过 QPS、限流 QPS、异常数、RT 等)。若资源配置了熔断降级规则,那么调用时就会取出当前时刻的统计数据进行判断。当达到熔断阈值时会将熔断器置为开启状态,切断所有的请求,直到过了降级时间窗口后再重置熔断器,继续允许请求通过。

四、降级设计:

1、什么是服务降级:

        所谓降级,就是指当访问量剧增、下游服务出现问题 或 非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务,这也是降级的最终目的。本质是为了解决资源不足和访问量过大的问题,当资源和访问量出现矛盾的时候,在有限的资源下,为了能够扛住大量的请求,我们就需要对系统进行降级操作,也就是暂时牺牲掉一些东西,以保障整个系统的平稳运行。一般来说,降级可以牺牲掉的东西有:

(1)降低一致性,从强一致性变成最终一致性:对于降低一致性,把强一致性变成最终一致性的做法可以有效地释放资源,并且让系统运行得更快,从而可以扛住更大的流量。通常会有两种做法:一种是简化流程的一致性(比如使用异步简化流程),一种是降低数据的一致性(比如使用缓存)。

(2)停止次要功能:停止访问不重要的功能,从而让系统释放出更多的资源。

(3)简化功能:把一些功能简化掉,比如简化业务流程,或是不再返回全量数据,只返回部分数据。

2、服务降级的类型与策略:

2.1、降级按照是否可以自动化可分为:人工开关降级和自动开关降级。

2.2、按触发降级的时机可以分为以下几个大类:

  • 限流降级:当服务端请求数的数量超过配置的限制阈值,后续请求会被降级
  • 超时降级:事先配置好超时时间和超时重试次数及机制,并使用异步机制探测恢复情况
  • 失败降级:该方式主要是针对不稳定的API,当失败调用次数或者比例达到一定阀值时自动降级,同样要使用异步机制探测回复情况
  • 故障降级:如要调用的远程服务挂掉了(比如网络故障、DNS故障、HTTP服务返回错误的状态码和RPC服务抛出异常),则可以直接降级

2.3、降级按照功能维度可分为:读服务降级和写服务降级

(1)读降级:可以暂时切换读数据来源,比如返回缓存数据、默认值、兜底数据。如果后端服务有问题,则可以降级为只读缓存,这种方式适合对读一致性要求不高的场景

① 缓存降级:使用缓存方式来降级部分读频繁的服务接口。每次服务调用成功时,记录服务的结果在缓存中,下一次失败时读取缓存的旧数据;可以根据时延要求选择本地缓存、分布式缓存、数据库或本地文件。适用场景为能够接受一定延迟的只读业务,且有足够的存储资源。但需要注意以下几点:

  • 分布式缓存或数据库只是把服务故障转移到另一个依赖;
  • 冷数据无法降级,即较长时间之前的状态数据无法降级;
  • 降级后的数据存在一致性问题,需要根据业务情况设置合理的有效期;
  • 数据量大时消耗过多的存储(特别是缓存)且命中率低,可以设置合理的缓存大小,使用LRU方式替换旧缓存。

② 默认值:直接返回配置中的默认值或者空数据。适用于弱依赖的只读业务,需要注意的是,默认值尽量有多种选择,避免千篇一律。

比如主播标语服务,在配置中心配置一批中性的默认标语,标语服务失败时直接随机取一条返回给用户,故障率不高的情况下用户基本上感知不到异常。

(2)写降级:可以使用异步处理并保证最终一致性,比如先更新缓存,然后异步同步到数据库中;或者采用事后处理,执行补偿机制,自动或者手动补偿。

        在 CAP 原理和 BASE 理论中写操作存在数据一致性这个问题,降级的目的是为了提供高可用性,在多数的互联网架构中,可用性是大于数据一致性的。所以丧失写入数据的同步,通过上面的理论,我们也能勉强接受数据最终一致性。高并发场景下,如果写入操作无法及时到达或抗压,可以异步消费数据、cache更新、log等方式进行补偿

文章小结:限流、熔断、降级都是解决服务间 RPC 过程出现的异常问题,保证服务稳定性的手段。限流发生在Server端,熔断发生在Client端,而降级是触发限流、熔断之后的补偿措施。在实际场景中通常会配合实现,比如熔断和降级,熔断决定何时降级,降级是服务在熔断状态下的对外表现。


相关阅读:

常见的服务器架构入门:从单体架构、EAI 到 SOA 再到微服务和 ServiceMesh

常见分布式理论(CAP、BASE)和一致性协议(Gosssip协议、Raft一致性算法)

一致性哈希算法原理详解

Nacos注册中心的部署与用法详细介绍

Nacos配置中心用法详细介绍

SpringCloud OpenFeign 远程HTTP服务调用用法与原理

什么是RPC?RPC框架dubbo的核心流程

服务容错设计:流量控制、服务熔断、服务降级

sentinel 限流熔断神器详细介绍

Sentinel 规则持久化到 apollo 配置中心

Sentinel-Dashboard 与 apollo 规则的相互同步

Spring Cloud Gateway 服务网关的部署与使用详细介绍

Spring Cloud Gateway 整合 sentinel 实现流控熔断

Spring Cloud Gateway 整合 knife4j 聚合接口文档

常见分布式事务详解(2PC、3PC、TCC、Saga、本地事务表、MQ事务消息、最大努力通知)

分布式事务Seata原理

RocketMQ事务消息原理

  • 13
    点赞
  • 90
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张维鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值