【SpringCloud】4.Resilience 4J——服务熔断/服务降级、隔离、限流
CircuitBreaker 断路器 Resilience4j :实现CircuitBreaker规范 熔断(CricutBreaker,服务熔断+ 服务降级): 隔离(BulkHead): 限流(RateLimiter):
CircutBreaker断路器
概述
官网地址:https://spring.io/projects/spring-cloud-circuitbreaker
历史
Hystrix (豪猪哥)目前也进入维护模式。Hystrix是一个用于处理分布式系统的延迟和容错的开源库。Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
Hystrix官网也建议送Resilience4j替换
分布式系统面临问题——雪崩
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出” 。。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
禁止雪崩的解决方案
这么要禁止雪崩,有问题的节点快速熔断。至少有问题的服务出了故障以后,向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
一句话,出故障了“保险丝”跳闸,别把整个家给烧了,😄
避免大面积故障
如果在服务中大面积故障,我们需要做什么?
需要做的事情 | 说明 |
---|---|
服务熔断 | 类比家里的保险丝,从闭合CLOSE供应状态→Open打开状态 保险丝闭合状态(CLOSE)可以正常使用,当达到最大服务访问后,直接决绝跳闸限电(OPEN),此刻调用方法会接受服务降级的处理并返回友好兜底的提示。 |
服务降级 | 返回一个友好提示。(服务器忙,请稍后再试) |
服务限流 | 秒杀等高并发操作,不能一窝蜂的处理。大家排队,有序进行 |
服务限时 | |
服务预热 | |
接近实时的监控 | |
兜底的处理动作 | |
…… |
CircuitBreaker
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
CircuitBreaker状态: 关闭(CLOSED)、打开(OPEN)、半开(HALF_OPEN)。
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
Circuit Breaker知识一条规范和接口,落地实现者是Resilience4J。
Resilience 4J
官网说明:https://resilience4j.readme.io/docs/circuitbreaker
GitHub地址:https://github.com/resilience4j/resilience4j
中文手册:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md
Resilience4j 是一个Java™的容错框架。
简介
Resilience4j 是一个专为函数式编程设计的轻量级容错库。Resilience4j 提供高阶函数(装饰器),以便利用断路器、速率限制器、重试机制或舱壁来增强任何函数式接口、lambda 表达式或方法引用。你可以在任何函数式接口、lambda 表达式或方法引用上叠加多个装饰器。这样做的好处是,你能够自行选择所需的装饰器,而无需使用其他多余的东西。
Resilience4j 能做什么
Resilience4j 提供了以下功能模块:
- resilience4j-circuitbreaker: 断路 ⭐⭐⭐
- resilience4j-ratelimiter: 速率限制 ⭐⭐⭐
- resilience4j-bulkhead: 仓壁⭐⭐⭐
- resilience4j-retry: 自动重试(同步或异步)
- resilience4j-timelimiter: 超时处理
- resilience4j-cache: 结果缓存
Resilience4j 2 需要 Java 17 环境支持。
这里,我们只需要学习断路、速率限制和仓壁。
实战
完成改造的前提条件:
- 使用Maven新建父工程、子工程模块。并包含公共方法、支付模块(端口号8001)、客户子模块(openfeign)客户端子模块(80端口)。
- 项目具体的模块,可以参考:https://www.bilibili.com/video/BV1gW421P7RD?spm_id_from=333.788.videopod.episodes&vd_source=09d6df3c02085a7ba697583326012939&p=42 41集前内容。
- 已开启consul
- JDK大于JDK17
熔断(CricutBreaker,服务熔断+ 服务降级)
概念
官网:https://resilience4j.readme.io/docs/circuitbreaker
断路器的三种状态:关闭(CLOSED)、打开(OPEN)、半开(HALF_OPEN)
failure-rate-threshold | 以百分比配置失败率峰值 |
---|---|
sliding-window-type | 断路器的滑动窗口期类型可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。 |
sliding-window-size | 若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器;****若为TIME_BASED则,此时还有额外的两个设置属性,含义为:在N秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过N秒(slow-call-duration-threshold)打开断路器。 |
slowCallRateThreshold | 以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于峰值时,断路器开启,并进入服务降级。 |
slowCallDurationThreshold | 配置调用时间的峰值,高于该峰值的视为慢调用。 |
permitted-number-of-calls-in-half-open-state | 运行断路器在HALF_OPEN状态下时进行N次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。 |
minimum-number-of-calls | 在每个滑动窗口期样本数,配置断路器计算错误率或者慢调用率的最小调用数。比如设置为5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。 |
wait-duration-in-open-state | 从OPEN到HALF_OPEN状态需要等待的时间 |
计数滑动窗口
具体步骤如下:
- 被调用支付端(cloud-payment-service8001)端新增接口用于测试,及公共调用方法
- 修改调用微服务(支付端,cloud-openfeign-service80),完成配置
- 测试
被调用支付端(cloud-payment-service8001)端新增接口用于测试,及公共调用方法
新建PayCircuitController
和接口方法。这里,当参数为-4时(模拟调用失败);当参数为9999时等待5s;其他数据模拟正常调用。
/** * CricuitController测试接口 * @author lyj * {@code @create} 2024/12/17 08:36 */ public class PayCircuitController { @GetMapping(value = "/pay/circuit/{id}") public ResultData<String > myCircuit(@PathVariable("id") Integer id){ if (id == -4) { throw new RuntimeException("---xircuit id 不是负数"); } if (id == 9999) { try { TimeUnit.SECONDS.sleep(5); }catch (InterruptedException e){ e.printStackTrace(); } } return ResultData.success("hello, circuit!" + id + "\t" + IdUtil.simpleUUID()); } }
经测试,方法调用成功。
学习了openFeign可知:https://www.cnblogs.com/luyj00436/p/18588116 ,我们想要方法被其他接口调用,需要添加在公共方法(cloud-api-commons)的PayFeignApi接口添加公共的引用。
/** * 支付模块的feign接口 * @Author:lyj * @Date:2024/12/4 09:17 */ @FeignClient(value = "cloud-payment-service") public interface PayFeignApi { // …… 其他被调用的支付模块feign接口 /** * Resilience4j CircuitBreaker 的例子 * @param id * @return */ @GetMapping(value = "/pay/circuit/{id}") public ResultData<String > myCircuit(@PathVariable("id") Integer id); }
修改调用微服务(支付端,cloud-openfeign-service80),完成配置
调用微服务(消费者端,cloud-openfeign-service80)。需要做的事情包括:改POM、写YML、新建调用微服务接口。
改POM:我们在pom.xml方法下引用circuitbreaker 和aop
<!-- resilience4j-circuitbreaker--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-circuitbreaker-resilience4j</artifactId> </dependency> <!-- 由于断路保护等需要AOP,所以必须导入AOP包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
写YML : 配置计数滑动窗口
openFeign开启断路器和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
spring: cloud: openfeign: circuitbreaker: enabled: true group: enabled: true # 默认不开启分组。优先级(精确、分组、默认)
reslience4j的配置,可以详见前文。设置计数滑动窗口,最小样本容量为6,当50%调用失败,开启断路器。
# reslience4j resilience4j: circuitbreaker: configs: default: failure-rate-threshold: 50 # 设置50%的调用失败时打开断路器,超过失败请求百分比,CircuitBreaker变为OPEN状态 slow-call-duration-threshold: 2s # 设置慢调用阈值为2s sliding-window-type: COUNT_BASED # 滑动窗口类型 sliding-window-type: COUNT_BASED # 滑动窗口类型 sliding-window-size: 6 # 缓动窗口配置,COUNT_BASED 6 表示6个请求,配置TIME_BASED 6 表示6s minimum-number-of-calls: 6 # 断路器最小样本(6,表示至少计算6个样本,才计算调用失败) automatic-transition-from-open-to-half-open-enabled: true # 是否启用半开状态,默认为true wait-duration-in-open-state: 5s # 从OPEN到HALF_OPEN需要等待的时间 permitted-number-of-calls-in-half-open-state: 2 # 半开状态下允许的最大请求数,默认值10。如果其中任意一个请求失败,将重新进入开启状态 record-exceptions: # 捕获的异常种类 - java.lang.Exception instances: cloud-payment-service: base-config: default
为了验证慢调用,这里超时设置为20s。
spring: openfeign: client: config: default: # 指定最大超时时间3s→20s: read-timeout: 20000 # 指定连接最大时间3s→20s connect-timeout: 20000
注:需要移除重试FeignConfig次数设置(当然,不改也可以)。
@Configuration public class FeignConfig { /** * 重试 * @return */ @Bean public Retryer retryer(){ // Retryer.Default( long period, long maxPeriod, int maxAttempts) // return new Retryer.Default(100,1,3); return Retryer.NEVER_RETRY; } }
新建调用微服务接口OrderCircuitController
。
/** * Resilience4j CircuitBreaker例子 * @author lyj * @date:2024/12/18 09:36 */ @RestController public class OrderCircuitController { @Resource private PayFeignApi payFeignApi; @GetMapping(value = "/feign/pay/circuit/{id}") @CircuitBreaker(name = "cloud-payment-service",fallbackMethod = "myCircuitFallback") public ResultData<String> myCircuitBreaker(@PathVariable("id") Integer id) { return payFeignApi.myCircuit(id); } /** * myCircuitFallback就是服务降级后的兜底处理方法 * @param id * @param throwable * @return */ public ResultData<String> myCircuitFallback(@PathVariable("id") Integer id , Throwable throwable) { return ResultData.fail("408","myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~"); } }
测试
启动consul、支付模块、订单模块。测试正确调用的接口:http://localhost:80/feign/pay/circuit/1
,错误调用的接口():` http://localhost:80/feign/pay/circuit/9999。测试方法和结果:
- 一次调用错误,一次调用正确,超过最小样本(这里设置6次)后,触发服务降级,后续即使正确访问也无法调用服务;过渡到半开状态(5s)后,继续访问正确地址,即可正常访问
- 多次调用错误,后调用正确的地址,发现刚开始调用正确的地址也不能进行。
正确调用后,返回消息:
触发服务降级后,返回消息:
计时滑动窗口
计时滑动窗口设置如下:
resilience4j: timelimiter: configs: default: timeout-duration: 1s # timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑 circuitbreaker: configs: default: failure-rate-threshold: 50 # 设置50%的调用失败时打开断路器,超过失败请求百分比,CircuitBreaker变为OPEN状态 slow-call-duration-threshold: 2s # 设置慢调用阈值为2s slow-call-rate-threshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级 sliding-window-type: TIME_BASED # 滑动窗口类型 sliding-window-size: 2 # 缓动窗口配置,COUNT_BASED 6 表示6个请求,配置TIME_BASED 6 表示6s minimum-number-of-calls: 6 # 断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。 wait-duration-in-open-state: 5s # 从OPEN到HALF_OPEN需要等待的时间 permitted-number-of-calls-in-half-open-state: 2 # 半开状态下允许的最大请求数,默认值10。如果其中任意一个请求失败,将重新进入开启状态 record-exceptions: # 捕获的异常种类 - java.lang.Exception instances: cloud-payment-service: base-config: default
隔离(舱壁,BulkHead)
概念
官网地址:https://resilience4j.readme.io/docs/bulkhead
舱壁来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。
作用:限并发。依赖隔离和负载保护,用来限制对下游服务的最大并发数量的限制。
Resilience4j提供了两种限制并发的实现方式:
- SemaphoreBulkhead(信号量仓壁)
- FixedThreadPoolBulkhead(固定线程池仓壁)
SemaphoreBulkhead(信号量仓壁)
概述
信号量舱壁(SemaphoreBulkhead)原理
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
步骤
- 消费者8001支付微服务,新增舱壁测试api
- 公共方法PayFeignAPI接口新增舱壁api方法
- 修改订单模块
消费者8001支付微服务,新增舱壁测试api
消费者微服务cloud-payment-service
的PayCircuitController新增舱壁的测试接口。
/** * Resilience4j bulkhead 的例子 * @param id * @return */ @GetMapping(value = "/pay/bulkhead/{id}") public ResultData<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 ResultData.success("Hello, bulkhead! inputId: "+id+" \t " + IdUtil.simpleUUID()); }
公共方法PayFeignAPI接口新增舱壁api方法
/** * Resilience4j bulkhead 的例子 * @param id * @return */ @GetMapping(value = "/pay/bulkhead/{id}") public ResultData<String > myBulkhead(@PathVariable("id") Integer id);
修改订单模块
改PoM,费者端,我们需要新增舱壁的依赖。
<!-- resilience4j-bulkhead--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-bulkhead</artifactId> </dependency>
写YAML,消费者端,我们需要新增舱壁的信息。
reslience4j的将配置如下:
#开启circuitbreaker和分组激活 spring: cloud: circuitbreaker: enabled: true group: enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后 ####resilience4j bulkhead 的例子 resilience4j: bulkhead: configs: default: max-concurrent-calls: 2 # 隔离允许并发线程执行的最大数量 max-wait-duration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback instances: cloud-payment-service: base-config: default timelimiter: configs: default: timeout-duration: 20s
改业务类,业务类接口(OrderCircuitController
)增加舱壁的调用方法和兜底方法
/** *(船的)舱壁,隔离 * @param id * @return */ @GetMapping(value = "/feign/pay/bulkhead/{id}") @Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE) public ResultData<String> myBulkhead(@PathVariable("id") Integer id) { return payFeignApi.myBulkhead(id); } public ResultData<String> myBulkheadFallback(Throwable t) { return ResultData.fail("408", "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~"); }
测试
超长等待接口:http://localhost/feign/pay/bulkhead/9999
,不同接口:http://localhost/feign/pay/bulkhead/3
。测试两个耗时5s的请求,此时第3个正常方法,被限制隔离了;等窗口停止后,就可以正常访问了。
FixedThreadPoolBulkhead(固定线程池仓壁)
概述
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,
若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,
在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
步骤
- 消费者8001支付微服务,新增舱壁测试api
- 公共方法PayFeignAPI接口新增舱壁api方法
- 修改订单模块
消费者8001支付微服务模块,公共模块的方法与信号量舱壁一致,这里就不赘述了。
修改订单模块
改PoM,费者端,我们需要新增舱壁的依赖。
<!-- resilience4j-bulkhead--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-bulkhead</artifactId> </dependency>
写YAML,消费者端,我们需要新增舱壁的信息。
####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
修改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ㄒ)/~~"); }
限流(RateLimiter):
概念
常见的限流算法
- 漏斗算法
- 令牌桶算法(resilience4j中使用的就是此方法)
- 滚动时间窗口
- 滑动时间窗口
步骤
- 支付接口8001新增限流测试方法
- 公共模块PayFeignApi新增限流方法
- 订单模块修改
支付接口8001新增限流测试方法
/** * Resilience4j ratelimit 的例子 * @param id * @return */ @GetMapping(value = "/pay/ratelimit/{id}") public ResultData<String> myRatelimit(@PathVariable("id") Integer id) { return ResultData.success( "Hello, myRatelimit欢迎到来 inputId: "+id+" \t " + IdUtil.simpleUUID()); }
公共模块PayFeignApi新增限流方法
/** * Resilience4j ratelimit 的例子 * @param id * @return */ @GetMapping(value = "/pay/ratelimit/{id}") public ResultData<String> myRatelimit(@PathVariable("id") Integer id);
订单模块修改
具体步如下:
- 改POM
- 写YAML
- 改接口方法
改POM,引入限流的依赖
<!--resilience4j-ratelimiter--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> </dependency>
写YAML,配置限流参数
####resilience4j ratelimiter 限流的例子 resilience4j: ratelimiter: configs: default: limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数 limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod timeout-duration: 1 # 线程等待权限的默认等待时间 instances: cloud-payment-service: baseConfig: default
改接口方法,修改对应的OrderCircuitController方法。
@GetMapping(value = "/feign/pay/ratelimit/{id}") @RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback") public ResultData<String> myRatelimit(@PathVariable("id") Integer id) { return payFeignApi.myBulkhead(id); } public ResultData<String> myRatelimitFallback(Integer id,Throwable t) { return ResultData.fail("408","你被限流了,禁止访问/(ㄒoㄒ)/~~"); }
测试
正常访问刷新后会看到限流
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
2020-12-20 HBuilder X : 安装SVN插件
2020-12-20 HBuilder X 安装教程