赞
踩
目录
当微服务数量越来越多之后,保证微服务自身的高可用性就变得异常重要了,高可用也是服务治理中的重要一环,在保障高可用解决方案中,服务的熔断、限流与降级可以说是其中非常重要的一种手段。
在Java的微服务生态中,对于服务保护组件,像springcloud的Hystrix,springcloud alibaba的Sentinel,以及当Hystrix停更之后官方推荐使用的Resilience4j。
很多同学在学习微服务限流组件时,容易对熔断、限流、降级这几个概念混淆,甚至在什么场景下使用哪种保护措施也缺乏深一定的认识,这一点有必要搞清楚。
关于熔断、限流、降级这几个关键词,做一下详细的解释。
熔断,即Circuit Breaker,熔断是一种故障处理机制,用于防止故障在系统中蔓延。
当一个服务出现故障时,熔断器会立即中断对该服务的请求,防止服务的故障传播到其他的组件。通过熔断机制,系统可以快速发现问题并做出相应的处理,避免系统整体崩溃。
Rate Limiting,限流是一种控制请求流量的机制,用于保护系统免受过载的影响。
通过限制请求的频率或数量,可以确保系统在处理请求时不会超出其承受范围,限流可有效地平滑系统的负载,并防止突发流量对系统的影响。
Degradation,降级是一种在系统负载过高或出现故障时,临时关闭部分功能或服务,以保证系统整体业务的可用性和稳定性机制。
通过降级,可以确保系统在面临异常情况时依然能够正常运行,而不是完全崩溃。降级可以根据不同情况选择关闭不同的服务或功能,以最大程度地保障系统的核心功能。
在这几个概念对应的微服务治理中,服务熔断是基础,同时也是保护系统受到意外冲击最基本也最有效的措施。
在微服务开发中,当微服务数量越来越多的时候,服务之间的调用链路也越来越复杂,当某个服务调用另一个服务时,如果服务提供者出现网络超时或其他故障,过多的请求这时候都打来时,不仅会造成服务提供者的资源被占用过多出现阻塞,同时服务调用者也会出现堆积最终造成无法正常提供服务。
在这种情况下,就需要一种保护机制,通过实现调用熔断,以达到保护服务调用者的目的。如下图,A服务调用B服务,B服务调用C服务,当C服务出现故障或超时时,大量A服务调用请求导致B服务大量超时,最终导致B服务出现故障,这种级联的故障蔓延,最终会导致起始处的服务被冲垮。
断路器,也叫熔断器,英文:CircuitBreaker,CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。
具体来说,当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能继续正常运行。同时,CircuitBreaker还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。
CircuitBreaker的核心原理如下图:
我们可以用状态机来实现 Circuit Breaker,它有以下三种状态:
关闭(Closed)状态;
默认情况下Circuit Breaker是关闭的,此时允许操作执行;
Circuit Breaker内部记录着最近失败的次数,如果对应的操作执行失败,次数就会续一次;
如果在某个时间段内,失败次数(或者失败比率)达到阈值,Circuit Breaker会转换到开启(Open)状态;
在开启状态中,CircuitBreaker会启用一个超时计时器,设这个计时器的目的是给集群相应的时间来恢复故障。当计时器时间到的时候,Circuit Breaker会转换到半开启(Half-Open)状态。
开启(Open);
在此状态下,执行对应的操作将会立即失败并且立即抛出异常。
半开启(Half-Open);
在此状态下,Circuit Breaker 会允许执行一定数量的操作,如果所有操作全部成功,Circuit Breaker就会假定故障已经恢复,它就会转换到关闭状态,并且重置失败次数;
如果其中 任意一次 操作失败了,Circuit Breaker就会认为故障仍然存在,所以它会转换到开启状态并再次开启计时器(再给系统一些时间使其从失败中恢复)。
在实际执行过程中,几个状态的切换流程如下图:
上面了详细解了CircuitBreaker的原理,可以认为CircuitBreaker定义了一套业务实现规则或规范,而具体的实现组件,则是Resilience4j,即CircuitBreaker只是一套规范接口,落地实现者是Resilience4j。
Resilience4j是一款轻量级,易于使用的容错库,其灵感来自于Netflix Hystrix,但是专为Java 8和函数式编程而设计。轻量级,因为库只使用了Vavr,它没有任何其他外部依赖下。相比之下,Netflix Hystrix对Archaius具有编译依赖性,Archaius具有更多的外部库依赖性。
Resilience4j是一个轻量级、易于使用的容错库,其灵感来自Netflix Hystrix,但专为Java 8和函数式编程设计。
Resilience4j提供高阶函数(decorators)来增强任何功能接口、lambda表达式或方法引用,包括断路器、速率限制器、重试或舱壁。可以在任何函数接口、lambda表达式或方法引用上使用多个装饰器。
circuitbreaker组件实现了断路器功能,是基于内存的断路器,采用ConcurrentHashMap来实现。
断路器(Circuit Breaker):在服务出现故障时自动熔断,防止请求继续失败导致雪崩效应。
限流(Rate Limiter):限制请求的并发数或速率,防止系统被过载。
重试(Retry):在请求失败时自动重试一定次数,增加系统的可靠性。
超时(Timeout):设置请求的最大执行时间,防止请求长时间阻塞。
Bulkhead:通过限制同时执行的请求数量,保护系统的部分资源不被耗尽。
使用Resilience4j可以通过简单的注解或者编程方式来实现以上功能,从而保护应用程序免受故障的影响,帮助应用程序在面对故障和不稳定性时保持稳定性和可靠性。
要使用Resilience4j,不需要引入所有依赖,只需要选择你需要的,Resilience4j提供了以下的核心模块和拓展模块:
组件名称 | 功能 |
---|---|
resilience4j-circuitbreaker | Circuit breaking(熔断器) |
resilience4j-ratelimiter | Rate limiting(限流器) |
resilience4j-bulkhead | Bulkheading(隔离器)--依赖隔离&负载保护 |
resilience4j-retry | Automatic retrying (sync and async)(重试、同步&异步) |
resilience4j-cache | Result caching(缓存) |
resilience4j-timelimiter | Timeout handling(超时处理) |
Bulkhead,即并发控制器(舱壁,Bulkhead),是用来控制并行(parallel)调用的次数。Resilience4j提供了两种舱壁模式的实现,可用于限制并发执行的次数:
SemaphoreBulkhead(信号量舱壁,默认),基于Java并发库中的Semaphore实现;
FixedThreadPoolBulkhead(固定 线程池舱壁),它使用一个有界队列和一个固定线程池。
由于基于信号量的Bulkhead能很好地在多线程和I/O模型下工作,所以选择介绍基于信号量的Bulkhead的使用。
Resilience4j是CircuitBreaker的具体实现,因此具备CircuitBreaker涉及到熔断的所有状态,在Resilience4j中,共有6种状态,在实际工作中就是在这6种状态之间转换。
CLOSED: 关闭状态,代表正常情况下的状态,允许所有请求通过,能通过状态转换为OPEN;
HALF_OPEN: 半开状态,即允许一部分请求通过,能通过状态转换为CLOSED和OPEN;
OPEN: 熔断状态,即不允许请求通过,能通过状态转为为HALF_OPEN;
DISABLED: 禁用状态,即允许所有请求通过,出现失败率达到给定的阈值也不会熔断,不会发生状态转换。
METRICS_ONLY: 和DISABLED状态一样,也允许所有请求通过不会发生熔断,但是会记录失败率等信息,不会发生状态转换。
FORCED_OPEN: 与DISABLED状态正好相反,启用CircuitBreaker,但是不允许任何请求通过,不会发生状态转换。
下面再对closed、open和half_open 这三种状态的切换做下补充:
closed -> open : 关闭状态到熔断状态,当失败的调用率(比如超时、异常等)默认50%,达到一定的阈值服务转为open状态,在open状态下,所有的请求都被拦截;
open-> half_open: 当经过一定的时间后,CircubitBreaker中默认为60s服务调用者允许一定的请求到达服务提供者;
half_open -> open: 当half_open状态的调用失败率超过给定的阈值,转为open状态;
half_open -> closed: 失败率低于给定的阈值则默认转换为closed状态;
常用的服务熔断,服务降级框架主要有Spring Cloud Hystrix、Spring Cloud Alibaba Sentinel、resilience4j,下面通过几个维度综合对比一下各自的特点,便于加深对各种组件的认识。
对比项 | Sentinel | Hystrix | Resilience4j |
---|---|---|---|
开源 | Apache-2.0 license | Apache-2.0 license | Apache-2.0 license |
更新 | 更新频繁(latest:2.1.1, Aug 8th 2022) | 已停更 | 更新较慢(latest:1.7.1, Jun 25th 2021) |
特点 | 轻量级,核心库无多余依赖,性能损耗小 | --- | 基于Java8和函数式编程,轻量级容错库,无多余依赖 |
隔离策略 | 信号量隔离 | 信号量/线程池隔离 | 信号量隔离 |
熔断降级策略 | 异常比率/响应时间/异常数 | 异常比率 | 异常比率/响应时间 |
控制台 | 实时监控、机器发现、规则管理等能力。 | 提供监控查看 | 不提供 |
系统自适应 | 支持,结合应用的机器负载、CPU 使用率,整体平均响应时间、入口 QPS 和 并发线程数等维度进行限流操做 | 不支持 | 不支持 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS、调用关系的限流 | 有限的支持 | 支持简单的Rtate Limiter模式 |
系统自适应
结合应用的机器负载、CPU 使用率,整体平均响应时间、入口QPS和并发线程数等几个维度的监控指标从而决定是否调用进行限流操做
上面详细介绍了Resilience4j的理论,接下来通过案例来看下如何在代码中集成并使用Resilience4j。
Resilience4j与springboot整合提供了两个版本的依赖包,springboot2的版本和springboot3的版本,其中sptingboot2的版本java8就可以支持,本文选择sptingboot2的版本依赖。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-aop</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-actuator</artifactId>
- </dependency>
-
- <dependency>
- <groupId>io.github.resilience4j</groupId>
- <artifactId>resilience4j-all</artifactId>
- <version>1.7.0</version>
- </dependency>
-
- <dependency>
- <groupId>io.github.resilience4j</groupId>
- <artifactId>resilience4j-spring-boot2</artifactId>
- <version>1.7.0</version>
- </dependency>
基本上Resilience4j的核心功能都可以在配置文件中通过官方提供的各种配置去完成,参考下面的配置信息,结合里面的注释加深对配置项的理解。
- resilience4j.circuitbreaker:
- instances:
- backendA:
- registerHealthIndicator: true # 是否启用健康检查
- slidingWindowSize: 10 #用于计算失败率的滑动窗口大小为10,即最近10次调用失败的情况会被考虑进去
- permittedNumberOfCallsInHalfOpenState: 3 # 断路器半开时允许最大的请求次数
- slidingWindowType: TIME_BASED #配置用于在CircuitBreaker关闭时记录调用结果的滑动窗口类型。 滑动窗口可以是基于计数或基于时间的。
- minimumNumberOfCalls: 5 # 熔断器开始计算失败率之前,至少需要的调用次数为5次
- waitDurationInOpenState: 5s # 断路器打开后,尝试等待5秒进入半开状态
- failureRateThreshold: 20 # 当失败率达到20%时,断路器会打开,组织进一步的调用
- eventConsumerBufferSize: 10 #用于存储断路器相关事件的缓冲区大小为10,这些事件可用于被监控
-
- #重试策略相关的配置
- resilience4j.retry:
- instances:
- backendA:
- maxAttempts: 3 #最大重试次数
- waitDuration: 2s #每次重试的时候间隔的等待时间
- enableExponentialBackoff: true
- exponentialBackoffMultiplier: 2
- retryExceptions:
- - java.lang.Exception
-
- resilience4j.bulkhead:
- instances:
- backendA:
- maxConcurrentCalls: 10
-
- resilience4j.thread-pool-bulkhead:
- instances:
- backendC:
- maxThreadPoolSize: 11 #配置最大线程池大小
- coreThreadPoolSize: 1 #配置核心线程池大小
- queueCapacity: 1 #配置队列的容量
-
- #限流的配置
- resilience4j.ratelimiter:
- instances:
- backendA: # 限流器的名字
- limitForPeriod: 1 # 一个限制周期内可访问次数
- limitRefreshPeriod: 1s # 限制周期,每个周期之后,速率限制器将重置回limitForPeriod值
- timeoutDuration: 10ms # 线程等待允许执行时间
- registerHealthIndicator: true
- eventConsumerBufferSize: 100
-
- server:
- port: 8081
自定义配置类,将配置文件中配置的每一种熔断器对应的项注册到spring的bean容器中,比如在配置文件中有一个实例名称为backendA;
- import io.github.resilience4j.common.circuitbreaker.configuration.CircuitBreakerConfigCustomizer;
- import io.github.resilience4j.common.ratelimiter.configuration.RateLimiterConfigCustomizer;
- import org.springframework.context.annotation.Bean;
-
- public class ResilienceConfig {
-
- @Bean
- public CircuitBreakerConfigCustomizer circuitBreakerConfigCustomizer() {
- return CircuitBreakerConfigCustomizer
- .of("backendA", builder -> builder.slidingWindowSize(10));
- }
-
- @Bean
- public RateLimiterConfigCustomizer rateLimiterConfigCustomizer() {
- return RateLimiterConfigCustomizer
- .of("backendA", builder -> builder.limitForPeriod(1));
- }
- }
在这里我们测试两个场景,第一个场景为重试,第二个场景为限流,对于下面的两个方法做如下的补充说明:
@CircuitBreaker,该注解里面有两个属性,name和fallbackMethod;
name,即配置文件中配置的那个实例名称;
fallbackMethod,出现错误时使用哪个降级方法;
@Retry,与@CircuitBreaker注解相同,也是两个相同的属性,具有类似的含义;
一般来说,@CircuitBreaker注解和@Retry可以搭配使用,也可以单独使用@Retry;
- import com.congge.entity.User;
- import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
- import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
- import io.github.resilience4j.retry.annotation.Retry;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.stereotype.Service;
- import org.springframework.web.client.RestTemplate;
-
- import java.util.Objects;
-
- @Service
- public class UserService {
-
- private Logger logger = LoggerFactory.getLogger(UserService.class);
-
- @CircuitBreaker(name = "backendA")
- @Retry(name = "backendA")
- // @RateLimiter(name = "backendA", fallbackMethod = "fallback")
- public User retry() {
- logger.info("backendA findUser");
- String res = new RestTemplate().getForObject("http://localhost:8088", String.class);
- if (Objects.nonNull(res)) {
- return new User("remote user", 18, "Man");
- }
- return new User("default user", 22, "Woman");
- }
-
- @CircuitBreaker(name = "backendA")
- @RateLimiter(name = "backendA",fallbackMethod = "fallback")
- public User limit() {
- return new User("remote user", 18, "Man");
- }
-
- public User fallback(Throwable e) {
- return new User("降级的用户", 18, "D");
- }
- }
控制器类用于测试使用
- @RestController
- public class UserController {
-
- @Resource
- private UserService userService;
-
- @GetMapping("/retry")
- public User retry() {
- return userService.retry();
- }
-
- @GetMapping("/limit")
- public User limit() {
- return userService.limit();
- }
- }
重试功能测试,在第一个方法中,远程调用一个不存在的链接,理论上会失败,按照参数配置,调用失败后,会重试3次,如果3次之后仍然失败,则接口抛出异常(或使用降级的方法的返回结果)。
重试接口测试
接口经过一段时间之后失败
同时查看控制台日志输出,不难发现,重试了3次,3次时候输出了异常的堆栈信息。
限流接口测试
调用限流接口,在配置文件中默认的是每秒允许通过一个请求,当我们正常请求接口时,可以正常得到结果。
快速刷接口时,由于方法中添加了降级方法,将会得到降级的响应结果。
通过上面的案例演示了使用Resilience4j进行接口的重试或限流操作,只需配置好核心的参数,并且在方法上添加相关的注解即可。
在实际开发中,对Resilience4j的使用,也是重点落在对配置文件中3个模块配置参数的调整,了解这些配置参数的具体含义才能结合实际场景合理使用,具体来说,主要是下面几部分:
circuitbreaker,断路器配置;
retry,重试配置;
ratelimiter,限流配置;
circuitbreaker的常用可配置参数项如下
配置参数 | 默认值 | 描述 |
---|---|---|
failureRateThreshold | 50 | 熔断器关闭状态和半开状态使用的同一个失败率阈值 |
ringBufferSizeInHalfOpenState | 10 | 熔断器半开状态的缓冲区大小,会限制线程的并发量,例如缓冲区为10则每次只会允许10个请求调用后端服务 |
ringBufferSizeInClosedState | 100 | 熔断器关闭状态的缓冲区大小,不会限制线程的并发量,在熔断器发生状态转换前所有请求都会调用后端服务 |
waitDurationInOpenState | 60(s) | 熔断器从打开状态转变为半开状态等待的时间 |
automaticTransitionFromOpenToHalfOpenEnabled | false | 如果置为true,当等待时间结束会自动由打开变为半开,若置为false,则需要一个请求进入来触发熔断器状态转换 |
recordExceptions | empty | 需要记录为失败的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |
recordFailure | throwable -> true | 自定义的谓词逻辑用于判断异常是否需要记录或者需要忽略,默认所有异常都进行记录 |
slowCallRateThreshold | 100 | 以百分比配置阈值。当呼叫持续时间大于等于或大于阈值时,CircuitBreaker 将呼叫视为慢速呼叫。 当慢速呼叫的百分比等于或大于阈值时,CircuitBreaker 转换为打开并开始短路呼叫 |
slowCallDurationThreshold | 60000(ms) | 配置持续时间阈值,超过该阈值呼叫被视为慢速并提高慢速呼叫率。 |
permittedNumberOfCallsInHalfOpenState | 10 | 配置 CircuitBreaker 半开时允许的调用次数。 |
maxWaitDurationInHalfOpenState | 100 | 配置在 CircuitBreaker 计算错误率或慢速调用率之前所需的最小调用次数(每个滑动窗口周期)。 |
waitDurationInOpenState | 100 | 断路器在从打开转换为半打开之前应等待的时间 |
slidingWindowSize | 100 | 如果滑动窗口是 COUNT_BASED,则记录并汇总最后一次调用。 如果滑动窗口是TIME_BASED,则记录并汇总最后几秒的调用。slidingWindowSize配置用于在 CircuitBreaker 关闭时记录调用结果的滑动窗口的大小。 |
slidingWindowType | COUNT_BASED | 配置用于在CircuitBreaker关闭时记录调用结果的滑动窗口类型。 滑动窗口可以是基于计数或基于时间的。 |
ratelimiter的常用可配置参数项如下
配置参数 | 默认值 | 描述 |
---|---|---|
timeoutDuration | 5s | 线程等待权限的默认等待时间 |
limitRefreshPeriod | 500ms | 权限刷新的时间,每个周期结束后,RateLimiter将会把权限计数设置为limitForPeriod的值 |
limiteForPeriod | 一个限制刷新期间的可用权限数 |
retry的常用可配置参数项如下
配置参数 | 默认值 | 描述 |
---|---|---|
maxAttempts | 3 | 最大重试次数 |
waitDuration | 500ms | 固定重试间隔 |
intervalFunction | numberOfAttempts -> waitDuration | 用来改变重试时间间隔,可以选择指数退避或者随机时间间隔 |
retryOnResultPredicate | result -> false | 自定义结果重试规则,需要重试的返回true |
retryOnExceptionPredicate | throwable -> true | 自定义异常重试规则,需要重试的返回true |
retryExceptions | empty | 需要重试的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |
熔断、限流与降级作为微服务治理中的重要一环,有必要对其引起重视和关注,而Resilience4j吸收了主流熔断器的优势,具备更为灵活的特点,适用于普通的springboot项目,也能很方便的集成到微服务框架中,值得深入的学习和研究。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。