Resilience4J 断路器

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 HystrixSpring Cloud Alibaba Sentinelresilience4j,下面通过几个维度综合对比一下各自的特点,便于加深对各种组件的认识。

对比项 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. 核心模块

  1. resilience4j-circuitbreaker: 熔断,一种故障处理机制,用于防止故障在系统中蔓延

    当一个服务出现故障时,熔断器会立即终端对服务的请求,防止服务的故障传播到其他的组件,通过熔断机制,系统可以快速发现问题并做出相应的处理,避免系统整体崩溃

  1. resilience4j-retelimiter: 限流,一种控制请求流量的机制,用于保护系统免受过载的影响

    通过限制请求的频率货数量,可以确保系统在处理请求是不会超出器承受范围,限流可以有效的平滑系统的负载,并防止突发流量对系统的影响

  1. resilience4j-bulkhead: 隔离,通过限制同时执行的请求数量,保护系统的部分资源不被耗尽

    并发控制器(舱壁,Bulkhead),是用来控制并行(parallel)调用的次数。Resilience4j提供了两种舱壁模式的实现,可用于限制并发执行的次数

  2. resilience4j-retry: 自动重试(同步、异步),在请求失败时自动重试一定次数,增加系统的可靠性。

  1. resilience4j-cache: 结果缓存

  2. resilience4j-timelimiter: 超时处理,设置请求的最大执行时间,防止请求长时间阻塞。

  1. 附加模块
  • resilience4j-retrofit: Retrofit 适配器
  • resilience4j-feign: Feign 适配器
  • resilience4j-consumer: 循环缓冲区事件消费者
  • resilience4j-kotlin: Kotlin 协程支持
  1. 框架模块
  • resilience4j-spring-boot: Spring Boot Starter
  • resilience4j-spring-boot2: Spring Boot 2 Starter
  • resilience4j-ratpack: Ratpack Starter
  • resilience4j-vertx: Vertx Future decorator
  1. 反应式模块
  • resilience4j-rxjava2: 定制的 RxJava2 操作符
  • resilience4j-reactor: 定制的 Spring Reactor 操作符
  1. 监控模块
  • resilience4j-micrometer: Micrometer Metrics exporter
  • resilience4j-metrics: Dropwizard Metrics exporter
  • resilience4j-prometheus: Prometheus Metrics exporter

2.1 Circuit breaker熔断器

熔断器Circuit Breaker目的是保护分布式系统免受故障和异常,提供系统的可用性和健壮性。

具体来说,当一个组件或服务出现故障时,circuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务,防止该组件或服务进一步崩溃,并影响整个系统的运行。

熔断器通过有效状态机实现,有三个普通状态:CLOSEDOPENHELF_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. 引入依赖

    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>
  2. 添加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
    58
    spring:
    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
  3. 自定义配置类

    将配置文件中的每一种熔断器赌赢的项都注册到spring的bean容器种

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
    import io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigCustomizer;
    import org.springframework.context.annotation.Bean;

    public class ResilienceConfig {

    @Bean
    public CircuitBreakerConfigCustomizer circuitBreakerConfigCustomizer() {
    return CircuitBreakerConfigCustomizer
    .of("backendA", builder -> builder.slidingWindowSize(10));
    }

    @Bean
    public RateLimiterConfigCustomizer rateLimiterConfigCustomizer() {
    return RateLimiterConfigCustomizer
    .of("backendA", builder -> builder.limitForPeriod(1));
    }
    }
  4. 测试

    在这里我们测试两个场景,第一个场景为重试,第二个场景为限流,对于下面的两个方法做如下的补充说明:

    • @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
    38
    import 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;

    @Service
    public class UserService {

    private Logger logger = LoggerFactory.getLogger(UserService.class);

    @CircuitBreaker(name = "backendA")
    @Retry(name = "backendA")
    // @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");
    }

    @CircuitBreaker(name = "backendA")
    @RateLimiter(name = "backendA",fallbackMethod = "fallback")
    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
    @RestController
    public class UserController {

    @Resource
    private UserService userService;

    @GetMapping("/retry")
    public User retry() {
    return userService.retry();
    }

    @GetMapping("/limit")
    public User limit() {
    return userService.limit();
    }
    }
  5. 接口测试
    重试功能测试,在第一个方法中,远程调用一个不存在的链接,理论上会失败,按照参数配置,调用失败后,会重试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 需要忽略的异常列表