前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
往期目录回顾:
接上期内容:上期完成服务调用组件的学习,接下来进入新的组件学习,话不多说,直接发车!
一、学习Circuit Breaker前言
随着业务持续拓展,微服务数量呈现出不断攀升的态势,其间的调用关系与相互依赖亦变得愈发错综复杂。在复杂的分布式体系架构之下,应用程序往往存在数十个依赖项,而鉴于网络环境的不稳定性与业务逻辑的多样性,每个依赖在特定时刻均难以完全规避故障的发生。当涉及多个微服务之间的交互时,以微服务 A 为例,它可能会同时调用微服务 B 与微服务 C,而微服务 B 和微服务 C 又会分别对其他众多微服务发起调用请求,此情形即所谓的 “扇出” 现象。在这样的调用链路中,倘若扇出链路上的某个微服务因代码性能瓶颈、资源竞争激烈或网络阻塞等缘由,致使其调用响应时间大幅延长乃至服务完全不可用,那么微服务 A 针对该微服务的调用请求将会持续处于等待或重试状态,期间会不断占用诸如线程、内存等系统资源,且随着时间推移与请求数量的累积,资源占用量将呈指数级增长,最终导致系统资源被耗尽,引发整个系统的崩溃瘫痪,这就是所谓的 “雪崩效应”。
那么如何预防上述情况,避免整个系统大面积故障。主要从以下几个方面预防:
- 服务熔断:服务熔断就像是电路中的保险丝,当发现被依赖的服务出现问题达到一定的阈值时,“熔断” 对该服务的调用,直接返回一个预设的结果(如错误信息),避免系统资源的浪费,保护系统的整体稳定性。
- 服务降级:当系统的资源(如 CPU、内存、带宽等)紧张或者被依赖的服务出现问题时,为了保证核心业务的正常运行,对一些非核心功能进行降低服务质量的处理
- 服务限流:服务限流是对进入系统的请求流量进行限制的一种措施。
而以上功能正是Spring Cloud Circuit Breaker组件所能干的事。
二、Circuit Breaker是什么
"Circuit Breaker" 直译为 “断路器”,在软件系统尤其是分布式系统领域,它是一种用于处理故障和防止系统故障蔓延的设计模式。官网地址:Spring Cloud Circuit Breaker
三、Circuit Breaker能干嘛
Spring Cloud Circuit Breaker只是一套规范和接口,最终落地实现的是 Resilience4J,其核心功能就是以下几个模块。
更多详细请参考官方文档:https://github.com/resilience4j/resilience4j#3-overview
四、Circuit Breaker案例实操
一、服务熔断和降级
1、Circuit Breaker拥有的状态
断路器有三个普通状态:关闭(CLOSED)、开启(OPEN)、半开(HALF OPEN),还有两个特殊状态:禁用(DISABLED)、强制开启(FORCED OPEN)。
2、Circuit Breaker熔断规则
当熔断器关闭时,所有的请求都会通过熔断器:
- 如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。
- 当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并且重新计算失败率。
- 如果失败率超过阈值,则变为打开状态,拒绝所有请求;如果失败率低于阈值,则变为关闭状态,所有请求正常通过。
3、Circuit Breaker熔断计算规则
-
基于访问数量的滑动窗口:当访问错误次数达到一定数量,开启熔断
-
基于时间的滑动窗口:当请求时长超过设定次数(可以理解为统计慢查询数量),开启熔断。
4、Circuit Breaker熔断降级实操(一)
基于COUNT_BASED(访问数量的滑动窗口)案例实操:
4.1、修改cloud-provider-payment8001
新增TestPayCountBasedCircuitController
@RestController
@RequestMapping("/pay/test")
public class TestPayCountBasedCircuitController {
/**
* =============== Resilience4j CircuitBreaker 的例子
* 基于COUNT_BASED(访问数量的滑动窗口)案例
*
* @param id 测试id
*/
@GetMapping(value = "/circuit/{id}")
public ResultResponse<String> testCircuitBreaker(@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 ResultResponse.success("Hello, circuit! timestamp:" + System.currentTimeMillis());
}
}
4.2、修改cloud-api-commons
修改PayFeignApi接口,新增测试Circuit Breaker功能接口
/**
* =============== Resilience4j CircuitBreaker 的例子
* 基于COUNT_BASED(访问数量的滑动窗口)案例
*
* @param id 测试id
*/
@GetMapping(value = "/pay/test/circuit/{id}")
ResultResponse<String> testCircuitBreaker(@PathVariable("id") Integer id);
4.3、修改cloud-consume-feign-order8002
修改pom.xml,导入相关依赖(记得刷新maven)
<!--resilience4j-circuit breaker-->
<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.4、修改cloud-consume-feign-order8002
新增TestOrderCircuitBreakerController
@RestController
@RequestMapping("/feign/consume")
public class TestOrderCircuitBreakerController {
@Resource
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/test/circuit/{id}")
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "testCircuitBreakerFallback")
public ResultResponse<String> testCircuitBreaker(@PathVariable("id") Integer id) {
return payFeignApi.testCircuitBreaker(id);
}
/**
* TestCircuitBreakerFallback 是服务降级后的兜底处理方法
* <p>
* 调用 fallbackMethod 源码逻辑:
* 在 resilience4j 的内部实现中,当决定要调用 fallbackMethod 时,
* 它会通过反射机制查找对应的 fallback 方法(依据 @CircuitBreaker
* 注解中指定的 fallbackMethod 名称)。找到方法后,会根据参数要求进行参数传递(在 resilience4j 里,
* 默认传递导致主方法失败的 Throwable 类型异常对象作为参数给 fallback 方法),
* 然后执行 fallback 方法并获取返回值,最终将这个返回值返回给调用方,替代原本应该由主方法返回的值,实现了故障时的回退逻辑。
*
* @param id 业务id
*/
public ResultResponse<String> testCircuitBreakerFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return ResultResponse.fail(ReturnCodeEnum.RC500.getCode(),
"错误id:" + id + " TestCircuitBreakerFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~");
}
}
4.5、修改cloud-consume-feign-order8002
修改application.yml文件,新增Circuit Breaker相关配置
####################### circuitbreaker 断路器配置 ######################################
# Resilience4j CircuitBreaker 按照次数:COUNT_BASED 的例子
# 6次访问中当执行方法的失败率达到50%时CircuitBreaker将进入开启OPEN状态(保险丝跳闸断电)拒绝所有请求。
# 等待5秒后,CircuitBreaker 将自动从开启OPEN状态过渡到半开HALF_OPEN状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常CircuitBreaker 将重新进入开启OPEN状态;如正常将进入关闭CLOSE闭合状态恢复正常处理请求。
spring:
cloud:
openfeign:
# 开启circuitbreaker和分组激活
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
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
# 是否启用自动从开启状态过渡到半开状态,默认值为true。如果启用,CircuitBreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
automaticTransitionFromOpenToHalfOpenEnabled: true
#从OPEN到HALF_OPEN状态需要等待的时间
waitDurationInOpenState: 60s
#半开状态允许的最大请求数,默认值为10。在半开状态下,CircuitBreaker将允许最多permittedNumberOfCallsInHalfOpenState个请求通过,如果其中有任何一个请求失败,CircuitBreaker将重新进入开启状态。
permittedNumberOfCallsInHalfOpenState: 2
# 只要是这个异常类及其子类都将开启熔断器
record-exceptions:
- java.lang.Exception
# 指定某个服务使用default配置
instances:
cloud-payment-service:
baseConfig: default
4.6、测试Circuit Breaker功能
启动8001、8002服务,测试Circuit Breaker功能:
- 访问1次正确的
- 访问1次错误的
- 连续访问6次错误,在访问1次正确的,结果正确的拿不到值,说明服务开启熔断降级
- 等待60s,在访问1次正确的
由上述可见,当服务请求错误次数达到配置阈值,Circuit Breaker将开启熔断降级服务,等待60s后,服务关闭熔断,如此反复工作。
5、Circuit Breaker熔断降级实操(二)
基于TIME_BASED (基于时间的滑动窗口)案例实操:
5.1、修改cloud-consume-feign-order8002
修改application.yml文件,注释掉COUNT_BASED的配置,记得注释掉OpenFeign的重试机制,改回默认不重试机制。
resilience4j:
timelimiter:
configs:
default:
#神坑的位置,timelimiter 默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
timeout-duration: 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: 60s
recordExceptions:
- java.lang.Exception
# 指定某个服务使用default配置
instances:
cloud-payment-service:
baseConfig: default
5.2、测试Circuit Breaker功能
重启8002服务,测试功能。注意事项:1、要用两个不同浏览器来做测试,如果用一个浏览器,多个窗口来测试,测不出结果(踩坑),具体原因没搞清楚,估计是浏览器的缓存问题。2、一定要修改OpenFeign的连接读取时间(改成20s),测试完成后改回去,不然也测不出结果。
测试步骤:
- 访问一次正确的。
- 访问时间长点的(时间有点长,但是能拿到结果)
- 一个浏览器多开几个访问时间长的,另外一个浏览器访问一个正确的,结果正确的也不行了,说明服务已经开启了熔断降级处理。
- 等待60s,访问一个正确的,服务回复正常。
由此可见,当多个请求花费时长超过配置的阈值,服务开启熔断降级处理,避免堆积请求,导致服务雪崩,发生级联事故。此熔断规则,用来解决慢查询sql是一个很好的方案。
二、服务隔离(Bulkhead)
1、什么是服务隔离
Bulkhead(直译过来是隔板的意思),隔板来自造船业,船的内部一般会分成很多小隔舱,隔板的作用就在于一旦一个隔舱漏水因为隔板的存在而不至于影响其他隔舱和整体船。而resilience4j-bulkhead模块就是用了这一原理,从而实现服务隔离。
服务隔离是一种用于处理分布式系统中服务间相互影响的机制。它主要是通过对共享资源(如线程池,数据库连接池等)的访问,来确保一个服务出现问题(如高并发下的性能问题或故障)时,不会过度占用资源而导致其他服务无法正常运行。
英文官方文档:Bulkhead
2、服务隔离是用来干嘛
- 限制并发数:当请求数达到最大线程阈值时,阻塞请求或者拒绝请求。
- 防止事故传播:假设一个电商系统包含了商品服务、订单服务、支付服务等。如果支付服务出现故障,在没有服务隔离的情况下,可能会导致订单服务也出现问题。通过Resilience4j的服务隔离,订单服务和支付服务可以在资源使用和故障影响上相互隔离。当支付服务故障时,订单服务仍任可以处理不涉及支付服务的操作,比如订单查询,订单详情查看等。
3、服务隔离实操
resilience4j-bulkhead提供了两种服务隔离方式:
3.1、SemaphoreBulkhead隔离
SemaphoreBulkhead原理:当信号量有空闲时,进入系统的请求会直接获取信号量开始处理业务;当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,如果阻塞状态的请求在阻塞计时内无法取得信号量则系统会拒绝这些请求,反之则处理业务。
基于SemaphoreBulkhead隔离方式的实操:
- 修改cloud-provider-payment8001包,新增TestPaySemaphoreBulkheadController
@RestController @RequestMapping("/pay") public class TestPayBulkheadController { /** * SemaphoreBulkhead(信号量) 的例子 */ @GetMapping(value = "/test/semaphoreBulkhead/{id}") public ResultResponse<String> testSemaphoreBulkhead(@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 ResultResponse. success("Hello, bulkhead! 请求id: " + id + "请求时间戳:" + System.currentTimeMillis()); } }
- 修改cloud-api-commons包,新增测试接口
/** * =============== Resilience4j bulkhead 的例子 * SemaphoreBulkhead(信号量) */ @GetMapping(value = "/pay/test/semaphoreBulkhead/{id}") ResultResponse<String> testSemaphoreBulkhead(@PathVariable("id") Integer id);
- 修改cloud-consume-feign-order8002包中的pom.xml文件,导入依赖(记得刷新Maven)
<!--resilience4j-bulkhead--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-bulkhead</artifactId> </dependency>
- 修改cloud-consume-feign-order8002的application.yml文件,加入SemaphoreBulkhead相关配置
####################### resilience4j bulkhead 信号舱壁隔离配置 ###################################### bulkhead: configs: default: #隔离允许并发线程执行的最大数量 max-concurrent-calls: 2 #当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback max-wait-duration: 1s instances: cloud-payment-service: base-config: default
- 修改cloud-consume-feign-order8002包,新增TestOrderBulkheadController
@RestController @RequestMapping("/feign/consume") public class TestOrderBulkheadController { @Resource private PayFeignApi payFeignApi; /** * 舱壁隔离(信号舱) */ @GetMapping(value = "/pay/test/semaphoreBulkhead/{id}") @Bulkhead(name = "cloud-payment-service", fallbackMethod = "testSemaphoreBulkheadFallback", type = Bulkhead.Type.SEMAPHORE) public ResultResponse<String> bulkheadTest(@PathVariable("id") Integer id) { return payFeignApi.testSemaphoreBulkhead(id); } //testSemaphoreBulkheadFallback就是服务隔离降级的兜底处理方法 public ResultResponse<String> testSemaphoreBulkheadFallback(Integer id, Throwable e) { // 这里是容错处理逻辑,返回备用结果 return ResultResponse.fail(ReturnCodeEnum.RC500.getCode(), "SemaphoreBulkhead,隔板超出最大数量限制,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~ ,请求时间戳" + System.currentTimeMillis()); } }
- 重启8001、8002服务,测试服务隔离效果。
浏览器打开两个新窗口,各点击一次,每个请求耗时5秒,2个线程达到后台配置的并发阈值,此时第三个请求访问,直接被隔离,做降级处理。等其中一个窗口请求结束,并发数小于阈值,第三个请求正常访问。
3.2、FixedThreadPoolBulkhead隔离
FixedThreadPoolBulkhead的原理:FixedThreadPoolBulkhead使用一个线程池和一个等待队列来实现服务隔离。当线程池中存在空闲时,此时进入系统的请求将获取空闲线程或开启新线程来处理请求。当线程池中无空闲线程时,此刻的请求将会进入等待队列。若等待队列没有剩余空间,那么这些请求将会被拒绝,在队列中的线程等待线程池中出现空闲线程时,开始处理业务。
基于FixedThreadPoolBulkhead隔离方式的实操:
- 修改cloud-provider-payment8001包,新增FixedThreadPoolBulkhead测试接口
/** * FixedThreadPoolBulkhead(有界队列和固定大小线程池)的例子 */ @GetMapping(value = "/test/bulkheadPool/{id}") public ResultResponse<String> testBulkheadPool(@PathVariable("id") Integer id) { if (id == 1 || id == 2 || id == 3) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } return ResultResponse. success("Hello, FixedThreadPoolBulkhead! 请求id: " + id + "请求时间戳:" + System.currentTimeMillis()); }
- 修改cloud-api-commons,新增FixedThreadPoolBulkhead测试接口
/** * =============== Resilience4j bulkhead 的例子 * FixedThreadPoolBulkhead(有界队列和固定大小线程池) */ @GetMapping(value = "/pay/test/bulkheadPool/{id}") ResultResponse<String> testBulkheadPool(@PathVariable("id") Integer id);
- 修改cloud-consume-feign-order8002包,新增FixedThreadPoolBulkhead测试接口
/** * 舱壁隔离 (有界队列和固定大小线程池) */ @GetMapping(value = "/pay/test/bulkheadPool/{id}") @Bulkhead(name = "cloud-payment-service", fallbackMethod = "testBulkheadPoolFallback", type = Bulkhead.Type.THREADPOOL) public CompletableFuture<String> testBulkheadPool(@PathVariable("id") Integer id) { return CompletableFuture. supplyAsync( () -> payFeignApi.testBulkheadPool(id).getData() + " FixedThreadPoolBulkhead(有界队列和固定大小线程池)"); } //服务隔离降级的兜底处理方法 public CompletableFuture<String> testBulkheadPoolFallback(Integer id, Throwable t) { return CompletableFuture .supplyAsync( () -> "FixedThreadPoolBulkhead,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~,请求时间戳:" + System.currentTimeMillis()); }
- 修改cloud-consume-feign-order8002包中application.yml文件,新增FixedThreadPoolBulkhead配置。
####################### resilience4j bulkhead 连接池隔离配置 ###################################### thread-pool-bulkhead: configs: default: #实际能处理的请求数:core-thread-pool-size + queue-capacity = 2,所以当第三个请求来的时候,直接降级处理 # 核心线程数1 core-thread-pool-size: 1 # 最大线程数1 max-thread-pool-size: 1 # 等待队列数 1 queue-capacity: 1 instances: cloud-payment-service: base-config: default
注意事项:# spring.cloud.openfeign.circuitbreaker.group.enabled 请设置为false 新启线程和原来主线程脱离
- 重启8001、8002服务,测试FixedThreadPoolBulkhead服务隔离
当线程数和等待队列超过配置的阈值时,后续的所有请求都会被拒绝,从而达到服务隔离
三、服务限流
1、服务限流是什么
服务限流是一种用于控制进入系统的请求流量的技术手段。在分布式系统、微服务架构等复杂的软件系统环境中,当系统面临的请求流量超过其处理能力时,通过限制请求的数量来保护系统的稳定性、可用性和性能。简单来说,就是对进入系统的流量进行“阀门”式的控制,避免过多的请求压垮系统。
2、常见的服务限流算法
比如漏洞算法、令牌桶算法(resilience4j使用)、滚动时间窗口算法、滑动时间窗口算法等,这里不展开,有兴趣自行学习了解。
3、服务限流实操
- 修改cloud-provider-payment8001包,新增限流测试接口
@RestController @RequestMapping("/pay") public class TestPayRateLimitController { /** * Resilience4j rateLimit 的例子 */ @GetMapping(value = "/test/rateLimit/{id}") public ResultResponse<String> testRateLimit(@PathVariable("id") Integer id) { return ResultResponse.success("Hello, 欢迎到来RateLimit,请求时间戳:" + System.currentTimeMillis()); } }
- 修改cloud-api-commons包,新增限流测试接口
/** * Resilience4j rateLimit 的例子 */ @GetMapping(value = "/pay/test/rateLimit/{id}") ResultResponse<String> testRateLimit(@PathVariable("id") Integer id);
- 修改cloud-consume-feign-order8002的pom.xml文件,导入相关依赖(记得刷新maven)
<!--resilience4j-ratelimiter--> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> </dependency>
- 修改cloud-consume-feign-order8002,新增测试限流接口
@RestController @RequestMapping("/feign/consume") public class TestOrderRateLimitController { @Resource private PayFeignApi payFeignApi; @GetMapping(value = "/test/rateLimit/{id}") @RateLimiter(name = "cloud-payment-service", fallbackMethod = "testRateLimitFallback") public ResultResponse<String> testRateLimit(@PathVariable("id") Integer id) { return payFeignApi.testRateLimit(id); } // 服务降级处理方法 public ResultResponse<String> testRateLimitFallback(Integer id, Throwable t) { return ResultResponse.success("你被限流了,禁止访问/(ㄒoㄒ)/~~,请求时间戳:" + System.currentTimeMillis()); } }
- 修改cloud-consume-feign-order8002中的application.yml文件,新增限流配置
参数说明:####################### resilience4j ratelimiter 限流配置 ###################################### ratelimiter: configs: default: #在一次刷新周期内,允许执行的最大请求数 limitForPeriod: 2 # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod limitRefreshPeriod: 1s # 线程等待权限的默认等待时间 timeout-duration: 1 instances: cloud-payment-service: base-config: default
- 重启8001、8002,测试服务限流功能。正常访问:
快速(高频点击)点击访问:
当在一个刷新周期内,请求数达到配置的阈值,服务将进行限流降级处理。
五、总结
本章节学习了Resilience4J的几个核心模块(Circuit breaker、Bulkhead、RateLimit),这些技术都是保障分布式系统、微服务架构高可用性、稳定性和高性能的关键策略。
这些技术相互关联、相辅相成,共同构成了分布式系统和微服务架构中保障系统可靠性和性能的重要防线。在实际的系统设计与开发过程中,需要充分理解这些技术的原理、特点和适用场景,根据具体业务需求和系统架构进行合理选型与灵活应用,并结合有效的监控与运维手段,持续优化系统性能,确保系统在复杂多变的环境下稳定高效运行。
ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。