一、概述
1.1背景
在一个分布式系统中,每个服务都可能会调用其它的服务器,服务之间是相互调用相互依赖。假如微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务。这就是构成所谓"扇出"。
如果扇出的链路上某个微服务的调用响应的时间过长或者不可用,对微服A的调用就会占用越来越多的系统资源,进而引起系统崩溃,即"雪崩效应"。
对于高流量的应用来说,单一的后端依赖可能会导致所有的服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
为应对微服务器系统出现"雪崩"事故,使用微服务Hystrix能避免级联故障,以提高分布式系统的弹性。
1.2断路器概述
- 有问题的节点,快速熔断(快速返回失败处理或者返回默认兜底数据【服务降级】)。
"断路器"本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
功能:
- 服务熔断
- 类比保险丝,保险丝闭合状态(CLOSE)可以正常使用,当达到最大服务访问后,直接拒绝访问跳闸限电(OPEN),此刻调用方会接受服务降级的处理并返回友好兜底提示
- 就是家里保险丝,从闭合CLOSE供电状态跳闸OPEN打开状态
- 服务降级
- 服务器忙,请稍后再试。不让客户端等待并立刻返回一个友好提示,fallback
- 服务限流
- 秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
- 服务限时
- 服务预热
- 接近实时的监控
- 兜底的处理动作
请求限流
服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。
请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。
线程隔离
当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。
为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其"隔离"起来。
服务熔断
线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。
所以,我们要做两件事情:
-
编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
-
异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
1.3Hystrix(过时了解)
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
1.4Spring Cloud断路器
Spring Cloud Circuit Breaker提供了跨不同断路器实现的抽象。它提供了在应用程序中使用的一致API,允许开发人员选择最适合您的应用程序需求的断路器实现。
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
Circuit Breaker!只是一套规范和接口,落地实现者是Resilience4J
1.5Resilience4J
Resilience4j是一个专为函数式编程设计的轻量级容错库。Resilience4i提供高阶函数(装饰器),以通过断路器、速率、限制器、重试或隔板增强任何功能接口、lambda表达式或方法引用。您可以在任何函数式接口、lambda表达式或方法引用上堆叠多个装饰器。优点是您可以选择您需要的装饰器,而没有其他选择。
Resilience42需要Java17。
中文网站https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md
二、实战
2.1熔断
原理
配置属性
需求
# 6 次访问中当执行方法的失败率达到 50% 时 CircuitBreaker 将进入开启 OPEN 状态 ( 保险丝跳闸断电 ) 拒绝所有请求。
# 等待 5 秒后, CircuitBreaker 将自动从开启 OPEN 状态过渡到半开 HALF_OPEN 状态,允许一些请求通过以测试服务是否恢复正常。
# 如还是异常 CircuitBreaker 将重新进入开启 OPEN 状态;如正常将进入关闭 CLOSE 闭合状态恢复正常处理请求。
实现 计数的滑动窗口
客户端控制类
java
package com.yanyu.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 // 声明这是一个RESTful风格的控制器
public class PayCircuitController {
// 使用Resilience4j CircuitBreaker的例子
@GetMapping(value = "/pay/circuit/{id}") // 定义一个GET请求映射,路径为/pay/circuit/{id}
public String myCircuit(@PathVariable("id") Integer id) { // 定义一个方法,接收一个名为id的路径参数
if (id == -4) throw new RuntimeException("----circuit id 不能负数"); // 如果id为-4,抛出异常
if (id == 9999) { // 如果id为9999
try {
TimeUnit.SECONDS.sleep(5); // 让线程休眠5秒
} catch (InterruptedException e) {
e.printStackTrace(); // 打印异常堆栈信息
}
}
return "Hello, circuit! inputId: " + id + " \t " + IdUtil.simpleUUID(); // 返回一个字符串,包含输入的id和一个随机生成的UUID
}
}
配置
java
<!--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配置
bash
# 开启circuitbreaker和分组激活 spring.cloud.openfeign.circuitbreaker.enabled
circuitbreaker:
enabled: true
group:
enabled: true #没开分组永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后
bash
# 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
服务端控制类
java
@RestController // 声明这是一个RESTful风格的控制器
public class OrderCircuitController {
@Resource // 注入PayFeignApi接口的实现类
private PayFeignApi payFeignApi;
@GetMapping(value = "/feign/pay/circuit/{id}") // 定义一个GET请求映射,路径为/feign/pay/circuit/{id}
@CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback") // 使用CircuitBreaker注解,指定熔断器名称和降级方法
public String myCircuitBreaker(@PathVariable("id") Integer id) { // 定义一个方法,接收一个名为id的路径参数
return payFeignApi.myCircuit(id); // 调用PayFeignApi接口的myCircuit方法,并返回结果
}
// myCircuitFallback是服务降级后的兜底处理方法
public String myCircuitFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "myCircuitFallback,系统繁忙,请稍后再试-----/(ㄒoㄒ)/~~";
}
}
@CircuitBreaker注解
@CircuitBreaker
是Spring Cloud Circuit Breaker库中的一个注解,用于为方法提供熔断器功能。当方法调用失败或超时时,熔断器会触发并执行备用逻辑,以避免对整个系统造成影响。使用
@CircuitBreaker
注解时,需要指定一个名称(name)和一个备用方法(fallbackMethod)。名称用于标识熔断器,备用方法用于在熔断器触发时执行的代码。
50%错误后触发熔断并给出服务降级,告知调用者服务不可用此时就算是输入正确的访问地址也无法调用服务(我明明是正确的也不让用(ToTY~),.它还在断路中(OPEN状态),一会儿过度到半开并继续正确地址访问,慢慢切换回CLO$E状态,可以正常访问了链路回复
实现 时间的滑动窗口
bash
# 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
2.2隔离
依赖隔离&负载保护:用来限制对于下游服务的最大并发数量的限制
Resilience4j提供了如下两种隔离的实现方式
SemaphoreBulkhead(信号量舱壁)
原理
配置
bash
<!--resilience4j-bulkhead-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
yml配置
bash
# resilience4j bulkhead 的例子
resilience4j:
# 配置bulkhead隔离策略
bulkhead:
configs:
# 默认配置
default:
maxConcurrentCalls: 2 # 隔离允许并发线程执行的最大数量
maxWaitDuration: 1s # 当达到并发调用数量时,新的线程的阻塞时间,我只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
# 实例名称为cloud-payment-service的配置
cloud-payment-service:
baseConfig: default # 使用默认配置
# 配置timelimiter超时策略
timelimiter:
configs:
# 默认配置
default:
timeout-duration: 20s # 设置超时时间为20秒
java
@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
是Spring Cloud Circuit Breaker库中的一个注解,用于为方法提供隔离功能。当方法调用失败或超时时,隔离器会阻止更多的请求进入该方法,以避免对整个系统造成影响。使用
@Bulkhead
注解时,需要指定一个名称(name)和一个最大并发数(maxConcurrentCalls)。名称用于标识隔离器,最大并发数用于限制允许同时执行的请求数量。
实现FixedThreadPoolBulkhead(固定线程池舱壁)
配置yml
bash
####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 新启线程和原来主线程脱离
java
/**
* (船的)舱壁,隔离,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ㄒ)/~~");
}
2.3限流
限流 就是限制最大访问流量。系统能提供的最大并发是有限的,同时来的请求又太多,就需要限流。
比如商城秒杀业务,瞬时大量请求涌入,服务器忙不过就只好排队限流了,和去景点排队买票和去医院办理业务排队等号道理相同。
常见限流算法
漏斗算法
这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法
SpringCloud默认使用该算法
滚动时间窗
缺点:间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮
滑动时间窗口
实现
配置
bash
<!--resilience4j-ratelimiter-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
yml配置
bash
####resilience4j ratelimiter 限流的例子
resilience4j:
ratelimiter:
configs:
default:
limitForPeriod: 2 #在一次刷新周期内,允许执行的最大请求数
limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod
timeout-duration: 1 # 线程等待权限的默认等待时间
instances:
cloud-payment-service:
baseConfig: default
控制类
bash
@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback")
public String myBulkhead1(@PathVariable("id") Integer id)
{
return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id,Throwable t)
{
return "你被限流了,禁止访问/(ㄒoㄒ)/~~";
}
@RateLimiter
@RateLimiter
是Spring Cloud Circuit Breaker库中的一个注解,用于为方法提供限流功能。当方法调用失败或超时时,限流器会限制请求的速率,以避免对整个系统造成影响。使用
@RateLimiter
注解时,需要指定一个名称(name)和一个最大速率(maxRate)。名称用于标识限流器,最大速率用于限制允许每秒执行的最大请求数量。