菜单
本页目录

CircuitBreaker 熔断基础

Circuit Breaker 能解决的问题

在分布式系统中,复杂的架构通常涉及多个应用程序和依赖关系。某些依赖在某些时刻不可避免地会发生故障,这些故障可能会引发严重的系统问题,例如“雪崩效应”。

雪崩效应的成因

在典型的微服务架构中,多个微服务之间通常存在调用链。例如,微服务 A 可能会调用微服务 B 和微服务 C,而 B 和 C 又可能进一步调用其他微服务。这种多层级的调用关系被称为“扇出”。当“扇出”链路中的某个微服务响应时间过长或无法响应时,逐渐占用越来越多的系统资源,最终导致微服务 A 的资源耗尽,进而引发系统崩溃,形成“雪崩效应”。

下图展示了这一现象:

截图_选择区域_20240824185459.png

高流量场景中的风险

对于高流量应用而言,即使仅有一个后端依赖出现问题,也可能迅速耗尽整个系统的资源。更糟糕的是,这种情况可能导致服务间的延迟增加,造成队列备份、线程阻塞及其他系统资源紧张,最终引发级联故障。因此,必须通过隔离和管理故障及延迟,确保单一依赖的失败不会影响整个应用或系统的可用性。

级联故障的防范

在实际场景中,当某个模块的实例发生故障后,该模块可能继续接收流量并调用其他模块,从而引发级联故障,导致雪崩效应。

为解决这些问题,Circuit Breaker 机制在调用链中引入了“断路器”逻辑,当检测到异常时会主动中断调用,以保护系统避免更大范围的故障扩散。

Circuit Breaker 的工作原理

Circuit Breaker 的工作原理基于有限状态机,包括以下三个主要状态:

  1. CLOSED(关闭状态):这是系统的正常状态,所有请求都能正常通过。当失败率低于阈值时,系统保持在该状态;当失败率超过阈值时,状态会转换为“OPEN”(开启状态)。

  2. OPEN(开启状态):当失败率超过设定的阈值时,Circuit Breaker 进入开启状态。在此状态下,所有请求将立即失败,阻止任何对下游服务的调用,以防止系统资源耗尽。

  3. HALF_OPEN(半开状态):经过设定的等待时间后,Circuit Breaker 从开启状态转换为半开状态。在此状态下,部分请求会被允许通过,以测试下游服务是否已恢复。如果通过的请求成功率较高,系统会切换回关闭状态;如果失败率依然较高,系统会再次切换回开启状态。

此外,还有两个特殊状态:

  • DISABLED(禁用状态):断路器功能被完全关闭,不再监控或限制请求。
  • FORCED_OPEN(强制开启状态):无论实际情况如何,断路器被强制设置为开启状态,所有请求都会被拒绝。

下图展示了这几个状态及其转换条件:

c65ca25a48324ea7f2767e885c394d4.png

Circuit Breaker 具体状态转换示例

  • 当执行的 6 次请求中,失败率达到 50% 时,Circuit Breaker 将进入“开启(OPEN)”状态(类似于保险丝跳闸),此时所有请求都会被拒绝。
  • 等待 5 秒后,Circuit Breaker 将自动从“开启(OPEN)”状态过渡到“半开(HALF_OPEN)”状态,允许部分请求通过以测试服务是否恢复正常。
  • 如果请求仍然异常,Circuit Breaker 将重新进入“开启(OPEN)”状态;如果请求正常,系统将恢复到“关闭(CLOSED)”状态,继续正常处理请求。

下图展示了上述转换过程:

截图_选择区域_20240824192028.png

通过这些状态的转换,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 的状态变化。以下是测试中的具体行为及结果展示:

  1. 初始状态:CLOSED

    • 在最初的几次调用中,系统处于正常的 CLOSED(关闭) 状态,所有请求都能够正常通过。
    • 当发生一次较大的异常或超时请求时,Circuit Breaker 开始记录失败。
  2. 第一次失败后状态转换:从 CLOSED 到 HALF_OPEN

    • 一旦请求失败并且超过配置的阈值,Circuit Breaker 状态从 CLOSED 转为 HALF_OPEN。此时,Circuit Breaker 允许部分请求通过,以检测下游服务是否恢复。
  3. 多次失败后状态转换:从 HALF_OPEN 到 OPEN

    • 当 Circuit Breaker 处于 HALF_OPEN 状态时,如果多个请求再次失败,系统将认为下游服务仍然不可用,Circuit Breaker 会进入 OPEN(开启) 状态,所有后续请求将直接被拒绝,避免进一步的资源浪费。
  4. 等待一段时间后状态转换:从 OPEN 到 HALF_OPEN

    • OPEN 状态保持一段时间后(例如 5 秒),Circuit Breaker 自动过渡到 HALF_OPEN 状态。这时,允许少量请求通过,检测下游服务是否恢复正常。
  5. 恢复后的状态转换:从 HALF_OPEN 到 CLOSED

    • HALF_OPEN 状态下,如果通过的请求成功率较高,说明下游服务已恢复正常,Circuit Breaker 会将状态恢复到 CLOSED,继续正常接收请求。

状态转换过程的可视化

  1. 一次失败后状态变化

    • 在一次请求失败后,Circuit Breaker 从 CLOSED 状态转换到 HALF_OPEN 状态,并持续监控后续请求的成功率。

    截图_选择区域_20240824214815.png

  2. 多次成功后的状态恢复

    • 在经过一段时间的观测并确认下游服务恢复后,Circuit Breaker 状态从 HALF_OPEN 恢复为 CLOSED,表示系统已恢复正常。

    截图_选择区域_20240824215506.png