微服务容错Resilence4j

1. 断路器(CircuitBreaker)

1.1 CircuitBreaker简介

断路器通过有限状态机实现,有三个普通状态:关闭(CLOSED)、开启(OPEN)、半开(HALF_OPEN),还有两个特殊状态:禁用(DISABLED)、强制开启(FORCED_OPEN)。如下图所示。

image.png

当熔断器关闭时,所有的请求都会通过熔断器。如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率,如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态。

断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。基于访问数量的滑动窗口统计了最近N次调用的返回结果。居于时间的滑动窗口统计了最近N秒的调用返回结果。

除此以外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)。这两个状态不会生成熔断器事件(除状态装换外),并且不会记录事件的成功或者失败。退出这两个状态的唯一方法是触发状态转换或者重置熔断器。

1.2 添加依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>
复制代码

1.3 CircuitBreaker配置

断路器配置属性,如下表所示

配置属性 默认值 描述
failureRateThreshold 50 以百分比配置失败率阈值。当失败率等于或大于阈值时,断路器状态并关闭变为开启,并进行服务降级。
slowCallRateThreshold 100 以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于阈值时,断路器开启,并进行服务降级。
slowCallDurationThreshold 60000 [ms] 配置调用时间的阈值,高于该阈值的呼叫视为慢调用,并增加慢调用比例。
permittedNumberOfCallsInHalfOpenState 10 断路器在半开状态下允许通过的调用次数。
maxWaitDurationInHalfOpenState 0 断路器在半开状态下的最长等待时间,超过该配置值的话,断路器会从半开状态恢复为开启状态。配置是0时表示断路器会一直处于半开状态,直到所有允许通过的访问结束。
slidingWindowType COUNT_BASED 配置滑动窗口的类型,当断路器关闭时,将调用的结果记录在滑动窗口中。滑动窗口的类型可以是count-based或time-based。如果滑动窗口类型是COUNT_BASED,将会统计记录最近slidingWindowSize次调用的结果。如果是TIME_BASED,将会统计记录最近slidingWindowSize秒的调用结果。
slidingWindowSize 100 配置滑动窗口的大小。
minimumNumberOfCalls 100 断路器计算失败率或慢调用率之前所需的最小调用数(每个滑动窗口周期)。例如,如果minimumNumberOfCalls为10,则必须至少记录10个调用,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
waitDurationInOpenState 60000 [ms] 断路器从开启过渡到半开应等待的时间。
automaticTransition FromOpenToHalfOpenEnabled false 如果设置为true,则意味着断路器将自动从开启状态过渡到半开状态,并且不需要调用来触发转换。创建一个线程来监视断路器的所有实例,以便在WaitDurationInOpenstate之后将它们转换为半开状态。但是,如果设置为false,则只有在发出调用时才会转换到半开,即使在waitDurationInOpenState之后也是如此。这里的优点是没有线程监视所有断路器的状态。
recordExceptions empty 记录为失败并因此增加失败率的异常列表。 除非通过ignoreExceptions显式忽略,否则与列表中某个匹配或继承的异常都将被视为失败。 如果指定异常列表,则所有其他异常均视为成功,除非它们被ignoreExceptions显式忽略。
ignoreExceptions empty 被忽略且既不算失败也不算成功的异常列表。 任何与列表之一匹配或继承的异常都不会被视为失败或成功,即使异常是recordExceptions的一部分。
recordException throwable -> true· By default all exceptions are recored as failures. 一个自定义断言,用于评估异常是否应记录为失败。 如果异常应计为失败,则断言必须返回true。如果出断言返回false,应算作成功,除非ignoreExceptions显式忽略异常。
ignoreException throwable -> false By default no exception is ignored. 自定义断言来判断一个异常是否应该被忽略,如果应忽略异常,则谓词必须返回true。 如果异常应算作失败,则断言必须返回false。

在订单项目中配置断路器,代码如下。

resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 30 #失败请求百分比,超过这个比例,CircuitBreaker变为OPEN状态
        slidingWindowSize: 10 #滑动窗口的大小,配置COUNT_BASED,表示10个请求,配置TIME_BASED表示10秒
        minimumNumberOfCalls: 5 #最小请求个数,只有在滑动窗口内,请求个数达到这个个数,才会触发CircuitBreader对于断路器的判断
        slidingWindowType: TIME_BASED #滑动窗口的类型
        permittedNumberOfCallsInHalfOpenState: 3 #当CircuitBreaker处于HALF_OPEN状态的时候,允许通过的请求个数
        automaticTransitionFromOpenToHalfOpenEnabled: true #设置true,表示自动从OPEN变成HALF_OPEN,即使没有请求过来
        waitDurationInOpenState: 2s #从OPEN到HALF_OPEN状态需要等待的时间
        recordExceptions: #异常名单
          - java.lang.Exception
    instances:
      backendA:
        baseConfig: default #熔断器backendA,继承默认配置default
      backendB:
        failureRateThreshold: 50
        slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的呼叫视为慢调用,并增加慢调用比例。
        slowCallRateThreshold: 30 #慢调用百分比阈值,断路器把调用时间大于slowCallDurationThreshold,视为慢调用,当慢调用比例大于阈值,断路器打开,并进行服务降级
        slidingWindowSize: 10
        slidingWindowType: TIME_BASED
        minimumNumberOfCalls: 2
        permittedNumberOfCallsInHalfOpenState: 2
        waitDurationInOpenState: 2s #从OPEN到HALF_OPEN状态需要等待的时间
复制代码

上面配置了2个断路器"backendA",和"backendB",其中backendA断路器配置基于default配置,"backendB"断路器配置了慢调用比例熔断,"backendA"熔断器配置了异常比例熔断。

4. OrderController

修改OrderController代码,以测试异常比例熔断和慢调用比例熔断效果,代码如下。

    @GetMapping("/payment/{id}")
    @CircuitBreaker(name = "backendD", fallbackMethod = "fallback")
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        log.info("now i enter the method!!!");

        Thread.sleep(10000L); //阻塞10秒,已测试慢调用比例熔断

        String url = "http://cloud-payment-service/payment/" + id;
        Payment payment = restTemplate.getForObject(url, Payment.class);

        log.info("now i exist the method!!!");

        return ResponseEntity.ok(payment);
    }

    public ResponseEntity<Payment> fallback(Integer id, Throwable e) {
        e.printStackTrace();
        Payment payment = new Payment();
        payment.setId(id);
        payment.setMessage("fallback...");
        return new ResponseEntity<>(payment, HttpStatus.BAD_REQUEST);
    }
复制代码

注意name="backendA"和name="backendD"效果相同,当找不到配置的backendD熔断器,使用默认熔断器配置,即为"default"。

1.5 启动并测试

分别启动Eureka,支付服务,订单服务。

使用JMeter并发测试,创建线程组,如下图所示。

image.png 创建HTTP请求、查看结果数,HTTP请求配置。

image.png 正常执行效果

image.png

此时关闭支付微服务,这时订单服务无法调用,所有请求报错,这时第一次并发发送20次请求,触发异常比例熔断。

image.png 断路器进入打开状态,2s后(waitDurationInOpenState: 2s),断路器自动进入半开状态(automaticTransitionFromOpenToHalfOpenEnabled: true),再次发送请求断路器处于半开状态,允许3次请求通过(permittedNumberOfCallsInHalfOpenState: 3),注意此时控制台打印3次日志信息,说明半开状态,进入了3次请求调用,接着断路器继续进入打开状态。如下图所示前3次请求通过。

image.png

image.png

image.png

以上是前三次时(permittedNumberOfCallsInHalfOpenState: 3)设置了处于HALF_OPEN状态时时允许通过3次的。

image.png

后续的都是说明CircuitBreaker 'backendA'处于半开状态不允许通过。

下来测试慢比例调用熔断,修改OrderController代码,使用"backendB"熔断器,因为backendB熔断器,配置了慢比例调用熔断,然后启动Eureka,订单微服务和支付微服务。第一次发送并发发送了20个请求,触发了慢比例熔断。

image.png

进入了10秒才开始退出。

但是因为没有配置(automaticTransitionFromOpenToHalfOpenEnabled: true),无法自动从打开状态转为半开状态,需要浏览器中执行一次请求,这时,断路器才能从打开状态进入半开状态,接下来进入半开状态

image.png

因此是进入了3次而不是两次,需要浏览器中执行一次请求,这时,断路器才能从打开状态进入半开状态。

根据配置,允许2次请求在半开状态通过(permittedNumberOfCallsInHalfOpenState: 2),第二次调用效果如下图所示。

image.png

2. 隔离(Builkhead)

Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量。

  • SemaphoreBulkhead使用了信号量
  • FixedThreadPoolBulkhead使用了有界队列和固定大小线程池

2.1 信号量隔离

2.1.1 添加依赖

        <dependency>
            <groupId>io.github.resilience4j</groupId>
            <artifactId>resilience4j-bulkhead</artifactId>
            <version>1.7.0</version>
        </dependency>
复制代码

2.1.2 信号量隔离

SemaphoreBulkhead使用了信号量,配置属性,如下表所示。

配置属性 默认值 描述
maxConcurrentCalls 25 隔离允许线程并发执行的最大数量
maxWaitDuration 0 当达到并发调用数量时,新的线程执行时将被阻塞,这个属性表示最长的等待时间。

在订单工程application.yml配置如下。

resilience4j:
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 5 # 隔离允许并发线程执行的最大数量
        maxWaitDuration: 20ms # 当达到并发调用数量时,新的线程的阻塞时间
    instances:
      backendA:
        baseConfig: default
      backendB:
        maxWaitDuration: 10ms
        maxConcurrentCalls: 20
复制代码

修改OrderController代码如下。

    @GetMapping("/payment/{id}")
//    @CircuitBreaker(name = "backendB", fallbackMethod = "fallback")
    @Bulkhead(name = "backendA", fallbackMethod = "fallback", type = Bulkhead.Type.SEMAPHORE)
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException {
        log.info("------enter the method--------");
        Thread.sleep(10000L); //阻塞10秒,已测试慢调用比例熔断
        String url = "http://cloud-payment-service/payment/" + id;
        Payment payment = restTemplate.getForObject(url, Payment.class);
        log.info("------exit the method----------");
        return ResponseEntity.ok(payment);
    }
复制代码

执行并测试,可以看到因为并发线程数为5(maxConcurrentCalls: 5),只有5个线程进入执行,其他请求降直接降级。效果如下图所示。

image.png

2.2 线程池隔离

FixedThreadPoolBulkhead配置如下,如下表所示。

配置名称 默认值 含义
maxThreadPoolSize Runtime.getRuntime().availableProcessors() 配置最大线程池大小
coreThreadPoolSize Runtime.getRuntime().availableProcessors() - 1 配置核心线程池大小
queueCapacity 100 配置队列的容量
keepAliveDuration 20ms 当线程数大于核心时,这是多余空闲线程在终止前等待新任务的最长时间

支付工程的application.yml,配置如下。

resilience4j:
  thread-pool-bulkhead:
    configs:
      default:
        maxThreadPoolSize: 4 # 最大线程池大小
        coreThreadPoolSize: 2 # 核心线程池大小
        queueCapacity: 2 # 队列容量
    instances:
      backendA:
        baseConfig: default
      backendB:
        maxThreadPoolSize: 1
        coreThreadPoolSize: 1
        queueCapacity: 1
复制代码

增加OrderService,注意,FixedThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法。代码如下。

@Service
@Slf4j
public class OrderService {

    @Bulkhead(name = "backendA", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<Payment> getPaymet() throws InterruptedException {
        log.info("now i enter the method!!!");
        Thread.sleep(10000L);
        log.info("now i exist the method!!!");
        return CompletableFuture.supplyAsync(() -> new Payment(123, "线程池隔离回退。。。"));
    }

}
复制代码

修改OrderController代码如下。

    @Autowired
    private OrderService orderService;

	@GetMapping("/payment/{id}")
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        return ResponseEntity.ok(orderService.getPaymet().get());
    }
复制代码

线程隔离配置如下图所示,4个请求进入线程执行(maxThreadPoolSize: 4 ),2个请求(queueCapacity: 2)进入有界队列等待,等待10秒后有线程执行结束,队列中的线程开始执行,如下图所示。

image.png 执行效果如下图所示

image.png

image.png

不难看出就是4个线程先执行后,退出了2个线程后,有界队列中的2个再进入。

3. 限流(RateLimiter)

3.1 添加依赖

    <dependency>
      <groupId>io.github.resilience4j</groupId>
      <artifactId>resilience4j-ratelimiter</artifactId>
      <version>1.7.0</version>
    </dependency>
复制代码

3.2 配置文件

R4的限流模块RateLimter基于滑动窗口,和令牌桶限流算法,配置如下,如下表所示。

属性 默认值 描述
timeoutDuration 5秒 线程等待权限的默认等待时间
limitRefreshPeriod 500纳秒 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod。
limitForPeriod 50 在一次刷新周期内,允许执行的最大请求数

在订单工程的application.yml中配置如下。

resilience4j:
  ratelimiter:
    configs:
      default:
        timeoutDuration: 5 # 线程等待权限的默认等待时间
        limitRefreshPeriod: 1s # 限流器每隔1s刷新一次,将允许处理的最大请求重置为2
        limitForPeriod: 2 #在一个刷新周期内,允许执行的最大请求数
    instances:
      backendA:
        baseConfig: default
      backendB:
        timeoutDuration: 5
        limitRefreshPeriod: 1s
        limitForPeriod: 5
复制代码

修改OrderController代码如下。

    @GetMapping("/payment/{id}")
    @RateLimiter(name = "backendA", fallbackMethod = "fallback")
    public ResponseEntity<Payment> getPaymentById(@PathVariable("id") Integer id) throws InterruptedException, ExecutionException {
        log.info("now i enter the method!!!");

        Thread.sleep(10000L); //阻塞10秒,已测试慢调用比例熔断

        String url = "http://cloud-payment-service/payment/" + id;
        Payment payment = restTemplate.getForObject(url, Payment.class);

        log.info("now i exist the method!!!");

        return ResponseEntity.ok(payment);
    }
复制代码

启动并测试,因为在一个刷新周期1s(limitRefreshPeriod: 1s)允许执行的最大请求数为2(limitForPeriod: 2),等待令牌时间5s(timeoutDuration: 5 ),限流逻辑如下图所示。并发发送20个请求后,只有2个请求拿到令牌执行,另外2个请求等5秒后拿到令牌,其他16个请求直接降级。

image.png

image.png

image.png

image.png

可以看到是先在1s刷新周期内先执行2个请求,等5s后拿到令牌再执行2个请求。

猜你喜欢

转载自juejin.im/post/7053683404625149965
今日推荐