CircuitBreaker 熔断基础
Circuit Breaker 能解决的问题
在分布式系统中,复杂的架构通常涉及多个应用程序和依赖关系。某些依赖在某些时刻不可避免地会发生故障,这些故障可能会引发严重的系统问题,例如“雪崩效应”。
雪崩效应的成因
在典型的微服务架构中,多个微服务之间通常存在调用链。例如,微服务 A 可能会调用微服务 B 和微服务 C,而 B 和 C 又可能进一步调用其他微服务。这种多层级的调用关系被称为“扇出”。当“扇出”链路中的某个微服务响应时间过长或无法响应时,逐渐占用越来越多的系统资源,最终导致微服务 A 的资源耗尽,进而引发系统崩溃,形成“雪崩效应”。
下图展示了这一现象:
高流量场景中的风险
对于高流量应用而言,即使仅有一个后端依赖出现问题,也可能迅速耗尽整个系统的资源。更糟糕的是,这种情况可能导致服务间的延迟增加,造成队列备份、线程阻塞及其他系统资源紧张,最终引发级联故障。因此,必须通过隔离和管理故障及延迟,确保单一依赖的失败不会影响整个应用或系统的可用性。
级联故障的防范
在实际场景中,当某个模块的实例发生故障后,该模块可能继续接收流量并调用其他模块,从而引发级联故障,导致雪崩效应。
为解决这些问题,Circuit Breaker 机制在调用链中引入了“断路器”逻辑,当检测到异常时会主动中断调用,以保护系统避免更大范围的故障扩散。
Circuit Breaker 的工作原理
Circuit Breaker 的工作原理基于有限状态机,包括以下三个主要状态:
-
CLOSED(关闭状态):这是系统的正常状态,所有请求都能正常通过。当失败率低于阈值时,系统保持在该状态;当失败率超过阈值时,状态会转换为“OPEN”(开启状态)。
-
OPEN(开启状态):当失败率超过设定的阈值时,Circuit Breaker 进入开启状态。在此状态下,所有请求将立即失败,阻止任何对下游服务的调用,以防止系统资源耗尽。
-
HALF_OPEN(半开状态):经过设定的等待时间后,Circuit Breaker 从开启状态转换为半开状态。在此状态下,部分请求会被允许通过,以测试下游服务是否已恢复。如果通过的请求成功率较高,系统会切换回关闭状态;如果失败率依然较高,系统会再次切换回开启状态。
此外,还有两个特殊状态:
- DISABLED(禁用状态):断路器功能被完全关闭,不再监控或限制请求。
- FORCED_OPEN(强制开启状态):无论实际情况如何,断路器被强制设置为开启状态,所有请求都会被拒绝。
下图展示了这几个状态及其转换条件:
Circuit Breaker 具体状态转换示例
- 当执行的 6 次请求中,失败率达到 50% 时,Circuit Breaker 将进入“开启(OPEN)”状态(类似于保险丝跳闸),此时所有请求都会被拒绝。
- 等待 5 秒后,Circuit Breaker 将自动从“开启(OPEN)”状态过渡到“半开(HALF_OPEN)”状态,允许部分请求通过以测试服务是否恢复正常。
- 如果请求仍然异常,Circuit Breaker 将重新进入“开启(OPEN)”状态;如果请求正常,系统将恢复到“关闭(CLOSED)”状态,继续正常处理请求。
下图展示了上述转换过程:
通过这些状态的转换,Circuit Breaker 能有效管理依赖服务的故障,防止系统因部分服务不可用而崩溃。
举个例子
消费者的 application.yml
spring:
cloud:
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 10000
#读取超时时间
readTimeout: 10000
profiles:
active: dev
消费者的 bootstrap.yml
spring:
application:
name: cloud-consumer-service-feign # Spring 应用的名称,在服务注册和发现中使用
cloud:
consul:
host: localhost # Consul 服务器的主机地址
port: 8500 # Consul 服务器的端口
discovery:
prefer-ip-address: true # 优先使用服务 IP 地址进行注册
service-name: ${spring.application.name} # 使用 Spring 应用名作为服务名进行注册
config:
prefix: config/consumer-service # 配置中心的前缀路径
profile-separator: '-' # 配置文件的分隔符,默认值是 ",",这里更新为 "-"
format: YAML # 配置文件的格式为 YAML
data-key: data # 数据键
enabled: true # 启用配置中心
消费者的 application-dev.yml
server:
port: 9988
spring:
openfeign:
client:
config:
default:
# connectTimeout 和 readTimeout 被应用于所有 Feign 请求
connectTimeout: 20000 # 连接超时时间设置为 20 秒
readTimeout: 20000 # 读取超时时间设置为 20 秒
httpclient:
hc5:
enabled: true # 启用 HttpClient 5
compression:
request:
enabled: true # 启用请求压缩
min-request-size: 2048 # 最小触发压缩的请求大小为 2048 字节
mime-types: text/xml,application/xml,application/json # 触发压缩的数据类型
response:
enabled: true # 启用响应压缩
circuitbreaker:
enabled: true # 启用 Feign 的 Circuit Breaker 支持
group:
enabled: true # 启用分组功能,精确优先、分组次之、默认配置最后
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug # 设置 Feign 日志级别为 debug
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50 # 设置 50% 的调用失败时打开断路器
slidingWindowType: COUNT_BASED # 滑动窗口的类型为 COUNT_BASED(基于请求次数)
slidingWindowSize: 6 # 滑动窗口大小为 6 次请求
minimumNumberOfCalls: 6 # 断路器计算失败率之前所需的最小样本数
automaticTransitionFromOpenToHalfOpenEnabled: true # 自动从 OPEN 过渡到 HALF_OPEN
waitDurationInOpenState: 5s # 从 OPEN 到 HALF_OPEN 状态的等待时间为 5 秒
permittedNumberOfCallsInHalfOpenState: 6 # 半开状态允许的最大请求数为 6
recordExceptions:
- java.lang.Exception # 记录哪些异常会触发断路器
instances:
cloud-payment-service:
baseConfig: default # 使用默认配置
OpenFeign 的 PayFeignApi
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
/**
* Resilience4j CircuitBreaker 的例子
*
* @param id
* @return
*/
@GetMapping(value = "/pay/circuit/{id}")
public ResultData<String> myCircuit(@PathVariable("id") Integer id);
}
消费者的 OrderCircuitController
import com.cluod.commons.api.PayFeignApi;
import com.cluod.commons.resp.ResultData;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @Title: OrderCircuitController
*
@Author David
* @Package com.cluod.feign.controller
* @Date 2024/7/17 下午4:14
* @description: 熔断
*/
@RestController
public class OrderCircuitController {
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/circuit/{id}")
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
public ResultData<String> myCircuitBreaker(@PathVariable("id") Integer id) {
return payFeignApi.myCircuit(id);
}
//myCircuitFallback 就是服务降级后的兜底处理方法
public ResultData<String> myCircuitFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return ResultData.defaultfail("myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~", "");
}
}
生产者的 PayCircuitController
import cn.hutool.core.util.IdUtil;
import com.cluod.commons.resp.ResultData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @Title: PayCircuitController
* @Author David
* @Package com.cluod.feign.controller
* @Date 2024/7/15 下午3:41
* @description: 熔断测试
*/
@RestController
public class PayCircuitController {
//=========Resilience4j CircuitBreaker 的例子
@GetMapping(value = "/pay/circuit/{id}")
public ResultData<String> myCircuit(@PathVariable("id") Integer id) {
if (id == -4) throw new RuntimeException("----circuit id 不能负数");
if (id == 9999) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return ResultData.defaultsuccess("Hello, circuit! inputId: " + id + " \t " + IdUtil.simpleUUID());
}
}
测试结果
测试场景说明
在这个测试场景中,通过模拟请求失败、超时和成功来观察 Circuit Breaker 的状态变化。以下是测试中的具体行为及结果展示:
-
初始状态:CLOSED
- 在最初的几次调用中,系统处于正常的 CLOSED(关闭) 状态,所有请求都能够正常通过。
- 当发生一次较大的异常或超时请求时,Circuit Breaker 开始记录失败。
-
第一次失败后状态转换:从 CLOSED 到 HALF_OPEN
- 一旦请求失败并且超过配置的阈值,Circuit Breaker 状态从 CLOSED 转为 HALF_OPEN。此时,Circuit Breaker 允许部分请求通过,以检测下游服务是否恢复。
-
多次失败后状态转换:从 HALF_OPEN 到 OPEN
- 当 Circuit Breaker 处于 HALF_OPEN 状态时,如果多个请求再次失败,系统将认为下游服务仍然不可用,Circuit Breaker 会进入 OPEN(开启) 状态,所有后续请求将直接被拒绝,避免进一步的资源浪费。
-
等待一段时间后状态转换:从 OPEN 到 HALF_OPEN
- 在 OPEN 状态保持一段时间后(例如 5 秒),Circuit Breaker 自动过渡到 HALF_OPEN 状态。这时,允许少量请求通过,检测下游服务是否恢复正常。
-
恢复后的状态转换:从 HALF_OPEN 到 CLOSED
- 在 HALF_OPEN 状态下,如果通过的请求成功率较高,说明下游服务已恢复正常,Circuit Breaker 会将状态恢复到 CLOSED,继续正常接收请求。
状态转换过程的可视化
-
一次失败后状态变化:
- 在一次请求失败后,Circuit Breaker 从 CLOSED 状态转换到 HALF_OPEN 状态,并持续监控后续请求的成功率。
-
多次成功后的状态恢复:
- 在经过一段时间的观测并确认下游服务恢复后,Circuit Breaker 状态从 HALF_OPEN 恢复为 CLOSED,表示系统已恢复正常。