当前位置: 首页 > news >正文

7.CircuitBreaker断路器

目录

一、Hystrix目前维护状态

二、断路器概述

三、Circuit Breaker简介

四、Resilience4J简介

五、Resilience4j 功能

六、案例实战

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

断路器3个状态的转换

断路器所有配置参数参考

熔断+降级案例需求说明

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

1.修改cloud-provider-payment8001——新建PayCircuitController

2.修改PayFeignApi接口

3.修改cloud-consumer-feign-order80

4.测试(按照错误次数达到多少后开启断路)

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

总结

2.隔离(BulkHead)

官网信息

隔离的理解

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

Resilience4j的两种隔离的实现方式

实现SemaphoreBulkhead(信号量舱壁)

概述

源码分析

cloud-provider-payment8001支付微服务修改Paycircuitcontroller

PayFeignApi接口新增舱壁api方法

修改cloud-consumer-feign-order80

测试

实现FixedThreadPoolBulkhead(因定线程池舱壁)

概述

源码分析

修改cloud-consumer-feign-order80

测试地址

3.限流(RateLimiter)

官网信息

限流解释

面试题:说说常见限流算法

1.漏斗算法(Leaky Bucket)

2.令牌桶算法(Token Bucket)

3.滚动时间窗(tumbling time window)

4.滑动时间窗囗(sliding time window)

cloud-provider-payment8001支付微服务

PayFeignApi接口新增限流api方法

修改cloud-consumer-feign-order80

测试


一、Hystrix目前维护状态

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

二、断路器概述

分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“ 扇出 ”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
问题:禁止服务雪崩故障 
解决:  - 有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。 
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝), 向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常 ,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
为了避免以上需要做到以下功能,所以我们使用 Spring Cloud Circuit Breaker
  • 服务熔断
  • 类比保险丝,保险丝闭合状态(CLOSE)可以正常使用,当达到最大服务访问后,直接拒绝访问跳闸限电(OPEN),此刻调用方会接受服务降级的处理并返回友好兜底提示
  • 就是家里保险丝,从闭合CLOSE供电状态→跳闸OPEN打开状态
  • 服务降级
不让客户端等待并立刻返回一个友好提示,fallback
比如:服务器忙,请稍后再试。
  • 服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
  • 服务限时
  • 服务预热
  • 接近实时的监控
  • 兜底的处理动作

三、Circuit Breaker简介

官网地址: Spring Cloud Circuit Breaker
Spring Cloud Circuit Breaker 提供了跨不同断路器实现的抽象。它提供了在应用程序中使用的一致 AP!,允许开发人员选择最适合您的应用程序需求的断路器实现。
Resilience For Java简称:Resilience4j
实现原理:
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。 
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
中文翻译图:
Circuit Breaker与Resilience4J的关系:
Circuit Breaker只是一套规范和接口,落地实现者是Resilience4J

四、Resilience4J简介

gitHub地址:https://github.com/resilience4j/resilience4j#1-introduction
官网地址:CircuitBreaker
中文手册: https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md
Resilience4是一个专为函数式编程设计的 轻量级容错库 。Resilience4j提供高阶函数(装饰器),以通过断路器、速率限制器、重试或隔板增强任何功能接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆善多个装饰器。优点是您可以选择您需要的装饰器,而没有其他选择。
Resilience4J 2 需要 Java 17.

五、Resilience4j 功能

Resilience4j 提供了几个核心模块:
  • resilience4j-Circuitbreaker:断路
  • resilience4j-ratelimiter:速率限制
  • resilience4j-bulkhead: 舱壁
  • resilience4j-retry:自动重试(同步和异步)
  • resilience4j-timelimiter:超时处理
  • resilience4j-cache:结果缓存
还有用于指标、Feign、Kotlin、Spring、Ratpack、Vertx、Rxava2 等的附加模块。
备注:Resilience4j 目前最常用的是:断路、速率限制、舱壁

六、案例实战

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

熔断:对多少访问失败的组件进行打开状态断路器,使该组件无法提供访问,并提供提示语,保护同时调用的其他组件防止雪崩,在进行半打开状态测试是否可以访问,可以访问关闭断路器,不能访问则继续打开断路器。
断路器3个状态的转换
  • 断路器有三个普通状态:关闭(CLOSED)、开启(OPEN)、半开(HALF_OPEN),还有两个特殊状态:禁用(DISABLED)、强制开启(FORCED OPEN)
  • 当熔断器关闭时,所有的请求都会通过熔断器。
    • 如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。
    • 当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率
    • 如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态。
  • 断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。
    • 基于访问数量的滑动窗口统计最近N次调用的返回结果。居于时间的滑动窗口统计最近N秒的调用回结果。
  • 除此以外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)
    • 这两个状态不会生成熔断器事件(除状态装换外),并且不会记录事件的成功或者失败。
    • 退出这两个状态的唯一方法是触发状态转换或者重置熔断器。
断路器所有配置参数参考
英文: CircuitBreaker
中文手册: https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/CircuitBreaker.md
默认CircuitBreaker.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状态需要等待的时间
中文手册全版参数配置
配置属性
默认值
描述
failureRateThreshold
50
以百分比配置失败率阈值。当失败率等于或大于阈值时,断路器状态并关闭变为开启,并进行服务降级。
slowCallRateThreshold
100
以百分比的方式配置,断路器把调用时间大于
slowCallDurationThreshold
的调用视为满调用,当慢调用比例大于等于阈值时,断路器开启,并进行服务降级。
slowCallDurationThreshold
60000 [ms]
配置调用时间的阈值,高于该阈值的呼叫视为慢调用,并增加慢调用比例。
permittedNumberOfCallsInHalfOpenState
10
断路器在半开状态下允许通过的调用次数。
maxWaitDurationInHalfOpenState
0
断路器在半开状态下的最长等待时间,超过该配置值的话,断路器会从半开状态恢复为开启状态。配置是0时表示断路器会一直处于半开状态,直到所有允许通过的访问结束。
slidingWindowType
COUNT_BASED
配置滑动窗口的类型,当断路器关闭时,将调用的结果记录在滑动窗口中。滑动窗口的类型可以是count-based或time-based。如果滑动窗口类型是COUNT_BASED,将会统计记录最近
slidingWindowSize
次调用的结果。如果是TIME_BASED,将会统计记录最近
slidingWindowSize
秒的调用结果。
slidingWindowSize
100
配置滑动窗口的大小。
minimumNumberOfCalls
100
断路器计算失败率或慢调用率之前所需的最小调用数(每个滑动窗口周期)。例如,如果minimumNumberOfCalls为10,则必须至少记录10个调用,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。
waitDurationInOpenState
60000 [ms]
断路器从开启过渡到半开应等待的时间。
automaticTransition
FromOpenToHalfOpenEnabled
false
如果设置为true,则意味着断路器将自动从开启状态过渡到半开状态,并且不需要调用来触发转换。创建一个线程来监视断路器的所有实例,以便在WaitDurationInOpenstate之后将它们转换为半开状态。但是,如果设置为false,则只有在发出调用时才会转换到半开,即使在waitDurationInOpenState之后也是如此。这里的优点是没有线程监视所有断路器的状态。
recordExceptions
empty
记录为失败并因此增加失败率的异常列表。
除非通过ignoreExceptions显式忽略,否则与列表中某个匹配或继承的异常都将被视为失败。
如果指定异常列表,则所有其他异常均视为成功,除非它们被ignoreExceptions显式忽略。
ignoreExceptions
empty
被忽略且既不算失败也不算成功的异常列表。
任何与列表之一匹配或继承的异常都不会被视为失败或成功,即使异常是recordExceptions的一部分。
recordException
throwable -> true·
By default all exceptions are recored as failures.
一个自定义断言,用于评估异常是否应记录为失败。
如果异常应计为失败,则断言必须返回true。如果出断言返回false,应算作成功,除非ignoreExceptions显式忽略异常。
ignoreException
throwable -> false
By default no exception is ignored.
自定义断言来判断一个异常是否应该被忽略,如果应忽略异常,则谓词必须返回true。
如果异常应算作失败,则断言必须返回false。
@CircuitBreaker
@CircuitBreaker注解用于启用断路器功能。‌ 这个注解允许开发人员使用断路器来处理故障和超时情况。‌在Spring Cloud中,‌@EnableHystrix和@EnableCircuitBreaker都是用于启用断路器的注解,‌但它们有一些区别。‌@EnableCircuitBreaker注解不仅用于启用断路器,‌还支持其他断路器实现,‌例如Resilience4j,‌因此,‌如果您想使用其他断路器实现,‌建议使用@EnableCircuitBreaker注解 1 。‌
熔断+降级案例需求说明
#  6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。#  等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。#  如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
具体时间和频次等属性见具体实际案例,这里只是作为case举例讲解
项目中稳定性方案有哪些降级,限流,熔断如何做的
项目有没有遇到过稳定性问题,如何解决和优化的
按照COUNT_BASED(计数的滑动窗口)
1.修改cloud-provider-payment8001——新建PayCircuitController
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();}
}
2.修改PayFeignApi接口
package com.atguigu.cloud.apis;import com.atguigu.cloud.entities.PayDTO;
import com.atguigu.cloud.resp.ResultData;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi
{/*** Resilience4j CircuitBreaker 的例子* @param id* @return*/@GetMapping(value = "/pay/circuit/{id}")public String myCircuit(@PathVariable("id") Integer id);
}
3.修改cloud-consumer-feign-order80
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>
3.2 写YML resilience4j 部分内容
server:port: 80spring:application:name: cloud-consumer-openfeign-order####Spring Cloud Consul for Service Discoverycloud:consul:host: localhostport: 8500discovery:prefer-ip-address: true #优先使用服务ip进行注册service-name: ${spring.application.name}openfeign:client:config:
#          default:
#            #连接超时时间
#            connectTimeout: 3000
#            #读取超时时间
#            readTimeout: 3000cloud-payment-service:#连接超时时间connectTimeout: 20000#读取超时时间readTimeout: 20000
#httpclient 开启httpclient:hc5:enabled: true
#开启请求压缩compression:request:enabled: truemin-request-size: 2048 #最小触发压缩的大小mime-types: text/xml,application/xml,application/json #触发压缩数据类型response:enabled: truecircuitbreaker:enabled: truegroup:enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
# feign日志以什么级别监控哪个接口  logging.level + 含有@FeignClient注解的完整带包名的接口名+debug   日志调试结束注释掉
#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.Exceptioninstances:cloud-payment-service:baseConfig: default
3.3 新建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
{@Resourceprivate 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 = "cloud-payment-service", fallbackMethod = "myCircuitFallback")参数要与YML中 resilience4j:instances:cloud-payment-service:参数相同
系统繁忙,请稍后再试。
不让调用者等待并立刻返回一个友好提示,fallback
4.测试(按照错误次数达到多少后开启断路)
  • 自测cloud-consumer-feign-order80
  • 查看YML
  • 正确:http://localhost/feign/pay/circuit/11
  • 错误:http://localhost/feign/pay/circuit/-4
  • -次error-次OK,trytry看看
    • 50%错误后触发熔断并给出服务降级,告知调用者服务不可用。
    • 此时就算是输入正确的访问地址也无法调用服务(我明明是正确的也不让用/(ㄒoㄒ)/~~),它还在断路中(OPEN状态),一会儿过度到半开并继续正确地址访问,慢慢切换回CLOSE状态,可以正常访问了链路回复
  • 多次故意填写错误值(负4)
    • 多次故意填写错误值(负4),然后慢慢填写正确值(正整数11),发现刚开始不满足条件,就算是正确的访问地址也不能进行。
按照TIME_BASED(时间的滑动窗口) 
  • 基于时间的滑动窗口
  • 修改cloud-consumer-feign-order80 编写YML
server:port: 80spring:application:name: cloud-consumer-openfeign-order####Spring Cloud Consul for Service Discoverycloud:consul:host: localhostport: 8500discovery:prefer-ip-address: true #优先使用服务ip进行注册service-name: ${spring.application.name}openfeign:client:config:default:#cloud-payment-service:#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒connectTimeout: 20000#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒readTimeout: 20000#开启httpclient5httpclient:hc5:enabled: true#开启压缩特性compression:request:enabled: truemin-request-size: 2048mime-types: text/xml,application/xml,application/jsonresponse:enabled: true#开启circuitbreaker和分组激活circuitbreaker:enabled: truegroup:enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后# feign日志以什么级别监控哪个接口
logging:level:com:atguigu:cloud:apis:PayFeignApi: debug# 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.Exceptioninstances:cloud-payment-service:baseConfig: default 
  • 为避免影响实验效果,记得关闭Feignconfig自己写的重试3次
测试(慢查询)
  • 一次超时,一次正常访问,同时进行
  • http://localhost/feign/pay/circuit/9999 故意超时,将会单独报错
  • http://localhost/feign/pay/circuit/11 可以访问,我是正常的
  • 第1~4个超时,整多一点干4个,一次正常访问,同时进行
    • http://localhost/feign/pay/circuit/9999
    • 正常访问也受到了牵连,因为服务熔断不能访问了:http://localhost/feign/pay/circuit/11
    • 运气好的话,可以看到全线崩,刺激。
总结
断路器开启或者关闭的条件
  • 当满足一定的峰值和失败率达到一定条件后,断路器将会进入OPEN状态(保险丝跳闸),服务熔断
  • 当OPEN的时候,所有请求都不会调用主业务逻辑方法,而是直接走fallbackmetnod兜底背锅方法,服务降级
  • 一段时间之后,这个时候断路器会从OPEN进入到HALF OPEN半开状态,会放几个请求过去探探链路是否通?如成功,断路器会关闭CLOSE(类似保险丝闭合,恢复可用);如失败,继续开启。重复上述
个人建议不要混合用,推荐按照调用次数count based

2.隔离(BulkHead)

官网信息
官网地址:Bulkhead
中文地址:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/bulkhead.md
隔离: 限制并发数量,对超过并发数量访问的进行提示。
隔离的理解
bulkhead(船的)舱壁/(飞机的)隔板
隔板来自造船行业,床仓内部一般会分成很多小隔舱,一旦一个隔舱漏水因为隔板的存在而不至于影响其它隔舱和整体船。
隔离用于限并发
隔离的作用:依赖隔离&负载保护:用来 限制 对于下游服务的最大 并发数量 的限制
Resilience4j提供了如下两种隔离的实现方式,可以限制并发执行的数量
Resilience4j的两种隔离的实现方式
  • SemaphoreBulkhead(信号量舱壁)
  • FixedThreadPoolBulkhead(因定线程池舱壁)
实现SemaphoreBulkhead(信号量舱壁)
  • 概述
  • 源码分析
  • cloud-provider-payment8001支付微服务修改Paycircuitcontroller
  • PayFeignApi接口新增舱壁api方法
  • 修改cloud-consumer-feign-order80
  • 测试
概述
基本上就是我们JUC信号灯内容的同样思想
  信号量舱壁(SemaphoreBulkhead)原理
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。
当信号量全被占用时,接下来的请求将会进入 阻塞状态 ,SemaphoreBulkhead提供了一个阻塞计时器,
如果阻塞状态的请求在阻塞计时内无法获取到信号量则系统会拒绝这些请求。
若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理。
源码分析
io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead
cloud-provider-payment8001支付微服务修改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();
}
PayFeignApi接口新增舱壁api方法
/*** Resilience4j Bulkhead 的例子* @param id* @return*/
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id);
修改cloud-consumer-feign-order80
  • POM
  • YML
  • 业务类
POM
<!--resilience4j-bulkhead-->
<dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-bulkhead</artifactId>
</dependency>
YML
1.示例
2.内容
server:port: 80spring:application:name: cloud-consumer-openfeign-order####Spring Cloud Consul for Service Discoverycloud:consul:host: localhostport: 8500discovery:prefer-ip-address: true #优先使用服务ip进行注册service-name: ${spring.application.name}openfeign:client:config:default:#cloud-payment-service:#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒connectTimeout: 20000#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒readTimeout: 20000#开启httpclient5httpclient:hc5:enabled: true#开启压缩特性compression:request:enabled: truemin-request-size: 2048mime-types: text/xml,application/xml,application/jsonresponse:enabled: true#开启circuitbreaker和分组激活circuitbreaker:enabled: truegroup:enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后# feign日志以什么级别监控哪个接口
logging:level:com:atguigu:cloud:apis:PayFeignApi: debug####resilience4j bulkhead 的例子
resilience4j:bulkhead:configs:default:maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallbackinstances:cloud-payment-service:baseConfig: defaulttimelimiter:configs:default:timeout-duration: 20s
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ㄒ)/~~";
}
@Bulkhead
Bulkhead.Type.SEMAPHORE
测试
步骤
浏览器新打开2个窗口,各点一次,分别点击http://localhost/feign/pay/bulkhead/9999 
每个请求调用需要耗时5秒,2个线程瞬间达到配置过的最大并发数2 
此时第3个请求正常的请求访问,http://localhost/feign/pay/bulkhead/3 
直接被舱壁限制隔离了,碰不到8001 
等其中一个窗口停止了,再去正常访问,并发数小于2 了,可以OK
http://localhost/feign/pay/bulkhead/9999
http://localhost/feign/pay/bulkhead/3
结果:
可以看到因为本案例并发线程数为2(maxConcurrentCalls: 2),只让2个线程进入执行,
其他请求降直接降级。
实现FixedThreadPoolBulkhead(因定线程池舱壁)
  • 概述
  • 源码分析
  • 修改cloud-consumer-feign-order80
  • 测试地址
概述
基本上就是我们JUC-线程池内容的同样思想
固定线程池舱壁(FixedThreadPoolBulkhead)
FixedThreadPoolBulkhead 的功能与 SemaphoreBulkhead 一样也是 用于限制并发执行 的次数 的,但是二者的实现原理存在差别而且表现效果也存在细微的差别。FixedThreadPoolBulkhead使用一个 固定线程池和一个等待队列 来实现舱壁。
当线程池中存在空闲时,则此时进入系统的请求将直接 进入线程池 开启新线程或使用空闲线程来处理请求。
当线程池中无空闲时时,接下来的请求将 进入等待队列
   若等待队列仍然无剩余空间时接下来的请求将 直接被拒绝
   在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外:ThreadPoolBulkhead只对CompletableFuture方法有效,所以我们必创建返回 CompletableFuture 类型的方法
源码分析
  • io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead
  • 底子就是JUC里面的线程池ThreadPoolExecutor
  • submit进线程池返回CompletableFuture<T>
修改cloud-consumer-feign-order80
  • POM
  • YML
  • controller
POM
<!--resilience4j-bulkhead-->
<dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-bulkhead</artifactId>
</dependency>
YML
示例
内容
上述内容解释
示例
实现FixedThreadPoolBulkhead(固定线程池舱壁)
FixedThreadPoolBulkhead的配置项如下:
配置名称
默认值
描述
maxThreadPoolSize
Runtime.getRuntime().availableProcessors()
配置最大线程池大小
coreThreadPoolSize
Runtime.getRuntime().availableProcessors() - 1
配置核心线程池大小
queueCapacity
100
配置队列的容量
keepAliveDuration
20 [ms]
当线程数大于核心时,这是多余空闲线程在终止前等待新任务的最长时间
writableStackTraceEnabled
true
在抛出舱壁异常时输出堆栈跟踪错误。
如果为错误,则输出带有舱壁异常的单行。
内容
server:port: 80spring:application:name: cloud-consumer-openfeign-order####Spring Cloud Consul for Service Discoverycloud:consul:host: localhostport: 8500discovery:prefer-ip-address: true #优先使用服务ip进行注册service-name: ${spring.application.name}openfeign:client:config:default:#cloud-payment-service:#连接超时时间,为避免演示出错,讲解完本次内容后设置为20秒connectTimeout: 20000#读取超时时间,为避免演示出错,讲解完本次内容后设置为20秒readTimeout: 20000#开启httpclient5httpclient:hc5:enabled: true#开启压缩特性compression:request:enabled: truemin-request-size: 2048mime-types: text/xml,application/xml,application/jsonresponse:enabled: true#开启circuitbreaker和分组激活circuitbreaker:enabled: true
#        group:
#          enabled: true # 演示Bulkhead.Type.THREADPOOL时spring.cloud.openfeign.circuitbreaker.group.enabled设为false新启线程和原来主线程脱离了。# feign日志以什么级别监控哪个接口
logging:level:com:atguigu:cloud:apis:PayFeignApi: debug####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 新启线程和原来主线程脱离
上述内容解释
max-thread-pool-size+queue-capacity是系统可容纳并发数,包括线程池1个队列1个(测试环境,生产环境则需要根据业务进行选择),超过2个则自动进行限流提示。
controller
Bulkhead.Type.THREADPOOL
@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/fixed/pay/bulkhead/1
  • http://localhost/fixed/pay/bulkhead/2
  • http://localhost/fixed/pay/bulkhead/3
备注:当三个同时访问时一个进入线程池,一个进入队列,第三个则 直接被拒绝, 实现舱壁。
core-thread-pool-size: 1 #配置核心线程池大小
max-thread-pool-size: 1 #最大线程池大小
queue-capacity: 1 #配置队列的容量

3.限流(RateLimiter)

  • 官网
  • 是什么
  • 面试题:说说常见限流算法
  • cloud-provider-payment8001支付微服务修改
  • Paycircuitcontroller新增myRatelimit方法
  • PayFeignApi接口新增限流api方法
  • 修改cloud-consumer-feign-order80
  • 测试
官网信息
官网地址 : RateLimiter
中文地址:https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/ratelimiter.md
限流器
介绍
限流是一种必不可少的技术,可以帮助您的API进行扩展,并建立服务的高可用性和可靠性。但是,这项技术还附带了一堆不同的选项,比如如何处理检测到的多余流量,或者您希望限制什么类型的请求。您可以简单地拒绝这个超限请求,或者构建一个队列以稍后执行它们,或者以某种方式组合这两种方法。
原理
Resilience4;提供了一个限流器,它将从epoch开始的所有纳秒划分为多个周期。每个周期的持续时间RateLimiterconfig.limitRefreshPeriod 。在每个周期开始时,限流器将活动权限数设置为RateLimiterconfig.limitForperiod 。期间,对于限流器的调用者,它看起来确实是这样的,但是对于 AtomicRatelimiter实现,如果RateLimiter未被经常使用,则会在后台进行一些优化,这些优化将跳过此刷新
@RateLimiter注解用于实现分布式环境下的限流功能。‌
@RateLimiter注解通过Lua脚本实现,‌该脚本接收三个变量:‌key、‌阈值count和过期时间time。‌脚本首先通过调用 get 方法获取key的值current,‌如果key存在且current大于count,‌则返回current;‌否则,‌通过 incr 函数对key进行自增操作,‌并在current等于1时(‌即第一次访问该接口时)‌通过 expire 函数为key设置过期时间。‌最后返回自增后的current值。‌这种实现方式在并发情况下能更好地满足原子性需求,‌尽管作者提到若依框架没有将脚本文件单独放在resources文件夹下,‌但这并不影响其限流功能的实现和阅读维护。‌
此外,‌@RateLimiter注解还涉及到分布式环境下的限流实现,‌适用于小型应用中对资源消耗较大操作的访问频率控制等场景。‌它采用令牌桶算法,‌通过控制令牌的发放速率来限制请求的处理速率,‌无论是单个请求还是批量请求的处理,‌都能保持相对固定的速率。‌
总的来说,‌@RateLimiter注解通过Lua脚本在分布式环境下实现了对请求速率的控制,‌适用于需要限制访问频率的场景,‌如获取手机验证码或控制对资源消耗较大操作的访问频率等。‌
创建和配置限流器参数
配置参数
默认值
描述
timeoutDuration
5 [s]
线程等待权限的默认等待时间
limitRefreshPeriod
500 [ns]
限额刷新周期。在每个周期之后,速率限制器将其权限计数恢复到limitForPeriod 值。
limitForPeriod
50
一次限制刷新期间的可用权限数目
限流解释
限流 就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。 
比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。
所谓限流,就是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速,以保护应用系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。
限流(频率控制)
面试题:说说常见限流算法
1.漏斗算法(Leaky Bucket)
2. 令牌桶算法(Token Bucket)
3.滚动时间窗(tumbling time window)
4.滑动时间窗囗(sliding time window)
这些算法各有优缺点,适用于不同的场景和需求。例如,固定窗口和滑动窗口算法适合于QPS限流和统计总访问量,而漏桶和令牌桶算法则更适合于保证请求处理的平滑性和速率限制。
1.漏斗算法(Leaky Bucket)
漏桶算法
漏桶算法。漏桶算法则是一种更加平滑的限流方式,它以固定的速率处理请求,就像漏桶以一定的速率释放水滴一样。如果请求速率超过漏桶的释放速率,则超出部分的请求会被丢弃。  
 一 个固定容量的漏桶,按照设定常量固定速率流出水滴 ,类似医院打吊针,不管你源头流量多大,我设定 匀速流出 。 
如果 流入水滴超出了桶的容量,则流入的水滴将会溢出了(被丢弃),而漏桶容量是不变的
原理 :漏桶算法是一种基于漏桶的限流算法,它维护一个固定容量的漏桶,按照固定速率漏水,当有请求到来时,漏桶中的水量增加,如果漏桶已满,则拒绝该请求。
优点 :平滑限流,可以限制请求的处理速率。
缺点 :对于突发流量,可能会出现短时的请求丢失。
这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
2.令牌桶算法(Token Bucket)
令牌桶算法
令牌桶算法。令牌桶算法中,系统以恒定的速率向桶中添加令牌,每个请求在处理前都需要从桶中获取一个令牌。如果桶中没有足够的令牌,则请求会被限流。
原理 :令牌桶算法是一种基于令牌的限流算法,它维护一个固定容量的令牌桶,按照固定速率往桶中添加令牌,每当有请求到来时,消耗一个令牌,如果桶中没有足够的令牌,则拒绝该请求。
优点 :平滑限流,可以应对突发流量;灵活控制流量速率。
缺点 :对于突发流量,需要足够的令牌桶容量用来应对,否则可能会出现丢弃部分请求的情况。
3.滚动时间窗(tumbling time window)
滚动时间窗(tumbling time window)
滚动时间窗也叫 固定窗口算法
固定窗口算法。这是一种简单的计数器算法,它在固定的时间窗口内累加访问次数。当访问次数达到设定的阈值时,触发限流策略。这种方法在每个新的时间窗口开始时进行计数器的清零。
允许固定数量的请求进入(比如1秒取4个数据相加(4个数据该秒分别访问次数相加如:9+6+8+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ㄒ)/~~ 
4.滑动时间窗囗(sliding time window)
滑动时间窗口(sliding time window)
顾名思义,该时间窗口是滑动的。所以,从概念上讲,这里有两个方面的概念需要理解: 
- 窗口:需要定义窗口的大小
- 滑动:需要定义在窗口中滑动的大小,但理论上讲滑动的大小不能超过窗口大小
滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第2个时间点,结束时间点增加一个时间点,
不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。下图统计了5次
滑动窗口算法。滑动窗口算法是对固定窗口算法的改进,它将时间窗口分为多个小周期,每个小周期都有自己的计数器。随着时间的滑动,过期的小周期数据被删除,这样可以更精确地控制流量。
cloud-provider-payment8001支付微服务
修改Paycircuitcontroller新增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);
修改cloud-consumer-feign-order80
  • POM
  • YML
  • order的controller
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刷新一次,将允许处理的最大请求数量重置为limitForPeriodtimeout-duration: 1 # 线程等待权限的默认等待时间instances:cloud-payment-service:baseConfig: default
order的controller
@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
结果
刷新上述地址,正常后F5按钮狂刷一会儿,停止刷新看到被限流的效果

相关文章:

  • DALI DT6与DALI DT8介绍
  • 嵌入式开发学习日志(linux系统编程--进程(4)——线程锁)Day30
  • 界面控件DevExpress WinForms中文教程:Banded Grid View - 如何固定Bands?
  • ESP32对接巴法云实现配网
  • IntelliJ IDEA 中进行背景设置
  • Python使用
  • 【工作笔记】 WSL开启报错
  • 参数化建模(三):SOLIDWORKS中的参数化应用实例
  • docker部署自动化测试环境笔记
  • (21)量子计算对密码学的影响
  • Redis持久化机制
  • 力扣HOT100之动态规划:322. 零钱兑换
  • 【大模型】情绪对话模型项目研发
  • 区域未停留检测算法AI智能分析网关V4打造铁道/工厂/机场等场景应用方案
  • 2025 年 Solana 生态全景分析:它如何从以太坊「高速替代方案」成长为成熟的基础设施?
  • 换ip是换网络的意思吗?怎么换ip地址
  • write和read命令中的通道号指南
  • 使用Vditor将Markdown文档渲染成网页(Vite+JS+Vditor)
  • LangChain第二页_【教程】翻译完了
  • 将Kotti从Pyramid1.0升级到2.0 (失败的记录)
  • 网络空间安全培训机构/关键词优化师
  • 动漫网站设计毕业论文/厨师培训学校
  • 万州那家做网站/免费自建网站有哪些
  • 个人创业做网站/谷歌网站优化
  • 杭州定制网站公司/哪家公司网站做得好
  • 毕业设计做b2c网站的意义/象山seo外包服务优化