07.CircuitBreaker断路器

1.Hystrix进入维护模式

1.1 是什么

Hystrix是一个用于处理分布式系统的延迟容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

了解,这是Netflix项目中的组件,不会再使用了。

1.2 替代方案

Resilience4j

2.断路器概述

2.1 分布式系统面临的问题

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

2.2 服务雪崩现象

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

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

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

2.3 微服务系统的述求

问题:禁止服务雪崩故障

解决: 有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据也被称为服务降级)。

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

一句话,出故障了“保险丝”跳闸,别把整个家给烧了

2.4 解决方案

2.4.1 服务熔断 + 服务降级

服务熔断,就是当服务b发生故障时,服务a不在访问b,也就是说不能再访问b。

服务降级,就是给调用方一个默认的fallback,而不是一直在等待。比如,服务器忙,请稍后再试。

2.4.2 服务限流

通过限流算法,将瞬间很大的请求数量,转换为系统承受范围内的请求,从而保证服务不会被流量冲垮。

3.Spring Circuit Breaker

3.1 官网

https://spring.io/projects/spring-cloud-circuitbreaker

3.2 实现原理

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

当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。

3.3 CircuitBreaker和Resilience4j关系

CircuitBreaker是一种思想,Resilience4j是其思想的具体实现。

4.Resilience4j

4.1 是什么


Resilience4j是一个为函数式编程设计的轻量级容错库。Resilience4j提供高阶函数(装饰器),以增强与断路器、速率限制器、重试或隔板的任何功能接口、lambda表达式或方法引用。您可以在任何函数接口、lambda表达式或方法引用上堆叠多个装饰器。优点是,您可以选择所需的装饰器,而无需其他任何选择。
Resilience4j 2需要Java 17。

4.2 能干啥


本课程,只讲了这三个主要功能,分别是熔断(Circuit Breaking),限流(limiting),舱壁(Bulkheading)。

4.3 官网

https://resilience4j.readme.io/docs/circuitbreaker

中文手册

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md

5.案例实战

5.1 熔断和降级(circuitbreaker&fallback)

5.1.1 断路器的三大状态

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

  • 当断路器关闭时,所有的请求都会通过断路器,就行入户的电都会流经关闭的保险丝。

    • 如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。
    • 当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率。
    • 如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态。
  • 断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。

    • 基于访问数量的滑动窗口统计了最近N次调用的返回结果。居于时间的滑动窗口统计了最近N秒的调用返回结果。
  • 除此以外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)。

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

官网

https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker

中文手册

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/CircuitBreaker.md

默认CircuitBreakerConfig.java配置类(io.github.resilience4j.circuitbreaker)

精简版配置参数

参数 含义
failure-rate-threshold 以百分比配置失败率峰值
sliding-window-type 断路器的滑动窗口期类型 可以基于“次数”(COUNT_BASED)或者“时间”(TIME_BASED)进行熔断,默认是COUNT_BASED。
sliding-window-size 若COUNT_BASED,则10次调用中有50%失败(即5次)打开熔断断路器;****若为TIME_BASED则,此时还有额外的两个设置属性,含义为:在N秒内(sliding-window-size)100%(slow-call-rate-threshold)的请求超过N秒(slow-call-duration-threshold)打开断路器。
slowCallRateThreshold 以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为慢调用,当慢调用比例大于等于峰值时,断路器开启,并进入服务降级。
slowCallDurationThreshold 配置调用时间的峰值,高于该峰值的视为慢调用。
permitted-number-of-calls-in-half-open-state 运行断路器在HALF_OPEN状态下时进行N次调用,如果故障或慢速调用仍然高于阈值,断路器再次进入打开状态。
minimum-number-of-calls 在每个滑动窗口期样本数,配置断路器计算错误率或者慢调用率的最小调用数。比如设置为5意味着,在计算故障率之前,必须至少调用5次。如果只记录了4次,即使4次都失败了,断路器也不会进入到打开状态。
wait-duration-in-open-state 从OPEN到HALF_OPEN状态需要等待的时间
5.1.4 熔断 + 降级需求说明

6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。

等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。

如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。

具体时间和频次等属性见具体实际案例

5.2 熔断降级案例

5.2.1 计数的滑动窗口(count_based)

1.修改cloud-provider-payment8001

新建PayCircuitController类

@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();
    }
}

2.修改PayFeignApi接口

新增

/**
 * Resilience4j CircuitBreaker 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/circuit/{id}")
String myCircuit(@PathVariable("id") Integer id);

3.修改cloud-consumer-feign-order80

改pom

<!--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>

完整YML

server:
  port: 8080

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      #新增	
      circuitbreaker:
        enabled: true
        group:
          enabled: true
      #    
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true
      client:
        config:
          #全局配置
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
          #单个微服务的配置,细粒度的重写全局
          cloud-payment-service:
            #连接超时时间
            connect-timeout: 20000
            #读取超时时间
            read-timeout: 20000
logging:
  level:
    com.atguigu.cloud.apis.PayFeignApi: debug
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
#新增
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
            

新增OrderCircuitController类

@RestController
public class OrderCircuitController {
    @Resource
    private PayFeignApi payFeignApi;

    @GetMapping(value = "/feign/pay/circuit/{id}")
    @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
    public String myCircuitBreaker(@PathVariable("id") Integer id) {
        return payFeignApi.myCircuit(id);
    }

    //myCircuitFallback就是服务降级后的兜底处理方法
    public String myCircuitFallback(Integer id, Throwable t) {
        // 这里是容错处理逻辑,返回备用结果
        return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
    }
}

@CircuitBreaker注解,以及默认的回调fallbackMethod

5.2.2 测试

5.2.3 计时的滑动窗口(TIME_BASED)

原理

基于时间的滑动窗口是通过有N个桶的环形数组实现。

如果滑动窗口的大小为10秒,这个环形数组总是有10个桶,每个桶统计了在这一秒发生的所有调用的结果(部分统计结果),数组中的第一个桶存储了当前这一秒内的所有调用的结果,其他的桶存储了之前每秒调用的结果。

滑动窗口不会单独存储所有的调用结果,而是对每个桶内的统计结果和总的统计值进行增量的更新,当新的调用结果被记录时,总的统计值会进行增量更新。

检索快照(总的统计值)的时间复杂度为O(1),因为快照已经预先统计好了,并且和滑动窗口大小无关。

关于此方法实现的空间需求(内存消耗)约等于O(n)。由于每次调用结果(元组)不会被单独存储,只是对N个桶进行单独统计和一次总分的统计。

每个桶在进行部分统计时存在三个整型,为了计算,失败调用数,慢调用数,总调用数。还有一个long类型变量,存储所有调用的响应时间。

YML修改

resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
  circuitbreaker:
    configs:
    #配置解释,在slidingWindowSize(s)这个窗口内,最少有minimumNumberOfCalls个请求,且这么多请求中,有minimumNumberOfCalls * slowCallRateThreshold次的时间是大于slowCallDurationThreshold时,就打开断路器,导致正常的请求也也会走降级方法,返回默认值。但是在5s后就变为半开状态,同时,再有请求时,放过去2个,如果都成功,切换回关闭状态,如果失败,继续等关闭5s然后尝试。
      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

为了避免影响实验,关闭FeignConfig的重试3次

    @Bean
    public Retryer retryer(){
        return Retryer.NEVER_RETRY;
        //maxAttempts - 1才是重试次数
        //最大请求次数(1 + 2),初始间隔时间100ms,重试间最大间隔时间1s
//        return new Retryer.Default(100,1,3);
    }

5.2.3 测试结果

5.3 熔断+降级小总结


断路器开闭的条件

当慢查询达到一定峰值,或失败率达到一定条件后,断路器转为open状态,服务熔断。当open时,都走的时fallbackMethod兜底方法,服务降级。

根据配置,一段时间后,open转为half_open状态,会根据配置,放几个请求过去,如果都成功转为closed,否则继续open。

阳哥建议使用COUNT_BASED类型,计数类型。

5.4 隔离(bulkhead)

5.4.1 官网

https://resilience4j.readme.io/docs/bulkhead

中文

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/bulkhead.md

5.4.2 是什么

bulkhead(船的)舱壁/(飞机的)隔板

隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。

限制并发

5.4.3 能干吗


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

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

信号量(SemaphoreBulkhead)固定线程池(FixedThreadPoolBulkhead)

5.5 SemaphoreBulkhead案例

5.5.1 概述

基本上就是JUC信号灯内容的同样思想

信号量舱壁(SemaphoreBulkhead)原理

当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。

当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。

5.5.2 源码
public SemaphoreBulkhead(String name, @Nullable BulkheadConfig bulkheadConfig,
    Map<String, String> tags) {
    this.name = name;
    this.config = requireNonNull(bulkheadConfig, CONFIG_MUST_NOT_BE_NULL);
    this.tags = requireNonNull(tags, TAGS_MUST_NOTE_BE_NULL);
    // init semaphore 
    //java.util.concurrent.Semaphore
    this.semaphore = new Semaphore(config.getMaxConcurrentCalls(), config.isFairCallHandlingEnabled());

    this.metrics = new BulkheadMetrics();
    this.eventProcessor = new BulkheadEventProcessor();
}

5.5.3 修改PayCircuitController

//=========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();
}
5.5.4 修改PayFeignApi接口
/**
 * Resilience4j Bulkhead 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/bulkhead/{id}")
String myBulkhead(@PathVariable("id") Integer id);
5.5.5 修改cloud-consumer-feign-order80

POM

<!--resilience4j-bulkhead-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>

YML

resilience4j:
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
        maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
    instances:
      cloud-payment-service:
        baseConfig: default
  timelimiter:
    configs:
      default:
        timeout-duration: 20s

OrderCircuitController修改

/**
 *(船的)舱壁,隔离
 * @param id
 * @return
 */
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id) {
    return payFeignApi.myBulkhead(id);
}

public String myBulkheadFallback(Throwable t) {
    return "myBulkheadFallback,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
5.5.6 测试

5.6 FixedThreadPoolBulkhead案例

5.6.1 概述

基本上就是我们JUC-线程池内容的同样思想

固定线程池舱壁(FixedThreadPoolBulkhead)

FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。

当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。当线程池中无空闲时时,接下来的请求将进入等待队列,若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。

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

5.6.2 源码
public FixedThreadPoolBulkhead(String name, @Nullable ThreadPoolBulkheadConfig bulkheadConfig,
    Map<String, String> tags) {
    this.name = name;
    this.config = requireNonNull(bulkheadConfig, CONFIG_MUST_NOT_BE_NULL);
    this.tags = requireNonNull(tags, TAGS_MUST_NOTE_BE_NULL);
    // init thread pool executor
    this.executorService = new ThreadPoolExecutor(config.getCoreThreadPoolSize(),
        config.getMaxThreadPoolSize(),
        config.getKeepAliveDuration().toMillis(), TimeUnit.MILLISECONDS,
        config.getQueueCapacity() == 0 ? new SynchronousQueue<>() : new ArrayBlockingQueue<>(config.getQueueCapacity()),
        new BulkheadNamingThreadFactory(name),
        config.getRejectedExecutionHandler());
    // adding prover jvm executor shutdown
    this.metrics = new FixedThreadPoolBulkhead.BulkheadMetrics();
    this.eventProcessor = new FixedThreadPoolBulkhead.BulkheadEventProcessor();
}

@Override
public <T> CompletableFuture<T> submit(Callable<T> callable) {
    final CompletableFuture<T> promise = new CompletableFuture<>();
    try {
        CompletableFuture.supplyAsync(ContextPropagator.decorateSupplier(config.getContextPropagator(),() -> {
            try {
                publishBulkheadEvent(() -> new BulkheadOnCallPermittedEvent(name));
                return callable.call();
            } catch (CompletionException e) {
                throw e;
            } catch (Exception e){
                throw new CompletionException(e);
            }
        }), executorService).whenComplete((result, throwable) -> {
            publishBulkheadEvent(() -> new BulkheadOnCallFinishedEvent(name));
            if (throwable != null) {
                promise.completeExceptionally(throwable);
            } else {
                promise.complete(result);
            }
        });
    } catch (RejectedExecutionException rejected) {
        publishBulkheadEvent(() -> new BulkheadOnCallRejectedEvent(name));
        throw BulkheadFullException.createBulkheadFullException(this);
    }
    return promise;
}
5.6.3 修改cloud-consumer-feign-order80

POM用修改了,引入resilience4j-bulkhead就包括Fixed..类型了

YML

####resilience4j bulkhead -THREADPOOL的例子
resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #timelimiter默认限制远程1s,超过报错不好演示效果所以加上10秒
  thread-pool-bulkhead:
    configs:
      #最大请求是max-thread-pool-size + queue-capacity个,后面的如果再来报错
      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 新启线程和原来主线程脱离

修改

/**
 * (船的)舱壁,隔离,THREADPOOL
 * @param id
 * @return
 */
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadPoolFallback",type = Bulkhead.Type.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)
{
    return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
5.6.4 测试

5.7 限流(ratelimiter)

5.7.1 官网

https://resilience4j.readme.io/docs/ratelimiter

中文

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/ratelimiter.md

5.7.2 是什么

限流:就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。

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

所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

5.7.3 常见限流算法

漏桶算法

一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。

如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。


这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。

令牌桶算法


令牌桶算法(Token Bucket Algorithm)是一种流量控制算法,广泛应用于计算机网络的流量整形和速率限制中。其工作原理相对直观:

  1. 初始化:创建一个令牌桶,并设定桶的最大容量(以令牌的数量表示)。初始时,桶内可能为空或含有一定数量的令牌。
  2. 令牌生成:以恒定的速率向桶内添加令牌。如果桶已满,则新生成的令牌会被丢弃。
  3. 请求处理:每当有一个数据包(或请求)到达时,系统会尝试从桶中取出一个令牌。如果此时桶中有可用的令牌,则取出令牌并允许数据包通过(即处理请求)。如果桶中没有令牌,则该数据包可能被延迟或者直接拒绝。
  4. 突发流量处理:由于桶可以存储一定数量的令牌,所以当短时间内有大量的数据包到来时(即突发流量),只要桶内的令牌足够,就可以立即处理这些数据包,从而支持一定程度上的流量突增。
  5. 动态调整:某些实现允许动态调整生成令牌的速率或桶的大小,以适应不同的网络状况或服务质量(QoS)需求。

与漏桶算法相比,令牌桶算法能够更好地处理突发流量,因为它允许某种程度上的“存储”流量处理能力(即存储令牌),而漏桶算法则严格按恒定速率处理所有到来的流量,超出处理能力的流量都会被丢弃。
滚动时间窗算法

允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。

由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次,but......


缺点

假如设定1分钟最多可以请求100次某个接口,如12:00:00-12:00:59时间段内没有数据请求但12:00:59-12:01:00时间段内突然并发100次请求,紧接着瞬间跨入下一个计数周期计数器清零;在12:01:00-12:01:01内又有100次请求。那么也就是说在时间临界点左右可能同时有2倍的峰值进行请求,从而造成后台处理请求加倍过载的bug,导致系统运营能力不足,甚至导致系统崩溃,/(ㄒoㄒ)/~~

滑动时间窗算法

滑动时间窗口(sliding time window)

顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解:

- 窗口:需要定义窗口的大小

- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小

滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,

不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次

5.8 限流案例

5.8.1 修改cloud-provider-payment8001

PayCircuitController类新增

//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id)
{
    return "Hello, myRatelimit欢迎到来 inputId:  "+id+" \t " + IdUtil.simpleUUID();
}
5.8.2 PayFeignApi
/**
 * Resilience4j Ratelimit 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/ratelimit/{id}")
String myRatelimit(@PathVariable("id") Integer id);
5.8.3 修改cloud-consumer-feign-order80

POM

<!--resilience4j-ratelimiter-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
</dependency>

YML

####resilience4j ratelimiter 限流的例子
resilience4j:
  ratelimiter:
    configs:
      default:
        limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
        limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
        timeout-duration: 1 # 线程等待权限的默认等待时间
    instances:
      cloud-payment-service:
        baseConfig: default

OrderCircuitController类新增

@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ㄒ)/~~";
}
5.8.4 测试

只是为了记录自己的学习历程,且本人水平有限,不对之处,请指正。