一、背景
在微服务架构中,服务的稳定性和弹性至关重要。当某个服务出现故障或延迟时,服务熔断可以防止故障的扩散,保护系统的可用性和稳定性。之前项目中使用的Hystrix作为熔断器,但是Hystrix官方宣布不再进行更新,建议使用resilience4j作为熔断器。
既然官方都推荐resilience4j,那么我们就去探索一下,resilience4j怎么集成到我们的项目中,以及能否达到预期的熔断效果。
二、什么是熔断器 ?
在集成resilience4j之前我们先回顾一下,什么是熔断器?
"熔断器"本身是一种开关装置,当某个服务单元发生故障后,通过熔断器的故障监控(类似电路中保险丝熔断),停止调用故障的服务。向调用方返回一个兜底的、符合预期的、可处理的备选响应,而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统中的蔓延,甚至雪崩。
例如,某视频软件的用户服务发生故障,拿不到用户名称,立即对用户服务进行熔断并返回兜底的XX用户,在页面用户名字处统一显示XX用户。
三、熔断器 Circuit Breaker的工作原理
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。下面是resilience4j官网中的介绍:
大概的意思是,Circuit Breaker是通过状态机实现的,正常的状态有关闭、打开和半开,还有两种特殊的状态禁止和强制打开。并且它使用滑动窗口来存储和聚合调用的结果,可以选择时间滑动窗口(常用)或者次数滑动窗口,结合配置进行不同状态之间的扭转,控制熔断器的关闭或打开。
从上面的状态扭转图中可以看出,当某个服务出现故障时,CircuitBreaker会将当前CLOSED状态切换成OPEN状态,也就是保险丝断了或者跳闸了。调闸后,服务不能一直断开,所以CircuitBreaker隔一段时间会放行几次请求查看服务是否恢复,这时候会切换到HALF_OPEN状态。如果服务恢复正常,就切换到CLOSED状态继续使用,如果还是异常状态,就切换会OPEN状态。
四、Spring Cloud中怎么集成resilience4j?
熔断器一般都是配在服务的调用方或者说上游服务,按照下面的步骤进行集成。
1. 在pom文件中引入circuitbreaker相关的包
<!--resilience4j-circuitbreaker-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- 由于断路保护等需要AOP实现,所以必须导入AOP包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 在application.yml文件中引入circuitbreaker的配置
# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子
# 10s时间窗口中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待20秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j.circuitbreaker:
configs:
default:
registerHealthIndicator: true #注册一个健康指示器,以便可以通过 Actuator 的健康端点监控熔断器的状态
failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
slidingWindowType: TIME_BASED # 滑动窗口的类型
slidingWindowSize: 10 #滑动窗口的大小,配置TIME_BASED表示10秒,配置COUNT_BASED表示10个请求
minimumNumberOfCalls: 5 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
automaticTransitionFromOpenToHalfOpenEnabled: true #是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 20s #从OPEN到HALF_OPEN状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 3 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果这些请求调用的失败率等于或高于50%,CircuitBreaker将重新进入开启状态。
eventConsumerBufferSize: 10 #事件消费者的缓冲区大小,表示事件消费者最多可以缓存 10 个事件。
recordExceptions: #记录哪些异常为失败
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
- feign.FeignException
3. 在服务调用方调用下游服务时通过注解引入熔断器
写个简单的例子,方便后面看效果。
@RestController
public class OrderController {
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
@Autowired
private PayService payService;
@GetMapping("/order/{id}")
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")//配置熔断器
public String order(@PathVariable("id") Integer id){
log.info("Order id: {}", id);
log.info("Request Pay For Order id: {}", id);
//通过open feign远程调用支付服务
return payService.payOrder(id);
}
//fallback就是服务降级后的兜底处理方法
public String fallback(Integer id,Throwable t) {
log.info("Pay Service invoke failed for order ID: {}", id);
log.error("Error Message : {}", t.getMessage());
return "Pay Service Was Busy Now. Please try again later!";
}
}
4. 在被调用服务中增加一个接口
@RestController
public class PayController {
private static final Logger log = LoggerFactory.getLogger(PayController.class);
@GetMapping("/pay/{id}")
public String pay(@PathVariable("id") Integer id){
log.info("Pay For Order id: {}", id);
if (id == 1) { // 模拟服务异常
throw new RuntimeException("Pay Service Exception");
}
return "Pay Success";
}
}
5. 进行测试
我们的测试场景是,在Order服务中去调用下游的Pay服务,在10s时间窗口内如果有50%的失败率,就会触发熔断器打开OPEN。熔断器打开后,会拒绝所有的请求,并返回兜底的数据。在熔断器打开20s后,最多会允许3次调用,测试服务是否恢复,同时切换至HALF_OPEN。如果这3次调用的失败率或慢调用率等于或高于50%,会切换回OPEN状态,反之则熔断器关闭CLOSED,允许调用。
1)访问正常的支付服务
2)模拟访问异常的支付服务
由于支付服务异常,会返回兜底的数据
3)在10s时间窗口内模拟多次异常支付服务的调用,触发熔断器打开
多次请求后,日志中出现了下面的错误信息。
从上面的日志可以看出,前面5次请求调用都失败了,从6次开始熔断器开始工作了,没有继续调用下游的支付服务了,而是直接走降级方法返回了兜底的数据。
4)熔断器触发后20s允许远程调用检测服务是否恢复健康
在熔断器触发后一直请求,在20s后会允许远程调用。我们在浏览器中一直发起请求:
从上面的日志可以看出,在熔断20s后,切换至HALF_OPEN状态,允许请求通过以检测服务是否恢复健康。
5)切换至半开状态后,若远程服务任然不可用会怎么样?
我们可用看到,处于半开状态后,放行了3次请求测试(上面的配置文件中配置最多允许3次)。
这3次请求都没有成功,然后熔断器又重新打开处于OPEN状态,远程请求被熔断,走兜底的方案。
6)切换至半开状态后,若远程服务恢复正常会怎么样?
熔断器处于半开状态的时候,我们请求http://localhost:8082/order/12 模拟服务恢复正常。
这时候服务开始恢复正常调用,熔断器切换至CLOSED状态。
五、总结
通过上面订单、支付的一个简单调用测试,我们验证了熔断器的工作流程和预期一致。通过上述实现,当下游服务出现故障时,熔断器能够快递熔断掉下游服务的调用,并返回降级后的结果或是友好的异常提升。避免了长时间的等待和大量失败的调用,确保系统的稳定性和响应能力。这种模式在微服务框架中非常常见并且有效。