Resilience4j 服务熔断和降级
Resilience4j 是对 Circuit Breaker 的实现,它提供了更多的功能,比如限流、重试、超时等。Resilience4j 是一个轻量级的熔断库,它提供了一些常用的熔断器实现,比如 RateLimiter、Retry、Bulkhead、CircuitBreaker 等。
可以将 Circuit Breaker 比作一个保险丝,当服务出现问题时,保险丝会断开,避免服务雪崩,把故障限制在最小范围内。
虽然题目叫服务熔断和降级,但是实际上 Resilience4j 提供的功能远不止这些,它还提供了限流、重试、超时等功能。
原理
Circuit Breaker 的原理是通过状态机来实现的,它有三种状态:闭路,开路,半开路,类比电路的开路、闭路。
- 闭路:闭路即正常状态,服务正常,请求正常处理。如果服务出现问题,Circuit Breaker 会进入开路状态。
- 开路:开路状态下,Circuit Breaker 会拒绝所有请求,直到超时时间到达。超时时间到达后,Circuit Breaker 会进入半开路状态。
- 半开路:半开路状态下,Circuit Breaker 会允许一个请求通过,如果请求成功,Circuit Breaker 会进入闭路状态,否则会继续保持开路状态。
对于服务出现问题的标准,可以按时间滑动窗口或者请求次数来判断。前者即在一定时间内,请求失败的次数超过一定阈值,就认为服务出现问题;后者即在一定次数的请求中,失败的次数超过一定阈值,就认为服务出现问题。
服务熔断与降级 Circuit Breaker
配置参数
首先介绍几个重要的参数。全部参数参考文档。
- failureRateThreshold:失败率阈值,当请求失败率超过这个阈值时,Circuit Breaker 会打开。这个参数是白分比,比如 50 表示 50%。
- slowCallDurationThreshold:慢调用持续时间阈值,当请求的慢调用持续时间超过这个阈值时,被视为慢调用。这个参数是毫秒,比如 1000 表示 1 秒。
- slowCallRateThreshold:慢调用率阈值,当请求的慢调用率超过这个阈值时,Circuit Breaker 会打开。这个参数是白分比,比如 50 表示 50%。
- slidingWindowType:滑动窗口类型,有计数和时间两种。计数即在一定次数的请求中,失败的次数超过一定阈值,就认为服务出现问题;时间即在一定时间内,请求失败的次数超过一定阈值,就认为服务出现问题。COUNT_BASED 和 TIME_BASED,默认是 COUNT_BASED。
- slidingWindowSize:滑动窗口大小,如果是 COUNT_BASED,表示在多少次请求中判断服务是否出现问题;如果是 TIME_BASED,表示在多少时间内判断服务是否出现问题。
- permittedNumberOfCallsInHalfOpenState:半开路状态下允许通过的请求数量,如果超过这个数量,Circuit Breaker 会进入开路状态。默认是 10。
- minimumNumberOfCalls:计算错误率或慢调用率的最小请求数量。例如,如果 minimumNumberOfCalls 为 100,那么在 100 个请求之前,Circuit Breaker 不会起任何作用。一般和窗口大小一致。
- waitDurationInOpenState:开路状态下的等待时间,超过这个时间后,Circuit Breaker 会进入半开路状态。默认是 60 秒。
如果使用 yaml 配置,将驼峰命名法改成 kebab-case 即可,比如 failureRateThreshold 改成 failure-rate-threshold。
实现
首先引入包。
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j:3.1.2'
implementation 'org.springframework.boot:spring-boot-starter-aop'
然后用 application.yml 创建一个 Circuit Breaker。
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-type: COUNT_BASED
sliding-window-size: 10
minimum-number-of-calls: 10
permitted-number-of-calls-in-half-open-state: 5
failure-rate-threshold: 50
wait-duration-in-open-state: 60s
slow-call-rate-threshold: 50
slow-call-duration-threshold: 1000ms
instances:
payment-service:
baseConfig: default
sliding-window-size: 100
这里的 config 是创建一系列的配置,然后 instance 是创建一个实例。这里的 paymentAPI 是一个实例的名字,可以随便取。
或者可以用 Java 代码创建。
@Bean
public CircuitBreakerConfigCustomizer externalServiceFooCircuitBreakerConfig() {
return CircuitBreakerConfigCustomizer
.of("externalServiceFoo",
builder -> builder.slidingWindowSize(10)
.slidingWindowType(COUNT_BASED)
.waitDurationInOpenState(Duration.ofSeconds(5))
.minimumNumberOfCalls(5)
.failureRateThreshold(50.0f));
}
然后在代码中使用。
package io.github.fingerbone;
import io.github.fingerbone.api.PaymentApi;
import io.github.fingerbone.record.PaymentRecord;
import io.github.fingerbone.wrapper.ResponseCode;
import io.github.fingerbone.wrapper.ResponseWrapper;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import java.util.List;
@RestController
@RequestMapping("/if/payment")
public class ConsumerPaymentController {
public ResponseWrapper<List<PaymentRecord>> fb(Throwable e) {
return ResponseWrapper.error(ResponseCode.CIRCUIT_OPEN, null);
}
private final PaymentAPIIf paymentAPIIf;
public ConsumerPaymentController(@Autowired PaymentAPIIf paymentAPIIf) {
this.paymentAPIIf = paymentAPIIf;
}
@GetMapping
@CircuitBreaker(name = "bk", fallbackMethod = "fb")
public ResponseWrapper<List<PaymentRecord>> getAllPayments() {
return paymentAPIIf.getAllPayments();
}
@PostMapping
public ResponseWrapper<PaymentRecord> createPayment(@RequestBody PaymentRecord paymentRecord) {
return paymentAPIIf.createPayment(paymentRecord);
}
@GetMapping("/{id}")
public ResponseWrapper<PaymentRecord> getPayment(@PathVariable Long id) {
return paymentAPIIf.getPayment(id);
}
@DeleteMapping("/{id}")
public ResponseWrapper<Void> deletePayment(@PathVariable Long id) {
return paymentAPIIf.deletePayment(id);
}
@PutMapping("/{id}")
public ResponseWrapper<PaymentRecord> updatePayment(@PathVariable Long id, @RequestBody PaymentRecord paymentRecord) {
return paymentAPIIf.updatePayment(id, paymentRecord);
}
}
现在我们就为 GetAllPayments 方法添加了 Circuit Breaker,当请求失败率超过 50% 时,Circuit Breaker 会打开,拒绝所有请求,直到 60 秒后进入半开路状态。
如果给类加这个注解,那么类中所有的公开方法都会使用这个 Circuit Breaker。
注解中,name 就是前面配置的 breaker 名称,fallbackMethod 就是当 Circuit Breaker 打开时,调用的方法。但是要注意,fallbackMethod 的函数返回值必须和原函数一致,参数可以接受与原本函数一致的参数,可以自己额外带一个 Throwable 参数。
下面例如中,这些 Fallback 都是合理的,
@CircuitBreaker(name = "BACKEND", fallbackMethod = "fallback")
public Mono<String> method(String param1) {
return Mono.error(new NumberFormatException());
}
private Mono<String> fallback(String param1, NumberFormatException e) {
return Mono.just("Handled the NumberFormatException");
}
private Mono<String> fallback(String param1, Exception e) {
return Mono.just("Handled any other exception");
}
private Mono<String> fallback(String param1) {
return Mono.just("Handled the fallback");
}
private Mono<String> fallback(Throwable e) {
return Mono.just("Handled the fallback");
}
匹配时,按照越详细,越优先的规则。Fallback 方法也允许重载。但是匹配的方法的返回值必须和原方法一致。
如果不带 Fallback Method,则会触发一个异常。因此也可以用 adviser 来处理 fallback。
package io.github.fingerbone.wrapper;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionWrapper {
@ExceptionHandler(RuntimeException.class)
public ResponseWrapper<String> handleException(RuntimeException e) {
return ResponseWrapper.error(ResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
不过这个方法颗粒度比较粗。
如果使用 OpenFeign,需要在配置文件中加入
spring:
application:
name: order-service
cloud:
openfeign:
httpclient:
hc5:
enabled: true
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
logger-level: basic
circuitbreaker:
enabled: true
其它和 Interface Client 一致。