熔断与限流

(一)服务熔断

1 基本概念

服务雪崩:
假设现场有一个场景ServiceA调用ServiceB, ServiceB调用Service C。如果流量比较大,ServiceC扛不住了,那么ServiceB的请求就会阻塞,慢慢耗尽ServiceB的资源,随后SerciceB也不可用,又会影响ServiceA,导致ServiceA也不可用。
这样有一个服务由于反噬导致大面积的服务不可用,就叫做服务雪崩

服务熔断:
当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证整体服务的可用性,不再继续调用目标服务,而是直接返回,以便快速释放资源。等到目标服务情况好转则恢复调用下游服务。

服务降级:

  • 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度。
  • 当下游的服务因为某种原因不可用,上游主动调用本地的一些降级逻辑,避免卡顿,迅速返回给用户。

服务降级有很多种降级方式,如开关降级、熔断降级、限流降级。熔断属于降级的一种方式

服务熔断适用的场景:

  • 发现近期某个接口的请求经常出现异常时,此时很有可能这个服务已经不可用了,那么就不再调用这个服务的接口了。
  • 发现某个接口的请求总是超时,先判断接口的服务是否不堪重负,如果是就先别访问它。

2 实现服务熔断的工具

目前实现熔断比较好的开源框架有两个:一个是Netflix开源的Hystrix, Spring Cloud默认使用这个组件。另一个是阿里开源的Sentinel. 两者性能对比如下:

特性 sentinel Hystrix
隔离策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于响应时间或失败比例 基于失败比例
实施指标实现 滑动窗口 滑动窗口
规则设置 支持多种数据源 支持多种数据源
基于注解的支持 支持 支持
限流 基于QPS(每秒查询数),基于调用关系的限流 不支持
流量整形(承接限流) 直接拒绝、慢启动预热、匀速器模式 不支持
系统负载保护 支持 不支持
控制台 开箱即用,支持规则配置,秒级监控查看,机器发现等 很简单的监控查看

3 Hystrix的设计思路

1 线程隔离:一般当前服务依赖的一个接口响应慢时,正在运行的线程就会一直处于未释放状态,最红把所有的线程都卷入慢接口中。为此,Hystrix为每个依赖接口维护一个线程池,通过线程池的大小,排队数等隔离每个服务对依赖接口的调用,以免占用太多线程。除了线程池,也可以使用信号量来实现隔离,调用方法的时候semaphoreA++,调用完之后semaphoreA--。
在使用线程池时经常需要切换线程,资源损耗较大,而信号量的优点恰巧就是切换快,所以正好能解决问题。不过它也有一个缺点,即接口一旦开始调用就无法中断,因为调用依赖的线程是当前请求的主线程,不像线程隔离那样调用依赖的是另外一个线程,当前请求的主线程可以根据超时时间把它中断。通过线程隔离,不会因为一个下游接口慢而将当前服务的所有资源占满。
2 熔断机制

  1. 如何触发熔断?熔断判断规则是某段时间内调用失败数超过特定的数量或比例时,就会触发熔断。在Hystrix机制中,会配置一个不断滚动的统计时间窗口metrics.rollingStats.timeInMilliseconds,在每个统计时间窗口中,若调用接口的总数量达到circuitBreakerRequestVolumeThreshold,且接口调用超时或异常的调用次数与总调用次数之比超过circuitBreakerErrorThresholdPercentage,就会触发熔断。
  2. 熔断之后怎么办?如果熔断被触发,在circuitBreakerSleepWindowInMilliseconds的时间内,便不再对外调用接口,而是直接调用本地的一个降级方法methodA,如:@HystrixCommand(fallbackMethod="methodA")
  3. 熔断后如何恢复?到达circuitBreakerSleepWindowInMilliseconds的时间后,Hystrix首先会放开对接口的限制(断路器状态为HALF-OPEN),然后尝试使用一个请求去调用接口,如果调用成功,则恢复正常(断路器状态为CLOSED),如果调用失败或出现超时等待,就需要重新等待circuitBreakerSleepWindowInMilliseconds的时间,之后再重试。

3 滑动时间窗口:
滑动时间窗口与滑动窗口算法很相似,只是这里是统计每个单位时间桶内的成功数,失败数,超实数,拒绝数,再单独汇总统计周期时间内桶的数据。
4 Hystrix调用接口的请求处理流程:

4 注意事项

在应用Hystrix实现熔断时,需要考虑数据的一致性问题、超时降级导致的数据异常、用户体验、熔断监控等方面。
数据一致性:比如服务A调用了服务B,服务B调用了服务C,在服务A中成功更新了数据库并成功调用了服务B,而服务B调用服务C时降级了,直接调用了Fallback方法,此时就会出现两个问题:服务B向服务A返回成功还是失败?服务A的数据库更新是否需要回滚?
超时降级:比如服务A调用服务B时,因为调用过程中B没有在设置的时间内返回结果,被判断超时了,所以服务A又调用了降级的方法,其实服务B在接收到服务A的请求后,已经在执行工作并且没有中断;等服务B处理成功后,还是会返回处理成功的结果给服务A,可是服务A已经使用了降级的方法,而服务B又已经把工作做完了,此时就会导致服务B中的数据出现异常。
用户体验:1)用户发出读数据的请求时遇到有些接口降级了,导致部分数据获取不到,就需要在界面上给用户一定的提示,或让用户发现不了这部分数据的缺失。2)用户发出写数据的请求时,熔断触发降级后,有些写操作就会改为异步,后续处理对用户没有任何影响,但要根据实际情况判断是否需要给用户提供一定的提示。3)用户发出写数据的请求时,熔断触发降级后,操作可能会因回滚而消除,此时必须提示用户重新操作。
熔断监控:熔断功能上线后,其实只是完成了熔断设计的第一步。因为Hystrix是一个事前配置的熔断框架,关于熔断配置对不对、效果好不好,还需要从Hystrix的监控面板查看各个服务的熔断数据,然后根据实际情况再做调整,只有这样,才能将服务器的异常损失降到最低。

业务场景:如何保障服务器承受亿级流量:
在常见的秒杀活动中,经常会抢购某个特价商品,且这个商品的价格非常低,活动在一段时间内谁先抢到谁就拥有,名额有限。在秒杀那一瞬间,会有海量的用户涌入,致使系统无法处理所有用户请求,为保障服务承受大流量,只能
通过限流的方式将部分流量放入后台服务。在谈到限流时,人们经常把它与熔断一起讨论,其实二者是有去别的。
熔断一般发生在服务调用方,比如服务A需要调用服务B,调用几次后发现服务B出现了问题且无法调用,此时服务A必须立即触发熔断,在一段实践内不再调用服务B。
限流一般发生在服务被调用方,且主要在网关层做限流操作。比如某个网站的后台服务每秒只能处理10万个请求,但是突然涌入了100万个请求,那么就会把多出的百分之90的请求全部抛弃,不做处理。然后重点处理剩下的百分之10的请求。

(二)服务限流

限流算法:

关于限流的算法分为固定时间窗口计数、活动时间窗口计数、漏桶、令牌桶四种

1 固定时间窗口计数算法:

假设后台服务每秒处理500个请求(以5秒为单位举例),那么每5秒就需要一个时间窗口来统计请求。比如中如12:00:01秒——12:00:05请求数是600,那么就会抛弃一百个;
12:00:06秒——12:00:10请求数是500,则抛弃0个。12:00:11秒——12:00:15,请求数是400,则抛弃0个。12:00:16秒——12:00:20,请求数是580个,则抛弃80个.
这样看来固定时间窗口算法是可以满足要求的,但是假如12:00:06——12:00:09有200个请求,12:00:09——12:00:10有300个请求,12:00:10——12:00:14有399个请求,
12:00:14——12:00:15只有一个请求,那么在12:00:09——12:00:14这段时间内一共有699个请求,多出来199个请求,此时后台服务器是撑不住的。因此,固定时间窗口计数算法在现实中并不实用。【两个固定窗口相连的某段时间内请求量非常大

2 滑动时间窗口计数算法

假设项目需求是后台服务在一秒内处理100个请求,滑动时间窗口计数算法就是每100毫秒设置一个时间区间,每个时间区间统计该区间内的请求数量,然后每10个时间区间合并计算请求总数,
请求数超出最大数量时就把多余的请求数据抛弃,当时间节点进入下一个区间(比如第11个区间)时,便不再统计第1个区间的请求数量,而是将第2~11个区间的请求数量进行合并来计算出一个总数,并以此类推,

滑动时间窗口算法通过将单位时间进行细化,大大减少像固定时间窗口末端与下一个首端请求数超出且没有被发现的机率。但是对于细化后的单个时间内有个单位时间内请求直接拉满,导致后面剩下的单位时间内一个请求都处理不了。这种在秒杀场景中,通过机器人操作,请求可能一瞬间全部占用了,认为操作在后面就不会被处理了。因此,不能把所有请求处理权在那一瞬全部给机器人。

3 漏桶算法:

漏桶算法的实现步骤分为3步:
任意请求进来后,直接进入漏桶队列
以特定的速度处理漏桶队列里面的请求
超出漏桶负载范围的请求(桶满)直接抛弃掉,无法进入排队队列。
该算法需要根据请求的处理速度设置漏桶队列的大小不然机器人的请求还是会一下子把桶填满
由于桶里面的请求是按照先进先出进行处理的,所以后面的请求很可能会被丢弃。

4 令牌桶算法:

按照特定的速度产生令牌(tokean)并存放在令牌桶中。如果桶满,则不再产生新的令牌。
新进来的请求如果需要处理,则需要消耗桶中的一个令牌。
如果桶中有令牌,则直接消耗一个。
如果桶没有令牌,进入一个队列中等待新的令牌。
如果等待令牌的队列也满了,新请求就会直接被抛弃。

方案实现:
使用令牌桶还是漏桶?
漏桶算法的流入速度不确定,桶大小固定,流出速度固定,当服务器空闲,并且桶满时,其实是可以处理一次洪峰的,但限于流出速率,所以浪费了资源,并且整体处理效率降低了。
令牌桶算法则不存在这个问题,服务器处理请求越快,令牌消耗越快,此时,只需要保证令牌桶是满的即可

在Nginx还是在网关层中实现限流?

Nginx中有一个限流插件,可以对单个用户的请求数做限制,这种做法是基于漏桶算法的当然也可以实现一个动态调整限流的相关配置界面,通过界面直接管理Nginx的配置。
另一种就是在网关层实现限流,在网关层实现限流的方式有两种:分布式限流和统一限流
网关层也是有负载均衡的,多个网关服务器可以共享一个令牌桶【统一限流】,也可以每个节点拥有自己的令牌桶【分布式限流】。
如果使用统一限流的方式,可以把令牌桶的数据存放在redis中,每次请求会先访问redis,这种在秒杀场景下可能导致redis崩溃,限流就会失败,后台服务器就会被拖垮。
如果使用分布式限流,假如有些网关服务器节点失效了,那么其它节点还是可以正常工作的,但是可能导致部分网关层的负载增加。当然统一限流也存在这个问题。

使用哪些开源技术实现限流

Google-Guava中的RateLimiter的相关类可以实现限流。这些类是基于令牌桶的算法来实现的。在使用RateLimiter的过程中,需要配置以下3项。
PermitsPerSecond:每秒允许的请求数
warmupPeriod: 令牌桶多久满
tryAcquire: 当令牌桶为空时,可以等待新的令牌多长时间

posted @ 2024-02-05 17:12  小兵要进步  阅读(633)  评论(0编辑  收藏  举报