CircuitBreaker断路器

chenxin
19
2024-11-27

概述

分布式系统可能面临的问题

复杂分布式体系结构中的应用程序有数十个依赖(可以理解为调用)关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

解决思路

有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
一句话,出故障了“保险丝”跳闸,别把整个家给烧了,😄

解决方法

  • 服务熔断:类似保险丝,闭合(Close)状态可以使用,达到最大服务访问后,跳闸(Open)状态,拒绝访问,调用方会接受服务降级的处理并返回友好兜底提示
  • 服务降级:不让客户端继续等待并返回一个友好提示(fallback),例如“服务器忙,请稍后再试”
  • 服务限流:秒杀等高并发操作,禁止一窝蜂拥挤,排队,一秒钟N个,有序进行
  • 服务限时:指定时间内才能访问
    ...
    使用SpringCloud Circuit Breaker解决

Circuit Breaker

官网地址

Circuit Breaker只是一套规范和接口,落地实现者是Resilience4J
Resilience4J = Resilience for java

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

Resilience4J

概述

什么使Resilience4J

Resilience4j is a lightweight fault tolerance library inspired by Netflix Hystrix, but designed for functional programming.
Resilience4j provides higher-order functions (decorators) to enhance any functional interface, lambda expression or method reference with a Circuit Breaker, Rate Limiter, Retry or Bulkhead. You can stack more than one decorator on any functional interface, lambda expression or method reference. The advantage is that you have the choice to select the decorators you need and nothing else.

Resilience4j是一个轻量级容错库,灵感来自 Netflix Hystrix,但专为函数式编程而设计。Resilience4j
提供高阶函数(装饰器),以使用断路器、速率限制器、重试或隔离来增强任何函数式接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆叠多个装饰器。这样做的好处是,您可以选择所需的装饰器,而无需选择其他装饰器。
Resilienced4J需要Java17
源码地址
Resilience4J的作用

用法
官网用法

熔断(CircuitBreaker)(服务熔断+服务降级)

断路器三大状态

断路器三大状态

断路器状态转换

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

  • 关闭状态(CLOSED)当熔断器关闭时,所有的请求都会通过熔断器:
    如果失败率超过设定的阈值,断路器将从关闭状态转移到打开状态。这时所有的请求都会被拒绝。
    当经过一段时间后,断路器会从打开状态转移到半开状态,这时仅有一定数量的请求会被允许,并重新计算失败率。
    如果失败率高于阈值,则继续为打开状态;如果失败率低于阈值,则变为关闭状态。
  • 断路器的滑动窗口期可以存储指定数量的调用的结果,你可以选择以下两种滑动窗口的模式:
    基于调用次数:最近N次调用的返回结果。
    基于时间:最近T秒内的调用。、
  • 开启状态(OPEN):
    在开启状态下,所有的请求都会直接失败,并返回错误信息。
  • 除以上以外,断路器还包含两个特殊状态:
    DISABLED(始终允许访问):不会记录事件的成功或失败。
    FORCED_OPEN(始终拒绝访问):断路器保持为开启状态,但不会记录事件的成功或失败。

断路器配置参数

官网相关配置
默认CircuitBreaker.java配置类:io.github.resilience4j.circuitbreaker.CircuitBreakerConfig
精简版中文配置手册

熔断降级案例

6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
案例

计数的滑动窗口

8001微服务新建Controller

package com.atguigu.cloud.controller;
import cn.hutool.core.util.IdUtil;
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;
@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();
    }
}

在通用api模块的PayFeignApi接口中加入下面部分

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

思考:Circuit应该装在哪个部分?
保险丝应该装在家庭部分而不是装在国家电网侧,因此需要装在调用接口侧,在本例中即feign80侧

修改feign80服务

引入依赖

<!--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
在application.yml中加入以下内容

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
      circuitbreaker:
        enabled: true
        group:
          enabled: true #没有开分组就不会用分组的配置,开了分组,则精确优先,分组次之,默认最后,现在开了分组什么都没写,可以说是开了一个默认配置
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         

开启熔断器

cloud:
      circuitbreaker:
        enabled: true
        group:
          enabled: true 

配置熔断器,设置完circuit的配置后,指定cloud-payment-service微服务的配置为default配置,即circuit的默认配置
recordExceptions:
- java.lang.Exception
即当出现这样的异常,就视为调用失败,走服务降级兜底方法

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   

完整的application.yml

server:
  port: 80

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:
      client:
        config:
          default:
            connect-timeout: 20000
            read-timeout: 20000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048
          mime-types: text/xml,application/xml,application/json
        response:
          enabled: true
      # 开启circuit breaker和分组激活功能
      circuitbreaker:
        enabled: true
        group:
          enabled: true #没有开分组就不会用分组的配置,开了分组,则精确优先,分组次之,默认最后,现在开了分组什么都没写,可以说是开了一个默认配置
logging:
  level:
    com:
      atguigu:
        cloud:
          apis:
            PayFeignApi: debug
# 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

新建OrderCircuitController

package com.atguigu.cloud.controller;

import com.atguigu.cloud.apis.PayFeignApi;
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;

@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注解内容如下,name指定了要熔断的服务,建议与application.yml中指定的要熔断服务相同;fallbackMethod则是指定了服务降级的兜底处理方法

package io.github.resilience4j.circuitbreaker.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface CircuitBreaker {
    String name();

    String fallbackMethod() default "";
}

通过http://localhost/feign/pay/circuit/-4访问错误接口
通过http://localhost/feign/pay/circuit/11访问正确接口
当服务熔断后,即使访问正确的接口,返回值也仍然是服务降级的兜底方法

时间的滑动窗口

基于时间的滑动窗口是通过有 N 个桶的环形数组实现。
如果滑动窗口的大小为 10 秒,这个环形数组是 10 个桶,每个桶统计了在这一秒内发生的所有调用的结果(部分统计结果),数组中的第一个桶存储了当前这一秒的所有调用的结果,其余的桶存储了之前每秒调用的结果。
滑动窗口不会单独存储所有调用的结果,而是对每个桶的统计结果和总的统计值进行增量更新,当前的调用结果被记录时,总的统计值会进行增量更新。
检查快照(总的统计值)的时间复杂度为 O(1),因为快照已经预先统计好了,并且和滑动窗口大小无关。
关于此方法实现的空间复杂度(内存消耗)约等于 O(n),由于每次调用结果(元素)不会被单独存储,只是对 N 个桶进行单独统计和一次的总统计。
每个桶在统计活动时存储的统计值有三个变量,为了计算,失败调用数、慢调用数、总调用数。还有一个 long 类型变量,存储所有调用用时的总时间。

修改feign80服务的yml

将计数的滑动窗口配置部分注释并引入下面部分

# 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 

为了测试方便,可以关闭OpenFeign的重试功能
通过http://localhost/feign/pay/circuit/11访问正常
http://localhost/feign/pay/circuit/9999 访问故意超时,会走服务降级的兜底方法。
超时和正常访问同时进行,正常仍然可以进行访问。
但同时访问多个超时和一个正常,那么正常访问也受到了牵连,因为服务熔断不能访问了。

总结

当满足一定的峰值和失败率达到一定条件后,断路器会进如OPEN状态,服务熔断。
OPEN状态时,所有的请求都会走fallbackmethod兜底背锅方法,服务降级
一段时间后,断路器会从OPEN状态转为HALF_OPEN半开状态,会允许几个请求调用主业务方法,若成功,则会进入CLOSED状态,否则继续开启,重复上述过程。
不要混合使用,建议按照调用次数进行服务熔断。

隔离(BulkHead)

概述


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

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

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

实现SemaphoreBulkhead

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

如图所示,三个停车位都已经停车,则信号灯就转为红色状态,那么红色状态的停车位就不能停车了。只有停的车走后,信号灯转为绿色,才可以继续停车。
即限制了最多只能停三辆车,从而不让系统崩溃。

修改8001服务Controller

在PayCircuitController中加入

  //=========Resilience4j bulkhead 的例子
    @GetMapping(value = "/pay/bulkhead/{id}")
    public String myBulkhead(@PathVariable("id") Integer id)
    {
        if(id == -4) throw new RuntimeException("----bulkhead id 不能负数");
        if(id == 9999){
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        return "Hello, bulkhead! inputId:  "+id+" \t " + IdUtil.simpleUUID();
    }

修改通用模块PayFeignApi接口

PayFeignApi接口新增仓壁api方法

/**
 * Resilience4j Bulkhead 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id)

修改Feign80服务

导入依赖

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

修改yml
示例

####resilience4j bulkhead 的例子
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ㄒ)/~~";
    }

测试,单独访问http://localhost/feign/pay/bulkhead/11即可正常,
同时访问两个http://localhost/feign/pay/circuit/9999,已经达到最大并发数2,在访问http://localhost/feign/pay/bulkhead/11,经过等待时间1秒后则会走服务降级方法

实现FixedTreadPoolBulkhead(固定线程池舱壁)

固定线程池舱壁(FixedThreadPoolBulkhead)
FixedThreadPoolBulkhead的功能与SemaphoreBulkhead一样也是用于限制并发执行的次数的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将进入等待队列,
若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,
在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回CompletableFuture类型的方法
FixedTreadPoolBulkhead的底层即JUC中的线程池TreadPoolExecutor

submit进线程池返回CompletableFuture

修改Feign80

引入依赖
与实现SemaphoreBulkhead的依赖相同,若引入则不用引入

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

修改yml
示例
注释掉实现SemaphoreBulkhead的部分并添加下面部分

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
# 由于CompletableFuture使用线程池是新起一个线程池,所以需要把circuit的分组功能去掉,要么配置下面这句话,要么把最初的分组功能注释掉才能取得成功
# 这里我下面这将之前配置的分组功能注释掉
# spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离

max-thread-pool-size包括了core-thread-pool-size,在加上queue-capacity的一个,即最多只能接受两个请求
完整的application.yml

server:
  port: 80

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:
      client:
        config:
          default:
            connect-timeout: 20000
            read-timeout: 20000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048
          mime-types: text/xml,application/xml,application/json
        response:
          enabled: true
      # 开启circuit breaker和分组激活功能
      circuitbreaker:
        enabled: true
#        group:
#          enabled: true #没有开分组就不会用分组的配置,开了分组,则精确优先,分组次之,默认最后,现在开了分组什么都没写,可以说是开了一个默认配置
logging:
  level:
    com:
      atguigu:
        cloud:
          apis:
            PayFeignApi: debug
# 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
# 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
####resilience4j bulkhead 的例子
#resilience4j:
#  bulkhead:
#    configs:
#      default:
#        maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
#        maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
#    instances:
#      cloud-payment-service:
#        baseConfig: default
#  timelimiter:
#    configs:
#      default:
#        timeout-duration: 20s
####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 新启线程和原来主线程脱离

修改OrderCircuitController
加入下面内容

/**
 * (船的)舱壁,隔离,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ㄒ)/~~");
}

测试
http://localhost/feign/pay/bulkhead/1
http://localhost/feign/pay/bulkhead/2
http://localhost/feign/pay/bulkhead/3
当我们首先请求前两个url,之后再请求第三个url,由于我们的配置最多并行度为2,所以第三次请求会直接走降级方法

限流(RateLimiter)

概述

限流就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。
比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。

常见的限流算法

漏桶算法

一个固定容量的漏桶,按照设定常量固定速率流出水滴,类似医院打吊针,不管你源头流量多大,我设定匀速流出。
如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。

有两个影响因素,分别是桶的大小,支持流量突发增多时可以存放多少的水(burst),另外则是水桶漏洞的大小(rate)。
漏出速率是固定的参数,所以即便网络不存在资源冲突,露桶算法也不能使流突发(burst)到端口速率。
所以露桶算法对存在突发特性的流量来说缺乏效率。

令牌桶算法

SpringCloud默认算法

桶中有令牌,只有桶中令牌存在的时候,请求才会拿到令牌并被接受。
请求结束后则向桶中添加令牌,若桶中不存在令牌,那么到达的请求就会被丢弃。

滚动时间窗

允许固定数量的请求进入(比如1秒取4个数据相加,超过25值就over)超过数量就拒绝或者排队,等下一个时间段进入。
由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。

第一秒的请求总数为27,超过了我们设置的25次,那么这27次请求都会被拒绝。

但滚动时间窗的缺点是,间隔临界的一段时间内的请求会超过系统限制,造成双杀

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

滑动时间窗口

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

  • 窗口:需要定义窗口的大小
  • 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
    滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,
    不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次

操作

8001服务新增myRatelimit方法

//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id)
{
    return "Hello, myRatelimit欢迎到来 inputId:  "+id+" \t " + IdUtil.simpleUUID();
}

PayFeignApi接口新增限流api方法

/**
 * Resilience4j Ratelimit 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id);

Feign80添加依赖

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

Feign80修改yml

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

Feign80修改Controller
使用@RateLimiter注解标记限流方法

 /***
 * 限流
  */
    @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ㄒ)/~~";
    }

测试
http://localhost/feign/pay/ratelimit/11
快速点击,则会出现服务降级方法

动物装饰