09、Circuit Breaker断路器

注:本篇文章主要参考周阳老师讲解的cloud进行整理的!

1、概述

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

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

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

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

  • 我们的诉求

问题:禁止服务雪崩故障

解决:

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

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

  • 如何搞定上述问题,避免整个系统大面积故障

    • 服务熔断
      类比保险丝,保险丝闭合状态(CLOUSE)可以正常使用,当达到最大服务访问请求后,就是家里保险丝从闭合CLOUSE供电状态->跳闸OPEN打开状态
    • 服务降级
      服务器忙,请稍后再试
      不让客户端等待并立刻返回一个友好提示,fallback
    • 服务限流
      秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
    • 服务限时
    • 服务预热
    • 接近实时的监控
    • 兜底的处理动作
  • 我们用什么替代?
    Spring Cloud Circuit Breaker

2、Circuit Breaker是什么

官网

  • 实现原理

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

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

3、Resilience4J

3.1、是什么

https://github.com/resilience4j/resilience4j#introduction

3.2、能干嘛

https://github.com/resilience4j/resilience4j#overview

3.3、怎么玩

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

4、案例实战

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

4.1.1、断路器3大状态

在这里插入图片描述

4.1.2、断路器3大状态之间的转换

在这里插入图片描述

4.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

  • 默认Circuit Breaker.java配置类
    io.github.resilience4j.circuitbreaker.CircuitBreakerConfig

  • 中文手册精简版

配置参数配置说明
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状态需要等待的时间

4.2、按照COUNT_BASED(计数的滑动窗口)

4.2.1、修改cloud-provider-payment8001

新建PayCircuitController

package com.mui.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 {

    @GetMapping("/pay/circuit/{id}")
    public String myCircuit(@PathVariable("id") Integer id) {
        if (id < 0) {
            throw new RuntimeException("----------circuit id 不能是负数");
        }
        if (id == 999) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        return "Hello, circuit! inputId: " + id + "\t" + IdUtil.simpleUUID();
    }

}

4.2.2、修改PayFeignApi接口

package com.mui.cloud.api;

import com.mui.cloud.entities.PayDTO;
import com.mui.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {

    @PostMapping(value = "/pay/add")
    ResultData<String> addPay(@RequestBody PayDTO payDTO);

    @DeleteMapping(value = "/pay/delete/{id}")
    ResultData<Integer> deletePay(@PathVariable("id") Integer id);

    @PutMapping(value = "/pay/update")
    ResultData<String> updatePay(@RequestBody PayDTO payDTO);

    @GetMapping(value = "/pay/get/{id}")
    ResultData getById(@PathVariable("id") Integer id);

    @GetMapping(value = "/pay/getAll")
    ResultData<List> getAll();

    @GetMapping(value = "/pay/get/info")
    String mylb();

    /**
     * Resilience4j CircuitBreaker 的例子
     * @date 2024/3/12 14:55
     * @param id
     * @return String
     */
    @GetMapping(value = "/pay/circuit/{id}")
    String myCircuit(@PathVariable(value = "id") Integer id);

}

4.2.3、修改cloud-consumer-feign-order80

4.2.3.1、改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>
4.2.3.2、修改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:
            # 连接超时时间
            connectTimeout: 3000
            # 读取超时时间
            readTimeout: 3000
          cloud-payment-service:
            # 连接超时时间
            connectTimeout: 3000
            # 读取超时时间
            readTimeout: 3000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          # 最小触发压缩的大小
          min-request-size: 2048
          # 触发压缩的数据类型
          mime-types:
            - text/xml
            - application/xml
            - application/json
        response:
          enabled: true
      circuitbreaker:
        enabled: true
        # 没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
        group:
          enabled: true

logging:
  level:
    com:
      mui:
        cloud:
          api:
            PayFeignApi: debug

# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
#  6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
#  等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
#  如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
resilience4j:
  circuitbreaker:
    configs:
      default:
        # 设置50%的调用失败时打开断路器,超过失败请求百分⽐CircuitBreaker变为OPEN状态。
        failureRateThreshold: 50
          # 滑动窗口的类型
        slidingWindowType: COUNT_BASED
        # 滑动窗⼝的⼤⼩配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
        slidingWindowSize: 6
        #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果minimumNumberOfCalls为10,则必须最少记录10个样本,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
        minimumNumberOfCalls: 6
        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
4.2.3.3、新建OrderCircuitController
package com.mui.cloud.controller;

import com.mui.cloud.api.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(value = "id") Integer id) {
        return payFeignApi.myCircuit(id);
    }

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

}

4.3、测试

  • 正确
    http://localhost/feign/pay/circuit/1
  • 错误
    http://localhost/feign/pay/circuit/-1

一次error一次ok,50%错误后触发熔断并给出服务降级,告知调用者服务不可用,此时就算是输入正确的访问地址也无法调用服务,他还在断路中(OPEN状态),一会过度到半开并继续正确地址访问,慢慢切换回CLOSE状态,就可以正常链路回复

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

5.1、基于时间的滑动窗口

在这里插入图片描述

5.2、修改cloud-consumer-feign-order80

5.2.1、修改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:
            # 连接超时时间
            connectTimeout: 3000
            # 读取超时时间
            readTimeout: 3000
          cloud-payment-service:
            # 连接超时时间
            connectTimeout: 3000
            # 读取超时时间
            readTimeout: 3000
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          # 最小触发压缩的大小
          min-request-size: 2048
          # 触发压缩的数据类型
          mime-types:
            - text/xml
            - application/xml
            - application/json
        response:
          enabled: true
      circuitbreaker:
        enabled: true
        # 没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
        group:
          enabled: true

logging:
  level:
    com:
      mui:
        cloud:
          api:
            PayFeignApi: debug

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

5.3、测试

一次超时,一次正常访问,同时进行

6、隔离(BulkHead)

官网

6.1、是什么

限并发

bulkhead(船的)舱壁/(飞机的)隔板
隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。
在这里插入图片描述

6.2、能干嘛

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

6.3、Resilience4J提供了如下两种隔隔离的实现方式,可以限制并发执行的数量

在这里插入图片描述

6.4、实现SemaphoreBulkHead(信号量舱壁)

6.4.1、概述

基本上就是我们JUC信号灯内容的同样思想在这里插入图片描述信号量舱壁(SemaphoreBulkhead)原理
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。

6.4.2、源码分析

io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead
在这里插入图片描述

6.4.3、cloud-provider-payment8001支付微服务修改PayCircuitController

package com.dashun.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 {

    @GetMapping(value = "/pay/circuit/{id}")
    public String myCircuit(@PathVariable(value = "id") Integer id) {
        if (id < 0) {
            throw new RuntimeException("----circuit id 不能是负数");
        }
        if (id == 999) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "Hello, circuit! inputId: " + id + "\t " + IdUtil.simpleUUID();
    }

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

}

6.4.4、PayFeignApi接口新增舱壁api方法

@GetMapping("/pay/bulkhead/{id}")
String myBulkhead(@PathVariable("id") Integer id);

6.4.5、修改cloud-consumer-feign-order80

6.4.5.1 增加依赖
<!--resilience4j-bulkhead-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>
6.4.5.2、修改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
6.4.5.3、业务类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ㄒ)/~~";
}
6.3.5.4、测试

浏览器新打开2个窗口,各点一次,分别点击http://localhost/feign/pay/bulkhead/9999
每个请求调用需要耗时5秒,2个线程瞬间达到配置过的最大并发数2
此时第3个请求正常的请求访问,http://localhost/feign/pay/bulkhead/3
直接被舱壁限制隔离了,碰不到8001
等其中一个窗口停止了,再去正常访问,并发数小于2 了,可以OK
在这里插入图片描述

6.5、实现FixedThreadPoolBulkhead(固定线程池舱壁)

6.5.1、概述

基本上就是我们JUC-线程池内容的同样思想
在这里插入图片描述固定线程池舱壁(FixedThreadPoolBulkhead)

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

  • 当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。

  • 当线程池中无空闲时时,接下来的请求将进入等待队列,若等待队列仍然无剩余空间时接下来的请求将直接被拒绝,在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。

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

6.5.2、源码分析

io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead

底子就是JUC里面的线程池submit进线程池返回CompletableFuture

6.5.3、修改cloud-consumer-feign-order80

6.5.3.1 修改pom
<!--resilience4j-bulkhead-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>
6.5.3.2、修改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:
            # 连接超时时间
            connectTimeout: 3000
            # 读取超时时间
            readTimeout: 3000
          # 指定某个服务超时时间
          cloud-payment-service:
            connectTimeout: 5000
            readTimeout: 5000
      # Apache HttpClient5 配置开启
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          # 最小触发压缩大小
          min-request-size: 2048
          # 触发压缩数据类型
          mime-types: text/xml, application/xml, application/json
        response:
          enabled: true
      circuitbreaker:
        enabled: true
        # 没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
        group:
          enabled: true

logging:
  level:
    com:
      dashun:
        cloud:
          api:
            PayFeignApi: debug

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

在这里插入图片描述

6.5.3.3、OrderCircuitController

@GetMapping("/feign/pay/bulkheadthreadpool/{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) {
        throw new RuntimeException(e);
    }
    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(() -> payFeignApi.myBulkhead(id) + "\t Bulkhead.Type.THREADPOOL,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
6.5.3.4、测试

http://localhost/feign/pay/bulkheadthreadpool/5
http://localhost/feign/pay/bulkheadthreadpool/6
http://localhost/feign/pay/bulkheadthreadpool/7

7、限流(RateLimiter)

官网
中文手册

7.1、是什么

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

比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。
在这里插入图片描述
所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

7.2、常见的限流算法

7.2.1、漏斗算法(Leaky Bucket)

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

如果流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的。在这里插入图片描述
缺点
这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。在这里插入图片描述

7.2.2、令牌桶算法(Token Bucket)

在这里插入图片描述spring cloud默认使用该算法

7.2.3、滚动时间窗(tumbling time window)

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

由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。下图统计了3次,but…
在这里插入图片描述
缺点:
间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮

7.2.4、滑动时间窗口(sliding time window)

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

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

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

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

7.3、cloud-provider-payment8001支付微服务

修改PayCircuitController新增Ratelimit方法

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

7.4、 PayFeignApi接口新增限流api方法

@GetMapping("/pay/ratelimit/{id}")
String myRatelimit(@PathVariable("id") Integer id);

7.5、修改cloud-consumer-feign-order80

7.5.1、修改pom

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

7.5.2、修改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:
            # 连接超时时间
            connectTimeout: 3000
            # 读取超时时间
            readTimeout: 3000
          # 指定某个服务超时时间
          cloud-payment-service:
            connectTimeout: 5000
            readTimeout: 5000
      # Apache HttpClient5 配置开启
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          # 最小触发压缩大小
          min-request-size: 2048
          # 触发压缩数据类型
          mime-types: text/xml, application/xml, application/json
        response:
          enabled: true
      circuitbreaker:
        enabled: true
        # 没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
        group:
          enabled: true

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

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

7.5.3、修改OrderCircuitController,新增方法

@GetMapping("/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service", fallbackMethod = "myRateLimitFallback")
public String myRatelimit(@PathVariable("id") Integer id) {
    return payFeignApi.myRatelimit(id);
}

public String myRateLimitFallback(Integer id, Throwable t) {
    return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}

7.5.4、测试

http://localhost/feign/pay/ratelimit/7
狂按F5刷新
在这里插入图片描述

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值