【SpringCloud】CircuitBreaker断路器之Resilience4J快速入门
文章目录
- 【SpringCloud】CircuitBreaker断路器之Resilience4J快速入门
- 1. 概述
- 2. 服务熔断+服务降级(CircuitBreaker)
- 2.1 案例说明
- 2.1.1 基于计数的滑动窗口
- 2.1.2 测试
- 2.2.1 基于时间的滑动窗口
- 2.2.2 测试
- 3. 隔离(BulkHead)
- 3.1 信号量舱壁
- 3.2 测试
- 3.3 固定线程池舱壁
- 4. 限流(RateLimiter)
- 4.1 测试
1. 概述
官网地址:[点击跳转](CircuitBreaker (readme.io))
中文手册:点击跳转
Resilience4J主要模块如下所示:
2. 服务熔断+服务降级(CircuitBreaker)
断路器有三种状态,状态之间的转换有两种策略(如下所示):
断路器主要配置参数如下所示:
其他参数可参考:官方文档
2.1 案例说明
2.1.1 基于计数的滑动窗口
使用基于计数的滑动窗口:
1)编写服务提供方接口如下:
@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();
}
}
提示:id为-4时,模拟服务提供方接口报错情况;id为9999时,模拟接口阻塞情况;id为其他值时模拟正常情况。
2)在OpenFeign接口中编写接口方法:
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
/**
* Resilience4j CircuitBreaker 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id);
}
3)在服务调用方引入依赖:
<!--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配置(屏蔽了其他配置,只写了circuitBreaker相关配置):
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
# 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)编写服务调用方接口:
@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ㄒ)/~~";
}
}
到此为止,案例编写成功,接下来测试。
2.1.2 测试
http://localhost/feign/pay/circuit/1
接口和 http://localhost/feign/pay/circuit/-4
来回各调用三次,失败率就达到了50%,触发了断路器功能,状态由 CLOSED
转为 OPEN
。5s后状态由 OPEN
转为 HALF_OPEN
。
正常调用:
失败调用,触发降级方法:
触发断路器,状态为 OPEN
时调用:
2.2.1 基于时间的滑动窗口
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
# 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
2.2.2 测试
测试慢调用是否会触发断路器的断路,连续调用 http://localhost/feign/pay/circuit/9999
接口,再调用一次正常接口看是否触发降级服务。
正常调用接口:
连续调用阻塞接口后:
测试成功。
3. 隔离(BulkHead)
Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量,即限制对于下游服务的最大并发数量。
SemaphoreBulkhead
使用了信号量FixedThreadPoolBulkhead
使用了有界队列和固定大小线程池
SemaphoreBulkhead
可以在各种线程和I/O模型上正常工作。与Hystrix不同,它不提供基于shadow的thread选项。由客户端来确保正确的线程池大小与隔离配置一致。
3.1 信号量舱壁
1)在服务提供方编写测试接口:
//=========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();
}
2)在公用模块编写Feign接口:
/**
* Resilience4j Bulkhead 的例子
* @param id
* @return
*/
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);
3)在服务调用方添加依赖:
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
4)配置yml:
信号量模式有如下两个核心参数:
配置属性 | 默认值 | 描述 |
---|---|---|
maxConcurrentCalls | 25 | 隔离允许线程并发执行的最大数量 |
maxWaitDuration | 0 | 当达到并发调用数量时,新的线程执行时将被阻塞,这个属性表示最长的等待时间。 |
示例yml配置如下:
####resilience4j bulkhead 的例子
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
cloud-payment-service:
baseConfig: default
timelimiter:
configs:
default:
timeout-duration: 20s
5)编写服务调用方测试接口:
/**
* (船的)舱壁,隔离
*
* @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ㄒ)/~~";
}
3.2 测试
根据上面的配置,接口必定只能有两个线程同时调用,那么我们用浏览器两个窗口访问 http://localhost:80/feign/pay/bulkhead/9999
接口,那么就能够达到最大并发,然后再开一个窗口访问 http://localhost:80/feign/pay/bulkhead/1
,这个接口本来是正常的,但是会因为触发了隔离机制调用降级方法。
测试结果如下:
3.3 固定线程池舱壁
1)引依赖
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
2)配置yml:
重要参数如下:
####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 新启线程和原来主线程脱离
3)编写服务调用方测试接口:
/**
* (船的)舱壁,隔离,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ㄒ)/~~");
}
4. 限流(RateLimiter)
1)编写服务提供方测试接口:
//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id) {
return "Hello, myRatelimit欢迎到来 inputId: " + id + " \t " + IdUtil.simpleUUID();
}
2)编写公用模块feign接口:
/**
* Resilience4j Ratelimit 的例子
*
* @param id
* @return
*/
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);
3)引入依赖:
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
4)yml配置
限流重要参数如下:
属性 | 默认值 | 描述 |
---|---|---|
timeoutDuration | 5秒 | 线程等待权限的默认等待时间 |
limitRefreshPeriod | 500纳秒 | 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod。 |
limitForPeriod | 50 | 在一次刷新周期内,允许执行的最大请求数 |
yml配置示例如下:
####resilience4j ratelimiter 限流的例子
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
baseConfig: default
5)编写服务调用方接口:
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service", fallbackMethod = "myRatelimitFallback")
public String myRateLimiter(@PathVariable("id") Integer id) {
return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id, Throwable t) {
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}
4.1 测试
按照上面的配置,一秒能够处理2个请求,我们访问接口并疯狂刷新必能触发限流: