1. Resilience4J 简介
Netflix Hystrix断路器是Spring Cloud中最早九开始支持的一种服务调用容错解决方案,但是目前的Hystrix已经处于维护模式,虽然并不影响已经上线的项目,并且短期内,可以继续使用Hystrix。但是长远来看,
处于维护状态的Hystrix走下历史舞台只是一个时间问题,特是是在Spring Cloud Greenwich
版本中,官方已经给出了Hystrix的建议替代方案,如下图:
Resilience4j它专为Java8 和函数式编程而设计轻量级容错框架,因为它的库只是用Vavr(以前称为Javaslang),它没有任何其他外部库依赖项。相比之下,Netflix Hystrix对Archaius具有编译依赖性,这导致了更多的外部依赖,例如Guava和Apache Commons。而如果使用Resilience4j,无需引用全部一粒啊,可以根据自己需要的功能引用相关的模块即可。
Resilience4j提供了提供了一组高阶函数(装饰器),包括断路器,限流器,重试机制,隔离机制。你可以使用其中的一个或多个装饰器对函数式接口,lambda表达式或方法引用进行装饰。这么做的优点是你可以选择所需要的装饰器进行装饰。
在使用Resilience4j的过程中,不需要引入所有的依赖,只引入需要的依赖即可。
常见服务熔断组件对比
常用的服务熔断,服务降级框架主要有Spring Cloud Hystrix
、Spring Cloud Alibaba Sentinel
、resilience4j
,下面通过几个维度综合对比一下各自的特点,便于加深对各种组件的认识。
对比项 | Sentinel | Hystrix | resilience4j |
---|---|---|---|
开源 | Apache-2.0 license | Apache-2.0 license | Apache-2.0 license |
更新 | 更新频繁(latest:2.1.1, Aug 8th 2022) | 已停更 | 更新较慢(latest:1.7.1, Jun 25th 2021) |
特点 | 轻量级,核心库无多余依赖,性能损耗小 | – | 基于Java8和函数式编程,轻量级容错库,无多余依赖 |
隔离策略 | 信号量隔离 | 信号量/线程池隔离 | 信号量隔离 |
熔断降级策略 | 异常比率/响应时间/异常数 | 异常比率 | 异常比率/响应时间 |
控制台 | 实时监控、机器发现、规则管理等能力 | 提供监控查看 | 不提供 |
系统自适应 | 支持,结合应用的机器负载、CPU 使用率,整体平均响应时间、入口 QPS 和 并发线程数等维度进行限流操做 | 不支持 | 不支持 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS、调用关系的限流 | 有限的支持 | 支持简单的Rtate Limiter模式 |
2. 核心模块
resilience4j-circuitbreaker
: 熔断,一种故障处理机制,用于防止故障在系统中蔓延当一个服务出现故障时,熔断器会立即终端对服务的请求,防止服务的故障传播到其他的组件,通过熔断机制,系统可以快速发现问题并做出相应的处理,避免系统整体崩溃
resilience4j-retelimiter
: 限流,一种控制请求流量的机制,用于保护系统免受过载的影响通过限制请求的频率货数量,可以确保系统在处理请求是不会超出器承受范围,限流可以有效的平滑系统的负载,并防止突发流量对系统的影响
resilience4j-bulkhead
: 隔离,通过限制同时执行的请求数量,保护系统的部分资源不被耗尽并发控制器(舱壁,Bulkhead),是用来控制并行(parallel)调用的次数。Resilience4j提供了两种舱壁模式的实现,可用于限制并发执行的次数
resilience4j-retry
: 自动重试(同步、异步),在请求失败时自动重试一定次数,增加系统的可靠性。
resilience4j-cache
: 结果缓存resilience4j-timelimiter
: 超时处理,设置请求的最大执行时间,防止请求长时间阻塞。
- 附加模块
- resilience4j-retrofit: Retrofit 适配器
- resilience4j-feign: Feign 适配器
- resilience4j-consumer: 循环缓冲区事件消费者
- resilience4j-kotlin: Kotlin 协程支持
- 框架模块
- resilience4j-spring-boot: Spring Boot Starter
- resilience4j-spring-boot2: Spring Boot 2 Starter
- resilience4j-ratpack: Ratpack Starter
- resilience4j-vertx: Vertx Future decorator
- 反应式模块
- resilience4j-rxjava2: 定制的 RxJava2 操作符
- resilience4j-reactor: 定制的 Spring Reactor 操作符
- 监控模块
- resilience4j-micrometer: Micrometer Metrics exporter
- resilience4j-metrics: Dropwizard Metrics exporter
- resilience4j-prometheus: Prometheus Metrics exporter
2.1 Circuit breaker熔断器
熔断器Circuit Breaker目的是保护分布式系统免受故障和异常,提供系统的可用性和健壮性。
具体来说,当一个组件或服务出现故障时,circuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务,防止该组件或服务进一步崩溃,并影响整个系统的运行。
熔断器通过有效状态机实现,有三个普通状态:CLOSED
、OPEN
、HELF_OPEN半开
,还有两个特殊状态:DISABLED禁用
、FORCED_OPEN强制开启
- 关闭(Closed)状态;
默认情况下Circuit Breaker是关闭的,代表正常情况下的状态,允许所有请求通过,能通过状态转换为OPEN;;
Circuit Breaker内部记录着最近失败的次数,如果对应的操作执行失败,次数就会续一次;
如果在某个时间段内,失败次数(或者失败比率)达到阈值,Circuit Breaker会转换到开启(Open)状态;
在开启状态中,CircuitBreaker会启用一个超时计时器,设这个计时器的目的是给集群相应的时间来恢复故障。当计时器时间到的时候,Circuit Breaker会转换到半开启(Half-Open)状态。
开启(Open);
- 在此状态下,执行对应的操作将会立即失败并且立即抛出异常。
半开启(Half-Open);
- 在此状态下,Circuit Breaker 会允许执行一定数量的操作,如果所有操作全部成功,Circuit Breaker就会假定故障已经恢复,它就会转换到关闭状态,并且重置失败次数;
- 如果其中 任意一次 操作失败了,Circuit Breaker就会认为故障仍然存在,所以它会转换到开启状态并再次开启计时器(再给系统一些时间使其从失败中恢复)。
禁用(DISABLED)
- 禁用状态,即允许所有请求通过,出现失败率达到给定的阈值也不会熔断,不会发生状态转换
强制开启FORCED_OPEN
- 与DISABLED状态正好相反,启用CircuitBreaker,但是不允许任何请求通过,不会发生状态转换
下面再对closed、open和half_open 这三种状态的切换做下补充:
closed -> open
: 关闭状态到熔断状态,当失败的调用率(比如超时、异常等)默认50%,达到一定的阈值服务转为open状态,在open状态下,所有的请求都被拦截;
open-> half_open
: 当经过一定的时间后,CircubitBreaker中默认为60s服务调用者允许一定的请求到达服务提供者;half_open -> open
: 当half_open状态的调用失败率超过给定的阈值,转为open状态;
half_open -> closed
: 失败率低于给定的阈值则默认转换为closed状态;
2.2 Bulkhead并发控制器
Bulkhead,即并发控制器(舱壁,Bulkhead),是用来控制并行(parallel)调用的次数。Resilience4j提供了两种舱壁模式的实现,可用于限制并发执行的次数:
SemaphoreBulkhead(信号量舱壁,默认),基于Java并发库中的Semaphore实现;
FixedThreadPoolBulkhead(固定 线程池舱壁),它使用一个有界队列和一个固定线程池
由于基于信号量的Bulkhead能很好地在多线程和I/O模型下工作,所以选择介绍基于信号量的Bulkhead的使用
3. 代码工程
实现断路器功能
引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.0</version>
</dependency>添加yaml配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58spring:
application.name: resilience4j-demo
jackson.serialization.indent_output: true
management:
endpoints.web.exposure.include:
- '*'
endpoint.health.show-details: always
health.circuitbreakers.enabled: true
## 断路器配置
resilience4j.circuitbreaker:
instances:
backendA:
registerHealthIndicator: true # 是否启用健康检查
slidingWindowSize: 10 #用于计算失败率的滑动窗口大小为10,即最近10次调用失败的情况会被考虑进去
permittedNumberOfCallsInHalfOpenState: 3 # 断路器半开时允许最大的请求次数
slidingWindowType: TIME_BASED #配置用于在CircuitBreaker关闭时记录调用结果的滑动窗口类型。 滑动窗口可以是基于计数或基于时间的。
minimumNumberOfCalls: 5 # 熔断器开始计算失败率之前,至少需要的调用次数为5次
waitDurationInOpenState: 5s # 断路器打开后,尝试等待5秒进入半开状态
failureRateThreshold: 20 # 当失败率达到20%时,断路器会打开,组织进一步的调用
eventConsumerBufferSize: 10 #用于存储断路器相关事件的缓冲区大小为10,这些事件可用于被监控
#重试策略相关的配置
resilience4j.retry:
instances:
backendA:
maxAttempts: 3 #最大重试次数
waitDuration: 2s #每次重试的时候间隔的等待时间
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.lang.Exception
resilience4j.bulkhead:
instances:
backendA:
maxConcurrentCalls: 10
resilience4j.thread-pool-bulkhead:
instances:
backendC:
maxThreadPoolSize: 11 #配置最大线程池大小
coreThreadPoolSize: 1 #配置核心线程池大小
queueCapacity: 1 #配置队列的容量
#限流的配置
resilience4j.ratelimiter:
instances:
backendA: # 限流器的名字
limitForPeriod: 1 # 一个限制周期内可访问次数
limitRefreshPeriod: 1s # 限制周期,每个周期之后,速率限制器将重置回limitForPeriod值
timeoutDuration: 10ms # 线程等待允许执行时间
registerHealthIndicator: true
eventConsumerBufferSize: 100
server:
port: 8081自定义配置类
将配置文件中的每一种熔断器赌赢的项都注册到spring的bean容器种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
import io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigCustomizer;
import org.springframework.context.annotation.Bean;
public class ResilienceConfig {
public CircuitBreakerConfigCustomizer circuitBreakerConfigCustomizer() {
return CircuitBreakerConfigCustomizer
.of("backendA", builder -> builder.slidingWindowSize(10));
}
public RateLimiterConfigCustomizer rateLimiterConfigCustomizer() {
return RateLimiterConfigCustomizer
.of("backendA", builder -> builder.limitForPeriod(1));
}
}测试
在这里我们测试两个场景,第一个场景为重试,第二个场景为限流,对于下面的两个方法做如下的补充说明:
- @CircuitBreaker,该注解里面有两个属性,name和fallbackMethod;
- name,即配置文件中配置的那个实例名称;
- fallbackMethod,出现错误时使用哪个降级方法;
- @Retry,与@CircuitBreaker注解相同,也是两个相同的属性,具有类似的含义;
- 一般来说,@CircuitBreaker注解和@Retry可以搭配使用,也可以单独使用@Retry;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import com.congge.entity.User;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Objects;
public class UserService {
private Logger logger = LoggerFactory.getLogger(UserService.class);
// @RateLimiter(name = "backendA", fallbackMethod = "fallback")
public User retry() {
logger.info("backendA findUser");
String res = new RestTemplate().getForObject("http://localhost:8088", String.class);
if (Objects.nonNull(res)) {
return new User("remote user", 18, "Man");
}
return new User("default user", 22, "Woman");
}
public User limit() {
return new User("remote user", 18, "Man");
}
public User fallback(Throwable e) {
return new User("降级的用户", 18, "D");
}
}控制器类用于测试使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserController {
private UserService userService;
public User retry() {
return userService.retry();
}
public User limit() {
return userService.limit();
}
}- @CircuitBreaker,该注解里面有两个属性,name和fallbackMethod;
接口测试
重试功能测试,在第一个方法中,远程调用一个不存在的链接,理论上会失败,按照参数配置,调用失败后,会重试3次,如果3次之后仍然失败,则接口抛出异常(或使用降级的方法的返回结果)。
调用限流接口,在配置文件中默认的是每秒允许通过一个请求,当我们正常请求接口时,可以正常得到结果
快速刷接口时,由于方法中添加了降级方法,将会得到降级的响应结果
4. 扩展
通过上面的案例演示了使用Resilience4j进行接口的重试或限流操作,只需配置好核心的参数,并且在方法上添加相关的注解即可。
在实际开发中,对Resilience4j的使用,也是重点落在对配置文件中3个模块配置参数的调整,了解这些配置参数的具体含义才能结合实际场景合理使用,具体来说,主要是下面几部分:
- circuitbreaker,断路器配置;
- retry,重试配置;
- ratelimiter,限流配置;
circuitbreaker常用配置参数
配置参数 | 默认值 | 描述 |
---|---|---|
failureRateThreshold | 50 | 熔断器关闭状态和半开状态使用的同一个失败率阈值 |
ringBufferSizeInHalfOpenState | 10 | 熔断器半开状态的缓冲区大小,会限制线程的并发量,例如缓冲区为10则每次只会允许10个请求调用后端服务 |
ringBufferSizeInClosedState | 100 | 熔断器关闭状态的缓冲区大小,不会限制线程的并发量,在熔断器发生状态转换前所有请求都会调用后端服务 |
waitDurationInOpenState | 60s | 熔断器从打开状态转变为半开状态等待的时间 |
automaticTransitionFromOpenToHalfOpenEnabled | false | 如果置为true,当等待时间结束会自动由打开变为半开,若置为false,则需要一个请求进入来触发熔断器状态转换 |
recordExceptions | empty | 需要记录为失败的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |
slidingWindowSize | 100 | 如果滑动窗口是 COUNT_BASED,则记录并汇总最后一次调用。 如果滑动窗口是TIME_BASED,则记录并汇总最后几秒的调用。slidingWindowSize配置用于在 CircuitBreaker 关闭时记录调用结果的滑动窗口的大小。 |
ratelimiter常用配置参数
配置参数 | 默认值 | 描述 |
---|---|---|
timeoutDuration | 5s | 线程等待权限的默认等待时间 |
limitRefreshPeriod | 500ms | 权限刷新的时间,每个周期结束后,RateLimiter将会把权限计数设置为limitForPeriod的值 |
limiteForPeriod | 一个限制刷新期间的可用权限数 |
retry常用配置参数
配置参数 | 默认值 | 描述 |
---|---|---|
maxAttempts | 3 | 最大重试次数 |
waitDuration | 500ms | 固定重试间隔 |
intervalFunction | numberOfAttempts -> waitDuration | 用来改变重试时间间隔,可以选择指数退避或者随机时间间隔 |
retryOnResultPredicate | result -> false | 自定义结果重试规则,需要重试的返回true |
retryOnExceptionPredicate | throwable -> true | 自定义异常重试规则,需要重试的返回true |
retryExceptions | empty | 需要重试的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |