1.概述:
1.1.分布式系统面临的问题:
a.问题描述:
- 1.复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败,这或者就可以导致服务雪崩
b.什么是雪崩:
总的来说:
雪崩就是微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况
。
- 1.微服务中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微服务。如下图所示:
- 2.如上图,如果
服务提供者I
发生了故障,当前的应用的部分业务因为依赖于服务I,因此也会被阻塞。此时,其它不依赖于服务I的业务似乎不受影响。
- 3.但是,依赖服务I的业务请求被阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
- 4.服务器支持的线程和并发数有限,请求一直阻塞,会导致
服务器资源耗尽
,从而导致所有其它服务都不可用,那么当前服务也就不可用了。那么,依赖于当前服务的其它服务随着时间的推移,最终也都会变的不可用,形成级联失败
,雪崩
就发生了:
1.2.如何避免整个系统的大面积故障:
- 经常使用到的解决办法:
限流、超时处理、线程隔离、降级、熔断
- 限流是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。
- 超时处理、线程隔离、服务熔断,服务降级是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施
a.服务限流:
- 1.秒杀高并发等操作,严谨一窝蜂的拥挤,大家排队,一秒钟N个,有序进行
- 2.限流方法:
- 服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;.还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。.限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如
令牌桶算法
和漏牌桶算法
就是时间窗口的限流算法。 - 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
- 服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;.还可用于平滑请求,类似秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。.限流算法有两种,一种就是简单的请求总量计数,一种就是时间窗口限流(一般为1s),如
b.服务熔断:
- 1.在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会
暂时断开与下游服务的调用连接
。这种方式就是熔断。类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。服务熔断一般情况下会有三种状态:闭合、开启和半熔断:- 闭合状态(保险丝闭合通电OK):服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
- 开启状态(保险丝断开通电Error):上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
- 半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
- 2.由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 3.断路器会统计访问某个服务的请求数量、异常比例
- 4.当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成
熔断
:
- 类比保险丝,保险丝闭合状态可以正常使用,当达到最大服务访问后,
直接拒绝访问跳闸限电(OPEN)
,此刻调用方法会接受服务降级的处理并友好兜底提示;一句话,出故障了“保险丝”跳闸,别把整个家给烧了
c.服务降级:
- 1.提示服务器忙的时候,稍后再试一下,
不让客户端等待并立刻返回一个友好的提示
- 2.服务降级,
说白了就是一种服务托底方案
,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。 - 3.例如,在商品详情页一般都会展示商品的介绍信息,
一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面
。
d.服务超时处理:
- 1.只能在某段时间内访问,其他时间不可以访问
- 2.整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而
形成一个个调用链
。形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。服务超时就是在上游服务调用下游服务时,设置一个最大响应时间
,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源 - 3.设置超时时间:即设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
e.服务预热:
- 1.一开始放23个请求进来,然后再放50个,逐渐提高
f.服务隔离:
有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。互联网行业常用的服务隔离方式有:
线程池隔离和信号量隔离
,如下是介绍了线程格式的原理:
-
1.仓壁模式:来源于船舱的设计,船舱都会被隔板分离为多个独立空间,当船体破损时,只
会导致部分空间进入
,将故障控制在一定范围内,避免整个船体都被淹没
。
-
2.于此类似,我们
可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离
g.接近实时监控
h.兜底的处理动作
1.4.多种服务保护技术解决方案:
2.Hystrix:
2.1.Hystrix是什么:
- 1.Hystrix是一个用于
处理分布式系统的延迟和容错的开源库
,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
2.2.Hystrix停更:
2.3.Hystrix的平替方案:
3.Circuit Breaker是什么:
3.1.官网介绍:
- 1.官网
3.2.实现原理:
- 1.CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
- 2.当
一个组件或服务出现故障
时,CircuitBreaker
会迅速切换到开放OPEN
状态(保险丝跳闸断电),阻止请求
发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。 - 3.同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
3.3.一句话概括CircuitBreaker
CircuitBreaker只是一套规范和接口,落地实现者是Resilience4J
4.Resilience4J:
4.1.是什么
- 1.Github地址
4.2.能干嘛:
4.3.使用参考手册:
5.CircuitBreaker实际使用:
5.1.服务熔断 + 降级(CircuitBreaker):
a.断路器3大状态及之间的转换:
b.断路器所有配置参数参考:
b1.各个配置介绍:
b2.中文手册精简版:
b3.Resilience4j之默认配置CircuitBreakerConf类说明:
c.熔断+降级案例需求说明:
- 1.在6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
- 2.等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
- 3.如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。具体时间和频次等属性见具体实际案例,这里只是作为case举例讲解:
d.服务熔断和降级的实现方案的两种方式:
d1.计数的滑动窗口
1.修改
cloud-provider-payment8001订单服务
- 1.新建PayCircuitController类:
package com.atguigu.cloud.controller;
import cn.hutool.core.util.IdUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class PayCircuitController
{
//=========Resilience4j CircuitBreaker 的例子
@GetMapping(value = "/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id)
{
//模拟直接报错:
if(id == -4) {
throw new RuntimeException("----circuit id 不能负数");
}
// 模拟超时
if(id == 9999){
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 正常返回
return "Hello, circuit! inputId: "+id+" \t " + IdUtil.simpleUUID();
}
}
2.修改共用接口中的PayFeignApi接口:
- 1.修改的类文件位置:
- 2.添加支持Resilience4j CircuitBreaker的接口:
package com.atguigu.cloud.apis;
import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi
{
/**
* 新增一条支付相关流水记录
* @param payDTO
* @return
*/
@PostMapping("/pay/add")
public ResultData addPay(@RequestBody PayDTO payDTO);
/**
* 按照主键记录查询支付流水信息
* @param id
* @return
*/
@GetMapping("/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id);
/**
* openfeign天然支持负载均衡演示
* @return
*/
@GetMapping(value = "/pay/get/info")
public String mylb();
/**
* Resilience4j CircuitBreaker 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id);
}
3.在消费端(调用端)引入断路器:修改cloud-consumer-feign-order80:
- 1.改调用端的pom
<!--resilience4j-circuitbreaker-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- 由于断路保护等需要AOP实现,所以必须导入AOP包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4.改写改调用端的yml,配置断路器的规则:
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#cloud-payment-service:
#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒
connectTimeout: 20000
#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒
readTimeout: 20000
#开启httpclient5
httpclient:
hc5:
enabled: true
#开启压缩特性
compression:
request:
enabled: true
min-request-size: 2048
mime-types: text/xml,application/xml,application/json
response:
enabled: true
# 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。如果开了分组,值为true的时候,精确优先、分组次之(开了分组)、默认最后
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
# 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slidingWindowType: COUNT_BASED # 滑动窗口的类型
slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default
5.更改调用端的OrderCircuitController并添加
@CircuitBreaker注解
:
package com.atguigu.cloud.controller;
import com.atguigu.cloud.apis.PayFeignApi;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderCircuitController
{
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/circuit/{id}")
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
public String myCircuitBreaker(@PathVariable("id") Integer id)
{
return payFeignApi.myCircuit(id);
}
//myCircuitFallback就是服务降级后的兜底处理方法
public String myCircuitFallback(Integer id,Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
}
6.测试:
d2.基于时间滑动窗口:
1.时间滑动窗口实现原理:
2.修改cloud-consumer-feign-order80:
- 1.写yml:
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#cloud-payment-service:
#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒
connectTimeout: 20000
#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒
readTimeout: 20000
#开启httpclient5
httpclient:
hc5:
enabled: true
#开启压缩特性
compression:
request:
enabled: true
min-request-size: 2048
mime-types: text/xml,application/xml,application/json
response:
enabled: true
#开启circuitbreaker和分组激活
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug
# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
circuitbreaker:
configs:
default:
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
slidingWindowType: TIME_BASED # 滑动窗口的类型
slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
recordExceptions:
- java.lang.Exception
instances:
cloud-payment-service:
baseConfig: default
3.为了避免影响实验结果,记得关闭FeignConfig自己重试3次:
4.测试(慢查询):
- 1.一次超时,一次正常访问,同时进行
- 2.第1~4超时,整多一点干4个,一次正常访问,同时进行
总结:最好不要混用两种方案,
推荐使用按照调用次数count_baseed
5.2.案例实战–舱壁隔离(BulkHead):
a.舱壁隔离概述:
a1.官网:
- 2.中文网站
a2.舱壁隔离是什么:
隔离是用来
限制并发的
- 1.
bulkhead(船的)舱壁/(飞机的)隔板
:隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船
a3.舱壁隔离能干嘛:
- 可以用来进行依赖隔离和负载保护:
用来限制对于下游服务的最大并发数量限制
a4.Resilience4j提供的隔离方式:
a5.舱壁隔离中两种方式的原理:
1.信号量方式原理:
- 1.底层原理就是JUC中的信号灯部分
- 2.信号量舱壁(SemaphoreBulkhead)原理:基本上就是我们JUC信号灯内容的同样思想
- 当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
- 当信号量全被占用时,接下来的请求将会进入阻塞状态,
SemaphoreBulkhead提供了一个阻塞计时器
- 如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求
- 若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理
- 3.源码分析:
2.有界队列和固定大小线程池方式的底层原理:就是JUC中ThreadPool线程池部分:
b.案例实现基于信号量方式舱壁隔离来实现并发控制:
b1.修改8001支付微服务中的PayCircuitController,新增接口:
//=========Resilience4j bulkhead 的例子
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id)
{
if(id == -4){
throw new RuntimeException("----bulkhead id 不能-4");
}
if(id == 9999){
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
}
return "Hello, bulkhead! inputId: "+id+" \t " + IdUtil.simpleUUID();
}
b2.修改PayFeignApi接口,新增舱壁api方法
b3.修改cloud-consumer-feign-order80:
- 1.改pom
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
- 2.改YML配置:
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#cloud-payment-service:
#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒
connectTimeout: 20000
#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒
readTimeout: 20000
#开启httpclient5
httpclient:
hc5:
enabled: true
#开启压缩特性
compression:
request:
enabled: true
min-request-size: 2048
mime-types: text/xml,application/xml,application/json
response:
enabled: true
#开启circuitbreaker和分组激活
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug
####resilience4j bulkhead 的例子
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
cloud-payment-service:
baseConfig: default
timelimiter:
configs:
default:
timeout-duration: 20s
b4.业务类:
- 1.注解
@Bulkhead
和@Bulkhead.Type.SEMAPHORE
/**
*(船的)舱壁,隔离
* @param id
* @return
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id)
{
return payFeignApi.myBulkhead(id);
}
public String myBulkheadFallback(Throwable t)
{
return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
b5.测试:
可以看到因为本案例并发线程数为2(maxConcurrentCalls: 2),只让2个线程进入执行,其他请求降直接降级
5.3.实现固定线程池舱壁
a.概述:
- 1.固定线程池舱壁(FixedThreadPoolBulkhead)基本上就是我们JUC-线程池内容的同样思想
- FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是
用于限制并发执行的次数的
,但是二者的实现原理存在差别
而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列
来实现舱壁。 - 当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
- 当线程池中无空闲时时,接下来的请求将进入等待队列
- 若等待队列仍然无剩余空间时接下来的请求将直接被拒绝
- 在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
- 另外:
ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
- FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是
b.源码分析:
- 1.源码中相关的类:
- 2.底子就是JUC里面的线程池
- 3.submit进线程池返回
CompletableFuture<T>
:
c.修改80订单模块服务:
c1.改pom:
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
c2.改yml
- 1.示例:
最多可以容纳6个线程:max + queue = 4+ 3= 6;core在计算的时候不用管,因为max已经包含了core
- 2.改yml:
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
#cloud-payment-service:
#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒
connectTimeout: 20000
#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒
readTimeout: 20000
#开启httpclient5
httpclient:
hc5:
enabled: true
#开启压缩特性
compression:
request:
enabled: true
min-request-size: 2048
mime-types: text/xml,application/xml,application/json
response:
enabled: true
#开启circuitbreaker和分组激活
circuitbreaker:
enabled: true
# group:
# enabled: true # 演示Bulkhead.Type.THREADPOOL时spring.cloud.openfeign.circuitbreaker.group.enabled
设为false新启线程和原来主线程脱离了。
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug
####resilience4j bulkhead -THREADPOOL的例子
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #timelimiter默认限制远程1s,超过报错不好演示效果所以加上10秒
thread-pool-bulkhead:
configs:
default:
core-thread-pool-size: 1
max-thread-pool-size: 1
queue-capacity: 1
instances:
cloud-payment-service:
baseConfig: default
# spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离
c3.改80模块的controller:
/**
* (船的)舱壁,隔离,THREADPOOL
* @param id
* @return
*/
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadPoolFallback",type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id){
System.out.println(Thread.currentThread().getName()+"\t"+"enter the method!!!");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"exist the method!!!");
return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL");
}
public CompletableFuture<String> myBulkheadPoolFallback(Integer id,Throwable t){
return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
d.测试地址:
5.4.案例实战–限流(ReateLimiter):
a.概述:
a1.官网:
a2.是什么:
- 限流就是
进行频率控制
- 1.就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。 比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。
- 2.所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
b.面试题举例:说说常见的限流算法:
b1.漏斗算法(Leaky Bucket):
- 一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。
- 如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。
- 这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流量突发(burst)到端口速率。因此,
漏桶算法对于存在突发特性的流量来说缺乏效率
b2. 令牌桶算法:
- 1.SpringCloud默认使用的算法
b3. 滚动时间窗:
滚动时间窗定义解释:
- 1.允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。
- 2.由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次
2.滚动时间窗缺点:
- 1.缺点:间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮:
- 2.
由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击
- 3.假如设定1分钟最多可以请求100次某个接口,如12:00:00-12:00:59时间段内没有数据请求但12:00:59-12:01:00时间段内突然并发100次请求,紧接着瞬间跨入下一个计数周期计数器清零;在12:01:00-12:01:01内又有100次请求。那么也就是说在时间临界点左右可能同时有2倍的峰值进行请求,从而造成后台处理请求加倍过载的bug,导致系统运营能力不足,甚至导致系统崩溃,/(ㄒoㄒ)/~~
b4.滑动时间窗(sliding time window):
- 1.顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:
- 窗口:需要定义窗口的大小
- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
- 2.滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,
- 3.不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次
c.项目实战:
c1.cloud-provider-payment8001支付微服务修改PayCircuitController
- 1.新增Rest接口:
//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id)
{
return "Hello, myRatelimit欢迎到来 inputId: "+id+" \t " + IdUtil.simpleUUID();
}
c2.PayFeignApi接口新增限流方法:
/**
* Resilience4j Ratelimit 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);
c3.修改cloud-consumer-feign-order80:
- 1.POM文件
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
- 2.改YML:
####resilience4j ratelimiter 限流的例子
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
baseConfig: default
- 3.修改订单模块的controller:
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id){
return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id,Throwable t){
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}
c4.测试:
- 1.刷新上述地址,正常后F5按钮狂刷一会儿,停止刷新看到被限流的效果