SpringCloud-04-Circuit Breaker断路器

一、概述

1、分布式系统面临的问题??

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。

服务雪崩:

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的"扇出"。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的"雪崩效应"。

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

2、问题解决

有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。

"断路器"本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

二、什么是Circuit Breaker

Circuit Breaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。

当一个组件或服务出现故障时,Circuit Breaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。

同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。

Circuit Breaker只是一套规范和接口,落地实现者是Resilience4J。

健壮

三、Resilience4J

‌Resilience4j 是一个轻量级容错库‌,专为 Java 应用程序设计,用于实现熔断、限流、重试和隔离等机制,以提升分布式系统的稳定性和可用性。‌‌

Resilience4j 通过模块化设计提供多种容错能力,核心组件包括: ‌

  • 熔断器(Circuit Breaker)‌:当服务失败率超过阈值(如 50%)时自动熔断,防止级联故障,支持关闭、开启、半开三种状态转换。‌‌
  • 限流器(Rate Limiter)‌:基于令牌桶算法控制请求速率,例如每秒最多 5 次请求,避免服务过载。‌‌
  • 重试机制(Retry)‌:对失败操作自动重试,适用于网络抖动等临时性故障。‌‌
  • 隔离器(Bulkhead)‌:通过信号量限制并发请求数,防止单个服务故障拖垮整个系统。‌‌

1、Resilience4J服务熔断和服务降级

断路器(Circuit Breaker):

  • 断路器有三个普通状态:关闭(CLOSED)、开启(OPEN)、半开(HALF_OPEN)。
  • 还有两个特殊状态:禁用(DISABLED)、强制开启(FORCED_OPEN)。

熔断器三大状态的改变(原理):

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

断路器使用滑动窗口来存储和统计调用的结果。可以选择基于调动数量的滑动窗口或者基于时间的滑动窗口。(即作为断路器开启/关闭的依据)。

  • 基于访问数量的滑动窗口:统计最近N次调用的返回结果:按照访问时的成功率进行判断。eg:访问6次3次失败,就判断被调用服务已经宕机,就不再调用它,立即开启服务熔断 断路器开启状态,进入服务降级(fallback)。
  • 基于时间的滑动窗口:统计最近N秒的调用返回结果:基于时间进行判断。eg:在一定的时间范围内,每次调用时间都超出设定时间,也会立即开启服务熔断状态。

除此之外,熔断器还会有两种特殊状态:disabled(始终允许访问)和forced_open(始终拒绝访问)。

  • 这两个状态不会生成熔断器事件(除状态转换外),并且不会记录事件的成功与失败。
  • 退出这两个状态的唯一方法是触发状态转换或者重置熔断器。

断路器装在消费者、即服务调用者处。

断路器常用配置:

XML 复制代码
<!--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>

1、案例1

按照COUNT_BASED(计数的滑动窗口) 按照错误次数达到多少次之后开启断路

断路器装在消费者、即微服务调用者处。

application.yml配置

XML 复制代码
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
#  6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
#  等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
#  如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
        slidingWindowType: COUNT_BASED # 滑动窗口的类型
        slidingWindowSize: 6 #滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
        minimumNumberOfCalls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
        automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
        waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
        permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
        recordExceptions:
          - java.lang.Exception
    instances:
      cloud-payment-service:  ##代表将上面断路器的配置到消费者
        baseConfig: default #默认配置

生产者微服务接口:

java 复制代码
@RestController
public class PayCircuitController {

        //=========Resilience4j CircuitBreaker 的例子

        @GetMapping(value = "/pay/circuit/{id}")
        public 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 "Hello, circuit! inputId:  "+id+" \t " + IdUtil.simpleUUID();//流水号
        }
}

Feign接口:

java 复制代码
@GetMapping(value = "/pay/circuit/{id}")
public String myCircuit(@PathVariable("id") Integer id);
      

消费者微服务接口:

需要引入@CircuitBreaker注解以及服务降级方法

java 复制代码
@GetMapping(value = "/feign/pay/circuit/{id}")
    @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
    //             客户端要调用微服务的名称               服务降级,调用下面的myCircuitFallback()
    public String myCircuitBreaker(@PathVariable("id") Integer id){
        return payFeignApi.myCircuit(id);//调用Feign
    }
    //myCircuitFallback()就是服务降级后的兜底处理方法:如果熔断之后/或者报错就会调用这个方法来返回结果!!!
    public String myCircuitFallback(Integer id,Throwable t) {
        // 这里是容错处理逻辑,返回备用结果
        return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";//不要让服务调用者等待并立即返回1个友好提示,																  fallback
    }

2、案例2

按照TIME_BASED(时间的滑动窗口)

断路器装在消费者、即微服务调用者处。

建议:不要混合使用,建议使用COUNT_BASED的方式

XML 复制代码
# Resilience4j CircuitBreaker 按照时间:TIME_BASED 的例子
resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
        slowCallDurationThreshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例。
        slowCallRateThreshold: 30 #慢调用百分比峰值,断路器把调用时间⼤于slowCallDurationThreshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
        slidingWindowType: TIME_BASED # 滑动窗口的类型
        slidingWindowSize: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
        minimumNumberOfCalls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。
        permittedNumberOfCallsInHalfOpenState: 2 #半开状态允许的最大请求数,默认值为10。
        waitDurationInOpenState: 5s #从OPEN到HALF_OPEN状态需要等待的时间
        recordExceptions:
          - java.lang.Exception
    instances:
      cloud-payment-service:
        baseConfig: default

2、Resilience4J舱壁隔离BulkHead

舱壁隔离:依赖隔离&负载保护:用来限制下游服务的最大并发数量的限制。

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

  • SemaphoreBulkhead(信号量舱壁):使用了信号量。
  • FixedThreadPoolBulkhead(固定线程池舱壁):使用了有界队列和固定大小线程池。

信号量舱壁(SemaphoreBulkhead) 舱壁装在消费者、调用者处

原理:

  • 当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
  • 当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器。
  • 如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
  • 若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
XML 复制代码
<!--resilience4j-bulkhead-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>

application.yml

  • 属性配置: maxConcurrentCalls:舱壁允许的最大并行执行量,默认值是25
  • maxWaitDuration:尝试进入饱和舱壁时,应阻塞线程的最长时间,默认值是0
XML 复制代码
###resilience4j bulkhead 的例子
resilience4j:
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
        maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
    instances:
      cloud-payment-service:
        baseConfig: default
  timelimiter:
    configs:
      default:
        timeout-duration: 20s

生产者微服务接口:

java 复制代码
 //=========Resilience4j bulkhead 的例子
    @GetMapping(value = "/pay/bulkhead/{id}")
    public String myBulkhead(@PathVariable("id") Integer id){
        if(id == -4) throw new RuntimeException("----bulkhead id 不能-4");

        if(id == 9999){
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        return "Hello, bulkhead! inputId:  "+id+" \t " + IdUtil.simpleUUID();
    }

OpenFeign接口:

java 复制代码
    //Resilience4j bulkhead 的例子
    @GetMapping(value = "/pay/bulkhead/{id}")
    public String myBulkhead(@PathVariable("id") Integer id);

消费者微服务接口:

消费者模块Controller 需要加入舱壁隔离注解@Bulkhead+注意:type = Bulkhead.Type.SEMAPHORE 以及服务降级方法fallbackMethod

java 复制代码
 // 案例:演示消费者/客户端80通过Feign接口调用8001舱壁服务
  //(船的)舱壁,隔离
    @GetMapping(value = "/feign/pay/bulkhead/{id}")
    //舱壁隔离注解
    @Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadFallback",type = Bulkhead.Type.SEMAPHORE)
    //        服务提供者模块               服务降级:如果失败,就执行下面的myBulkheadFallback()方法来返回给消费者/客户端   指定隔离的实现方式
    public String myBulkhead(@PathVariable("id") Integer id){
        return payFeignApi.myBulkhead(id);
    }
    public String myBulkheadFallback(Throwable t){
        return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
    }

3、固定线程池舱壁(FixedThreadPoolBulkhead)

FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。 当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。 当线程池中无空闲时时,接下来的请求将进入等待队列, 若等待队列仍然无剩余空间时接下来的请求将直接被拒绝, 在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。

另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法 FixedThreadPoolbulkhead的配置项:

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

使用步骤: 在服务消费端pom文件加入相关依赖(同上方的信号舱舱壁隔离的一样的)

XML 复制代码
###resilience4j bulkhead -THREADPOOL的例子
resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #timelimiter默认限制远程1s,超过报错不好演示效果所以加上10秒
  thread-pool-bulkhead:
    configs:
      default:
        core-thread-pool-size: 1
        max-thread-pool-size: 1
        queue-capacity: 1
    instances:
      cloud-payment-service:
        baseConfig: default
# spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离

消费端Controller 需要加入舱壁隔离注解@Bulkhead,注意:type = Bulkhead.Type.THREADPOOL, 且服务降级方法返回值类型必须为CompletableFuture

java 复制代码
//案例:演示消费者/客户端通过Feign接口调用舱壁服务(2)treadPool
    //(船的)舱壁,隔离
    @GetMapping(value = "/feign/pay/bulkhead/{id}")
    //舱壁隔离注解
    @Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadPoolFallback",type = Bulkhead.Type.THREADPOOL)
    //        服务提供者模块               服务降级:如果失败,就执行下面的myBulkheadFallback()方法来返回给消费者/客户端   指定隔离的实现方式:THREADPOOL
    public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id){
        System.out.println(Thread.currentThread().getName()+"\t"+"enter the method!!!");
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName()+"\t"+"exist the method!!!");

        return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL");
    }
    public CompletableFuture<String> myBulkheadPoolFallback(Integer id,Throwable t)//在fallback中多加个Throwable t即可{
        return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
    }

4、Resilience4J限流

限流是频率的控制,限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。 就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。

感觉限流类似于舱壁:舱壁是没有可用位置/线程就返回兜底。限流是没有位置就等待。

常见限流算法:

  • 漏斗算法:让请求像水流过漏斗一样,请求的处理效率是固定的,来不及处理的请求在"漏斗"中等待。漏斗算法对于存在突发特性的流量来说缺乏效率。
  • 令牌桶算法:准备一定的令牌数,请求通过时,如果令牌有剩余,该请求就领取一个令牌,并进入处理,处理完后释放令牌。没有令牌剩余的时候就是到等待队列中等待令牌。这种方法是SpringCloud默认使用的算法。
  • 滚动时间窗:取一段时间内,允许固定数量的请求进入,如果这段时间内超过该固定数量,就拒绝或者排队,等下一个时间段进入。但是间隔临界的一段时间内的请求超过系统限制,可能导致系统被压垮。
  • 滑动时间窗:滑动事件窗口是把固定时间片段进行划分,并且随着时间移动,移动方式为开始时间点变为时间列表中的第二个时间点,结束时间点增加一个时间点,不断重复。达到一种滑片滑动的效果。
XML 复制代码
<!--resilience4j-ratelimiter-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
XML 复制代码
####resilience4j ratelimiter 限流的例子
resilience4j:
  ratelimiter:
    configs:
      default:
        limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
        limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
        timeout-duration: 1 # 线程等待权限的默认等待时间
    instances:
        cloud-payment-service:
          baseConfig: default

消费端微服务接口,加入@RateLimiter注解,写好服务提供者和兜底方法

java 复制代码
//案例:演示消费端通过Feign接口调用服务提供者
    @GetMapping(value = "/feign/pay/ratelimit/{id}")
    @RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback")
    //              服务提供者                     兜底方法
    public String myBulkhead(@PathVariable("id") Integer id){
        return payFeignApi.myRatelimit(id);
    }
    public String myRatelimitFallback(Integer id,Throwable t){
        return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
    }

    //之后,在客户端访问这个地址,如果1s内访问次数大于2次就会被限流,返回兜底方法
相关推荐
woniu_maggie2 小时前
SAP导入WPS编辑的Excel文件报错处理
后端
想个名字太难2 小时前
springboot 源码分析(自动装配原理)
java·spring boot·spring
回家路上绕了弯2 小时前
Spring Retry框架实战指南:优雅处理分布式系统中的瞬时故障
分布式·后端
yuuki2332332 小时前
【C++】vector底层实现全解析
c++·后端·算法
czlczl200209252 小时前
基于SpringBoot与RBAC的功能权限设计
spring boot·后端·spring
华仔啊2 小时前
如何查看 SpringBoot 当前线程数?3 种方法亲测有效
java·后端
weixin_425023002 小时前
Spring Boot 生成短链接
java·spring boot·后端
shark_chili2 小时前
浅谈CPU流水线的艺术
后端
while(1){yan}2 小时前
Spring,SpringBoot,SpringMVC
java·spring boot·spring