Spring Boot使用Resilience4j容错:熔断、重试、限时、限流、隔板

Resilience4j是一个轻量级、易于使用的轻量级“容错”包。它受Neflix Hystrix启发但只有一个依赖(Vavr),而不像Hystrix很多很多的依赖。

Resilience4j在“容错”方面提供了各种模式:断路器(Circuit Breaker)、重试(Retry)、限时器(Time Limiter)、限流器(Rate Limiter)、隔板(BulkHead)。

在Spring Boot下,Resilience4j比Hystrix更适合用在容错的各种模式下。我们只要在程序中使用简单的注解即可实现。

那我们新建一个演示项目来演示功能:

暂时的依赖只需要Spring Web、Spring Boot Actuator。

1、项目准备

1. 1 添加Resilience4j依赖

在Spring Boot下使用Resilience4j,需要加上下面两个依赖

implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
implementation 'org.springframework.boot:spring-boot-starter-aop'

    io.github.resilience4j
    resilience4j-spring-boot2
		1.7.1



    org.springframework.boot
    spring-boot-starter-aop


2、断路器(Circuit Breaker)

断路器来自于生活中的断路器,是指当电流超过规定值时,以本身产生的热量使熔体熔断,断开电路的一种电器。

软件开发中的断路器有三个状态:

关闭(CLOSED):正常情况,所有的请求都正常通过断路器,没有任何限制。

打开(OPEN):在过去的请求或者时间中,如果故障或者慢的响应率大于或者等于一个配置的阈值,断路器就会打开。在这种情况下,所有的请求都会受到限制。

半开(HALF_OPEN):在打开状态下经过可配置的等待时间后,断路器允许少量(数值可配置)的请求通过。若失败/慢响应超过阈值,断路器重新打开;低于阈值,则断路器进入关闭状态。

断路器通过限制上游服务调用,在下游服务在部分或者全部停止服务的情况下,对下游服务进行保护。

我们新建控制器,并在当前的控制器上编写断路器的相关代码:

@RestController
public class Resilience4jController {

    private final RestTemplate restTemplate;

    public Resilience4jController(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }


    @GetMapping("/circuit-breaker")
    @CircuitBreaker(name = "circuitBreakerDemo")
    public String circuitBreaker(){
        return restTemplate.getForObject("http://no-such-site/api/external", String.class);
    }
}

我们在控制器方法circuitBreaker() 调用下游的服务,本例演示中的下游服务并不存在。

我们可以简单的在方法上使用@CircuitBreaker 注解即可使用Resilience4j提供的断路器,name 中定义的circuitBreakerDemo 是当前断路器的名称。

我们可以通过application.properties 对断路器的具体行为进行配置,instances 后面的名称是在@CircuitBreaker 中配置的名字:

resilience4j.circuitbreaker.instances.circuitBreakerDemo.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.circuitBreakerDemo.sliding-window-size=10
resilience4j.circuitbreaker.instances.circuitBreakerDemo.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.circuitBreakerDemo.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.circuitBreakerDemo.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.circuitBreakerDemo.wait-duration-in-open-state=5s

1、sliding-window-type :断路器的滑动窗口期类型可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。

2、failure-rate-threshold :设置50%的调用失败时打开断路器。

3、sliding-window-size :若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器;若为TIME_BASED则,此时有额外的两个设置属性,含义为:在*秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过2秒(slow-call-duration-threshold)打开断路器。

resilience4j.circuitbreaker.instances.circuitBreakerDemo.slow-call-rate-threshold=100
resilience4j.circuitbreaker.instances.circuitBreakerDemo.slow-call-duration-threshold=2000

4、permitted-number-of-calls-in-half-open-state :运行断路器在HALF_OPEN状态下时进行3次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。

5、minimum-number-of-calls :在每个滑动窗口期,配置断路器计算错误率或者慢调用率的最小调用数。本例中设置的5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。

6、wait-duration-in-open-state :一旦断路器是打开状态,它会拒绝请求5秒钟,然后转入半开状态。

更多关于断路器的配置可参考官方文档:https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker

2.1 断路器的Acturator

application.properties 里添加相关的配置:

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

#显示断路器的健康状态
management.health.circuitbreakers.enabled=true 
resilience4j.circuitbreaker.instances.circuitBreakerDemo.register-health-indicator=true

2.3 断路器的异常处理

当断路器处于打开和半开状态时,会抛出CallNotPermittedException 异常,我们可以来进行全局处理。

@RestControllerAdvice
public class Resilience4jExceptionHandler {
    @ExceptionHandler({ CallNotPermittedException.class })
    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    public String handleCallNotPermittedException() {
        return "调用不被允许";
    }
}

2.4 演示

启动程序,用postman访问:http://localhost:8080/circuit-breaker,调用5次。第6次,断路器被打开:


访问:http://localhost:8080/actuator/health

访问:http://localhost:8080/actuator/metrics

我们可以继续访问:

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.buffered.calls

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.calls

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.failure.rate

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.not.permitted.calls

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.slow.call.rate

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.slow.call

http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.state

http://localhost:8080/actuator/circuitbreakerevents

http://localhost:8080/actuator/circuitbreakerevents/{name}/{eventType}

http://localhost:8080/actuator/circuitbreakerevents/{name}

http://localhost:8080/actuator/circuitbreakers

http://localhost:8080/actuator/circuitbreakers/{name}


3、重试(Retry)

重试在远程服务调用时是必须的能力。我们在控制器中添加演示重试的控制器方法:

@RestController
public class Resilience4jController {

    private final RestTemplate restTemplate;

    public Resilience4jController(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }

    @GetMapping("/retry")
    @Retry(name = "retryDemo" , fallbackMethod = "fallback")
    public String retry() {
        return restTemplate.getForObject("http://no-such-site/api/external", String.class);
    }
  
    public String fallback(Exception exception){
        return "系统故障,无法访问";
    }

}

同样,我们可以用@Retry 注解轻松使用Resilience4j的重试能力。

同样,我们可以在application.properties 中配置重试。

resilience4j.retry.instances.retryDemo.max-attempts=3
resilience4j.retry.instances.retryDemo.wait-duration=1s
resilience4j.retry.metrics.legacy.enabled=true
resilience4j.retry.metrics.enabled=true

1、max-attempts:最大尝试重试次数。

2、wait-duration: 每次重试之间的间隔时间。

更多的重试配置,请参考官网:https://resilience4j.readme.io/docs/retry

3.1 演示

访问:http://localhost:8080/retry ,重试后,进入后备方法fallback

/actuator/metrics

/actuator/metrics/{requiredMetricName}

/actuator/retries

/actuator/retryevents

/actuator/retryevents/{name}

/actuator/retryevents/{name}/{eventType}

4、隔板(Bulkhead)

隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。

隔板(Bulkhead)可以用来限制对于下游服务的最大并发数量的限制。

我们在控制器中添加方法来演示隔板:

@GetMapping("/bulkhead")
@Bulkhead(name = "bulkheadDemo")
public String bulkhead() {
  return restTemplate.getForObject("https://reqres.in/api/users", String.class);
}

使用@Bulkhead 注解实现隔板功能,在application.properties 中配置隔板:

resilience4j.bulkhead.instances.bulkheadDemo.max-concurrent-calls=3
resilience4j.bulkhead.instances.bulkheadDemo.max-wait-duration=1
resilience4j.bulkhead.metrics.enabled=true

1、max-concurrent-calls :隔板允许的最大并发执行数量

2、max-wait-duration :当试图进入一个饱和的隔板时,线程应被阻断的最大时间。

Resilience4j的隔板支持两种类型:

默认的Semaphore:使用用户请求的线程,而不创建新的线程。

线程池:创建新的线程用来处理。配置如:

resilience4j.thread-pool-bulkhead.instances.bulkheadDemo.max-thread-pool-size= 3
resilience4j.thread-pool-bulkhead.instances.bulkheadDemo.core-thread-pool-size=2
resilience4j.thread-pool-bulkhead.instances.bulkheadDemo.queue-capacity=1

更多隔板的配置,参考官网:https://resilience4j.readme.io/docs/bulkhead

4.1 异常处理

当隔板饱和的时候会抛出BulkheadFullException 异常,添加到Resilience4jExceptionHandler

@ExceptionHandler({ BulkheadFullException.class })
@ResponseStatus(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)
public String handleBulkheadFullException() {
    return "隔板已满";
}

4.2 演示

用ApiFox访问:http://localhost:8080/bulkhead,并发设置为4,得到结果:

我们看下失败请求的明细:

/actuator/metrics

/actuator/metrics/{requiredMetricName}

/actuator/bulkheads

/actuator/bulkheadevents

/actuator/bulkheadevents/{name}

/actuator/bulkheadevents/{name}/{eventType}

5、限流器(RateLimiter)

限流器用来限制访问下游服务的速度。控制器中添加演示方法:

@GetMapping("/rate-limiter")
@RateLimiter(name = "rateLimitDemo")
public String rateLimit() {
    return restTemplate.getForObject("https://reqres.in/api/users", String.class);
}

同样限流只需要使用注解@RateLimiter 即可。在application.properties 添加配置:

resilience4j.ratelimiter.instances.rateLimitDemo.limit-for-period=5
resilience4j.ratelimiter.instances.rateLimitDemo.limit-refresh-period=60s
resilience4j.ratelimiter.instances.rateLimitDemo.timeout-duration=0s
resilience4j.ratelimiter.instances.rateLimitDemo.allow-health-indicator-to-fail=true
resilience4j.ratelimiter.instances.rateLimitDemo.subscribe-for-events=true
resilience4j.ratelimiter.instances.rateLimitDemo.event-consumer-buffer-size=50
resilience4j.ratelimiter.metrics.enabled=true
resilience4j.ratelimiter.instances.rateLimitDemo.register-health-indicator=true

上面的配置限制调用的限制是每60秒(limit-refresh-period )的访问次数是5次(limit-for-period )。

更多关于限流器的配置请参考官网:https://resilience4j.readme.io/docs/ratelimiter

5.1 异常处理

当达到允许的速度的时候,请求将会被拒绝,程序抛出RequestNotPermitted 异常。

@ExceptionHandler({ RequestNotPermitted.class })
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public String handleRequestNotPermitted() {
    return "请求不被允许";
}

5.2 效果

访问:http://localhost:8080/rate-limiter,在60秒内访问次数超过5次:

/actuator/health

/actuator/metrics

/actuator/metrics/{requiredMetricName}

/actuator/ratelimiters

/actuator/ratelimiterevents

/actuator/ratelimiterevents/{name}

/actuator/ratelimiterevents/{name}/{eventType}

6、限时器(TimeLimiter)

限时器用来限制在另外一个线程中执行的服务调用的时间。在控制器中新建方法,我们需要调用一个异步服务,即在另一个线程中执行的服务:

RestController
public class Resilience4jController {

    private final RestTemplate restTemplate;

    private final AsyncService asyncService;

    public Resilience4jController(RestTemplateBuilder builder, AsyncService asyncService) {
        this.restTemplate = builder.build();
        this.asyncService = asyncService;
    }

    @GetMapping("/time-limiter")
    @TimeLimiter(name = "timeLimiterDemo")
    public CompletableFuture timeLimiter() {
        return asyncService.doSomething();
    }

}

通过@TimeLimiter 注解使用显示器功能,我们程序还需使用@EnableAsync 开启异步的支持:

@SpringBootApplication
@EnableAsync
public class Resilience4jDemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(Resilience4jDemoApplication.class, args);
   }

}

异步服务:

@Service
public class AsyncService {

    private final RestTemplate restTemplate;

    public AsyncService(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }

    @Async
    public CompletableFuture doSomething()  {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return new AsyncResult(restTemplate.getForObject("https://reqres.in/api/users", String.class)).completable();
    }
}

在这个异步线程任务里,线程睡眠了3秒钟。

我们在application.properties中对限时器进行配置:

resilience4j.timelimiter.instances.timeLimiterDemo.timeout-duration=2s
resilience4j.timelimiter.instances.timeLimiterDemo.cancel-running-future=true
resilience4j.timelimiter.metrics.enabled=true

配置意味着当异步线程处理时间超过2s(timeout-duration )后将会限制访问。上面异步线程处理中线程睡了3秒,那请求必然超时。

更多关于限时器的配置,请查看官网:https://resilience4j.readme.io/docs/timeout

6.1 异常处理

当异步处理线程超时后,将会抛出TimeoutException 异常。

@ExceptionHandler({ TimeoutException.class })
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public String handleTimeoutException() {
    return "访问超时";
}

6.2 演示

访问:http://localhost:8080/time-limiter,提示访问超时。

/actuator/metrics

/actuator/metrics/{requiredMetricName}

/actuator/timelimiters

/actuator/timelimiterevents

/actuator/timelimiterevents/{name}

/actuator/timelimiterevents/{name}/{eventType}

感谢对我的书《从企业级开发到云原生微服务:Spring Boot实战》的支持。

转载请注明出处:今日头条:爱科学的卫斯理

(此处已添加书籍卡片,请到今日头条客户端查看)
展开阅读全文

页面更新:2024-03-21

标签:隔板   隔舱   阈值   断路器   注解   线程   下游   演示   异常   状态

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top