文章目录
- 背景
- 简介
- Maven
- 重试器
- 注解式
- 编程式
- 配置
- 事件监听
- 指标监控
- 健康检查
- 速率限制
- 注解式
- 编程式
- 配置
- 事件监听
- 指标监控
- 动态修改配置
- 断路器
- 注解式
- 配置
- 舱壁
- 注解式
- 时间限制器
- 多组件配合使用
- 最佳实践
- 配置
- 参考:
背景
在应用程序开发的过程中,特别是在构建基于微服务的应用时,我们常常会在实时运行环境中遭遇各种偏差和挑战。有时候,这些偏差可能表现为响应的延迟、网络通信中断、REST调用的失败,甚至可能是因为突发的大量请求而导致的服务失效等等。为了有效地应对这些潜在的异常情况,我们必须在我们的应用程序中嵌入弹性和容错机制,以确保我们的系统能够在不确定的环境下保持稳定性和可靠性。通过采取这些措施,我们能够提高应用程序的鲁棒性,确保它在面对各种不可预测的情况时依然能够持续提供高质量的服务。
简介
官网:https://resilience4j.readme.io/
Github:https://github.com/resilience4j/resilience4j
Resilience4j是一个轻量级容错库,灵感来自 Netflix Hystrix,但专为函数式编程而设计。
Resilience4j提供装饰器,以通过断路器、速率限制器、重试或舱壁增强任何功能接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆叠多个装饰器。
以下是关于Resilience4j的一些要点:
- 断路器模式(Circuit Breaker):当操作连续失败时快速失败或执行默认操作,Resilience4j实现了断路器模式,它可以在系统出现故障时自动切换到开启状态,阻止请求继续发送到故障的组件。这有助于防止故障蔓延到整个系统,保持系统的可用性。resilience4j-circuitbreaker
- 重试器(Retry):自动重试失败的操作,库中内置了重试机制,允许在请求失败时进行重试。可以配置重试的次数、延迟和指数退避等策略,以期待后续的尝试可以成功。resilience4j-retry
- 限流器(Rate Limiter):限制一定时间内调用操作的次数,Resilience4j还提供了限流器功能,用于控制对某个操作或服务的请求速率。这有助于防止过多的请求同时涌入系统,导致系统过载。resilience4j-ratelimiter
- 时间限制器(Timeout):设置调用操作的时间限制,通过设置操作的最大执行时间,可以避免某个操作耗时过长而阻塞其他请求。Resilience4j可以在指定的时间内取消或中断执行超时的操作。resilience4j-timelimiter
- 舱壁(Bulkhead):限制并发操作的数量和保护请求隔离,该功能允许您限制并发请求的数量,确保资源在不同的任务之间得到合理的分配。这有助于防止一个失败的请求影响到其他请求。resilience4j-bulkhead
- 结果缓存(Cache): 存储昂贵的操作的结果。resilience4j-cache
- 事件监听与监控:Resilience4j允许您注册事件监听器,以便在断路器状态变化、重试等事件发生时进行通知或记录。这有助于监控系统的运行状况以及故障情况。
简单示例
// 待包装方法
String result = service.sayHelloWorld(param1);
// 包装方法
Supplier<String> supplier = () -> service.sayHelloWorld(param1);
String result = Decorators.ofSupplier(supplier) // 包装方法
.withBulkhead(Bulkhead.ofDefaults("name")) // 加装舱壁器
.withCircuitBreaker(CircuitBreaker.ofDefaults("name"))// 加装断路器
.withRetry(Retry.ofDefaults("name")) // 加装重试器
.withFallback(asList(CallNotPermittedException.class, BulkheadFullException.class), // 加装降级方法
throwable -> "Hello from fallback")
.get()
Resilience4j 2 需要 Java 17。
https://resilience4j.readme.io/v1.7.0/docs
Maven
首先,我们需要将目标模块添加到pom.xml中(例如,在这里我们添加断路器):
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.12.1</version>
</dependency>
Spring Boot2中添加如下依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
<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>
所有模块及其最新版本都可以在Maven Central上找到。
下面我们的部分示例会包含注解式和编程式。
重试器
自动重试失败的调用
设想微服务“A”在其运作过程中依赖于另一微服务“B”。假设微服务“B”的运行质量存在缺陷,其成功率仅为50-60%。然而,故障的根源可以多种多样,例如服务不可用、有缺陷的服务间歇性响应、网络问题等。在这种情形下,如果微服务“A”能够对失败的调用进行自动重试,将调用的成功机会提升至有益的程度。
实现目标
我们的应用程序在尝试获取某个API时,时常因内部问题遭遇调用失败。我们留意到在经过几轮尝试后,该API通常会恢复稳定,返回预期的响应。我们通过以下逻辑进行优化:
- 当出现HttpServerErrorException异常时,进行3次重试。
- 在每次重试后等待2秒。
- 若经过第三次重试仍然失败,返回缓存值以确保数据可用性。
注解式
示例代码如下:
@Service
@Slf4j
public class UserService {
// 制定策略 和 降级方法
@Retry(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
public String getUserInfoById(String id) {
log.info("开始请求远程服务");
// 模拟请求远端异常
if (id.equals("1")) {
throw new HttpServerErrorException(HttpStatus.BAD_GATEWAY);
}
log.info("结束请求远程服务");
return id;
}
// 降级方法
private String getUserInfoFallback(String id, Exception exception) {
log.error("远程服务异常,启用降级方法,id:{}", id, exception);
return "getUserInfoFallbackFromLocalCache";
}
}
测试日志如下:
2023-08-09 21:03:42.801 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:03:44.826 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:03:46.829 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:03:46.829 [nio-8080-exec-1] com.laker.service.UserService: 远程服务异常,启用降级方法,id:1
org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY
at com.laker.service.UserService.getUserInfoById(UserService.java:23) ~[classes/:na]
...
默认情况下
Resilience4J 将尝试调用带注释的方法 3 次,每次调用之间的等待时间为 500 毫秒。
如果没有成功调用,resilience4j 将调用后备方法并使用其返回值。
但要小心:您需要确保重试的操作是幂等的,否则您最终可能会得到损坏的数据。
配置
resilience4j:
retry:
instances:
getUserInfo:
# 最大重试次数值
max-attempts: 3
# 等待下一次尝试的长值
waitDuration: 2s
# 重试异常类列表
retry-exceptions:
- org.springframework.web.client.HttpServerErrorException
降级方法/回退(Fallback)方法/备选方法
针对所有模块中(重试,断路器等等)的降级方法
回退方法机制的工作原理类似于 try/catch 块。如果配置了回退方法,则每个执行都会转发到后备方法执行器。回退方法执行器正在搜索可以处理异常的最佳匹配回退方法。
即如果把上面示例中的异常改为模拟抛出RuntimeException
,虽然没有了重试了但是还是会走降级方法。
public String getUserInfoById(String id) {
...
if (id.equals("1")) {
// 只修改了这里
throw new RuntimeException("123");
}
...
return id;
}
测试日志如下:
2023-08-09 21:06:33.530 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:06:33.531 [nio-8080-exec-1] com.laker.service.UserService: 远程服务异常,启用降级方法,id:1
java.lang.RuntimeException: 123
at com.laker.service.UserService.getUserInfoById(UserService.java:21) ~[classes/:na]
...
编程式
@Test
public void test(){
UserService userService = mock(UserService.class);
RetryConfig config = RetryConfig.custom().maxAttempts(3).build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("my");
Function<Integer, Void> decorated
= Retry.decorateFunction(retry, (Integer s) -> {
userService.getUserInfoById("1");
return null;
});
when(userService.getUserInfoById(anyString())).thenThrow(new RuntimeException());
try {
decorated.apply(1);
fail("Expected an exception to be thrown if all retries failed");
} catch (Exception e) {
verify(userService, times(3)).getUserInfoById(anyString());
}
}
配置
配置参数 | 默认值 | 描述 |
---|---|---|
maxAttempts | 3 | 最大重试次数 |
waitDuration | 500[ms] | 固定重试间隔 |
intervalFunction | numberOfAttempts -> waitDuration | 用来改变重试时间间隔,可以选择指数退避或者随机时间间隔 |
retryOnResultPredicate | result -> false | 自定义结果重试规则,需要重试的返回true |
retryOnExceptionPredicate | throwable -> true | 自定义异常重试规则,需要重试的返回true |
retryExceptions | empty | 需要重试的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |
resilience4j:
retry:
configs:
default:
maxRetryAttempts: 3
waitDuration: 10s
enableExponentialBackoff: true # 是否允许使用指数退避算法进行重试间隔时间的计算
expontialBackoffMultiplier: 2 # 指数退避算法的乘数
enableRandomizedWait: false # 是否允许使用随机的重试间隔
randomizedWaitFactor: 0.5 # 随机因子
resultPredicate: com.example.resilience4j.predicate.RetryOnResultPredicate
retryExceptionPredicate: com.example.resilience4j.predicate.RetryOnExceptionPredicate
retryExceptions:
- com.example.resilience4j.exceptions.BusinessBException
- com.example.resilience4j.exceptions.BusinessAException
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
ignoreExceptions:
- io.github.resilience4j.circuitbreaker.CallNotPermittedException
instances:
backendA:
baseConfig: default
waitDuration: 5s
backendB:
baseConfig: default
maxRetryAttempts: 2
注意指数退避和随机间隔不能同时启用。
事件监听
注册事件侦听器。事件侦听器捕获过程中发生的不同事件并执行特定操作,例如相应地记录或处理事件。
@Autowired
private RetryRegistry retryRegistry;
@PostConstruct
public void postConstruct() {
io.github.resilience4j.retry.Retry.EventPublisher eventPublisher = retryRegistry.retry("getUserInfo").getEventPublisher();
eventPublisher.onEvent(event -> System.out.println("Retry - On Event. Event Details: " + event));
eventPublisher.onSuccess(event -> System.out.println("Retry - On Success. Event Details: " + event));
eventPublisher.onError(event -> System.out.println("Retry - On Failure. Event Details: " + event));
eventPublisher.onRetry(event -> System.out.println("Retry - On Retry. Event Details: " + event));
}
...
@Retry(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
public String getUserInfoById(String id) {
....
2023-08-09 21:47:30.114 INFO 4612 --- [nio-8080-exec-1] com.laker.service.UserService : 开始请求远程服务
Retry - On Event. Event Details: 2023-08-09T21:47:30.114+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '1'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
Retry - On Retry. Event Details: 2023-08-09T21:47:30.114+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '1'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
2023-08-09 21:47:32.116 INFO 4612 --- [nio-8080-exec-1] com.laker.service.UserService : 开始请求远程服务
Retry - On Event. Event Details: 2023-08-09T21:47:32.116+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '2'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
Retry - On Retry. Event Details: 2023-08-09T21:47:32.116+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '2'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
2023-08-09 21:47:34.128 INFO 4612 --- [nio-8080-exec-1] com.laker.service.UserService : 开始请求远程服务
Retry - On Event. Event Details: 2023-08-09T21:47:34.128+08:00[Asia/Shanghai]: Retry 'getUserInfo' recorded a failed retry attempt. Number of retry attempts: '3'. Giving up. Last exception was: 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
Retry - On Failure. Event Details: 2023-08-09T21:47:34.128+08:00[Asia/Shanghai]: Retry 'getUserInfo' recorded a failed retry attempt. Number of retry attempts: '3'. Giving up. Last exception was: 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
2023-08-09 21:47:34.129 ERROR 4612 --- [nio-8080-exec-1] com.laker.service.UserService : 远程服务异常,启用降级方法,id:1
org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY
at com.laker.service.UserService.getUserInfoById(UserService.java:39) ~[classes/:na]
...
事件:http://localhost:8080/actuator/retryevents
指标监控
management:
endpoint:
health:
show-details: ALWAYS
endpoints:
web:
exposure:
include: "*"
CircuitBreaker、Retry、RateLimiter、Bulkhead 和 TimeLimiter 指标会自动发布在指标端点上。请参阅执行器指标文档。
http://localhost:8080/actuator/metrics
要检索指标,请向 发出 GET 请求。 例如:/actuator/metrics/{metric.name}
http://localhost:8080/actuator/metrics/resilience4j.retry.calls
{
"name":"resilience4j.retry.calls",
"description":"The number of successful calls after a retry attempt",
"baseUnit":null,
"measurements":[
{
"statistic":"COUNT",
"value":2
}
],
"availableTags":[
{
"tag":"kind",
"values":[
"successful_without_retry",
"successful_with_retry",
"failed_with_retry",
"failed_without_retry"
]
},
{
"tag":"name",
"values":[
"getUserInfo"
]
}
]
}
健康检查
Spring Boot Actuator 健康信息可用于检查正在运行的应用程序的状态。监控软件经常使用它来提醒某人生产系统是否存在严重问题。
默认情况下,CircuitBreaker 或 RateLimiter 运行状况指示器处于禁用状态,但您可以通过配置启用它们。运行状况指示器被禁用,因为当断路器打开时,应用程序状态为“关闭”。这可能不是您想要实现的目标。
management.health.circuitbreakers.enabled: true
management.health.ratelimiters.enabled: true
resilience4j.circuitbreaker:
configs:
default:
registerHealthIndicator: true
resilience4j.ratelimiter:
configs:
instances:
registerHealthIndicator: true
闭合的 CircuitBreaker 状态映射到 UP,打开状态映射到 DOWN,半打开状态映射到 UNKNOWN。
速率限制
速率限制是一种用于控制系统或 API 请求流的技术。想象一下,您有一个受欢迎的网站或 Web 服务,收到大量请求。如果没有速率限制,您的系统可能会不堪重负,导致响应时间变慢、服务器负载增加,甚至崩溃。速率限制对特定时间范围内可以发出的请求数量进行限制,确保系统保持稳定和可用。它有助于防止滥用、防止 DDoS 攻击并允许公平使用资源。通过实施速率限制,您可以有效管理系统流量、保持性能并为用户提供更好的体验。
速率限制有两种主要方法:client-side
和server-side
速率限制。在客户端速率限制中,客户端控制其向外部服务发出的请求数量。另一方面,服务器端速率限制对传入请求设置限制。了解这些方法之间的差异有助于保持系统稳定性并优化性能。
服务器端速率限制需要诸如缓存和多个服务器实例之间的协调之类的东西,这是resilience4j不支持的。对于服务器端速率限制,有 API 网关和 API 过滤器。
resilience4j常用于客户端速率限制
bucket4j常用于服务端速率限制
<dependency>
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
<artifactId>bucket4j-spring-boot-starter</artifactId>
</dependency>
速率限制器的概念
- 能够限制对方法的访问,在定义的时间段内限制指定数量的调用
- 如果达到阈值,我们可以在实际尝试调用该方法之前设置最大等待时间
- 如果仍然达到阈值,则抛出 RequestNotPermission 并执行后备方法
例如,我们需求如下:
- 将我们的 API 使用限制为每秒2次调用
- 如果给定时间段内 API 使用率高于阈值,后续调用将等待 1 秒
- 等待一段时间后,如果API使用率仍然高于阈值,则返回缓存值(或者向用户返回 429)
注解式
@Service
@Slf4j
public class UserService {
// 制定策略 和 降级方法
@SneakyThrows
@RateLimiter(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
public String getUserInfoById(String id) {
log.info("开始请求远程服务");
log.info("结束请求远程服务");
return id;
}
// 降级方法
private String getUserInfoFallback(String id, Exception exception) {
if (exception instanceof RequestNotPermitted){
log.error("触发限流,启用降级方法,id:{}", id, exception);
}
return "getUserInfoFallbackFromLocalCache";
}
}
配置
resilience4j:
ratelimiter:
instances:
getUserInfo:
# 周期内可用的令牌数
limit-for-period: 2
# 刷新周期
limit-refresh-period: 1s
# 等待许可持续时间
timeout-duration: 1s
向用户返回 429
这里一般情况下使用bucket4j而不是resilience4j
@RateLimiter(name = "getUserInfo")
public String getUserInfoById(String id) {
log.info("开始请求远程服务");
log.info("结束请求远程服务");
return id;
}
@ControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler({ RequestNotPermitted.class })
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public void handleRequestNotPermitted() {
}
}
编程式
在 Resilience4j 中,您可以灵活地以编程方式创建速率限制器实例。这意味着您可以根据应用程序的要求动态配置和自定义速率限制器,从而对请求的管理和限制方式进行精细控制。
@Configuration
public class RateLimiterConfiguration {
@Autowired
private RateLimiterRegistry rateLimiterRegistry;
@Bean
public RateLimiter rateLimitWithCustomConfig() {
RateLimiterConfig customConfig = RateLimiterConfig.custom()
.limitForPeriod(2)
.limitRefreshPeriod(Duration.of(5, ChronoUnit.SECONDS))
.timeoutDuration(Duration.of(5, ChronoUnit.SECONDS))
.build();
return rateLimiterRegistry.rateLimiter("customRateLimiterConfig", customConfig);
}
}
@RateLimiter(name = "customRateLimiterConfig")
public Movie getUserInfoById(String Id) {
}
配置
配置参数 | 默认值 | 描述 |
---|---|---|
timeoutDuration | 5[s] | 线程等待权限的默认等待时间 |
limitRefreshPeriod | 500[ns] | 权限刷新的时间,每个周期结束后,RateLimiter将会把权限计数设置为limitForPeriod的值 |
limiteForPeriod | 50 | 一个限制刷新期间的可用权限数 |
resilience4j.ratelimiter:
configs:
default:
limit-for-period: 5
limit-refresh-period: 1s
timeout-duration: 3s
allow-health-indicator-to-fail: true
subscribe-for-events: true
event-consumer-buffer-size: 50
registerHealthIndicator: true
instances:
externalService:
baseConfig: default
limitForPeriod
:该选项指定在特定时间段内允许的最大请求数。例如,如果将其设置为 5,则表示该时间段内仅允许 5个请求。limitRefreshPeriod
:此选项定义上述时间段的持续时间。它决定重置速率限制的频率。例如,如果将其设置为 1 秒,则每秒都会重置速率限制,再次允许指定数量的请求。timeoutDuration
:该选项设置请求的超时时间。如果请求超过此持续时间,则会被视为花费时间过长,并可能会触发回退行为或异常。设置为 3s 意味着如果没有机会执行,请求将在三秒后失败。registerHealthIndicator
:设置为 时true
,此选项启用速率限制器的运行状况指示器注册。运行状况指示器提供有关速率限制器当前状态的信息,使您可以在应用程序中监控其运行状况。eventConsumerBufferSize
:此选项确定用于存储速率限制器事件的缓冲区的大小。速率限制器事件包括成功或拒绝的请求等信息。缓冲区大小应足够大,以避免在高流量期间丢失事件。
事件监听
同重试器
rateLimiter.getEventPublisher()
.onSuccess(event -> log.info(event.toString()))
.onFailure(event -> log.info(event.toString()));
http://localhost:8080/actuator/ratelimiterevents
指标监控
编程式获取
RateLimiter.Metrics metrics = rateLimiter.getMetrics();
// Returns the number of availablePermissions in this duration.
int availablePermissions = metrics.getAvailablePermissions();
// Returns the number of WaitingThreads
int numberOfWaitingThreads = metrics.getNumberOfWaitingThreads();
log.info(time + ", metrics[ availablePermissions=" + availablePermissions +
", numberOfWaitingThreads=" + numberOfWaitingThreads + " ]");
执行器端点提供有价值的信息和指标,可以帮助您监控和分析 Resilience4j 速率限制器的行为和性能。
/actuator/health
: Resilience4j 中的端点提供有关应用程序中速率限制器的运行状况的信息。此端点概述了所有注册的速率限制器的运行状况,指示它们是否正常运行或遇到任何问题。- **
/actuator/ratelimiterevents
:**该端点提供有关速率限制器事件的信息。它可以深入了解事件的发生情况,例如成功的请求、拒绝的请求或其他与速率限制器相关的事件。监视这些事件可以帮助您了解速率限制器的行为和性能。 - **
/actuator/ratelimiters
:**此端点提供有关应用程序中注册的速率限制器的详细信息。它提供诸如速率限制器名称之类的信息。 - **
/actuator/metrics/resilience4j.ratelimiter.available.permissions
:**此端点提供与速率限制器中的可用权限相关的指标。它提供了有关可用于在定义的速率限制内接受新请求的剩余权限或插槽数量的见解。 - **
/actuator/metrics/resilience4j.ratelimiter.waiting_threads
:**此端点提供与速率限制器中等待线程相关的指标。它提供有关当前因达到速率限制而等待从速率限制器获取权限或插槽的线程数量的信息。
为了在执行器端点中正确显示速率限制器运行状况和事件信息,请考虑在文件中添加以下属性application.yml
。
management:
endpoint:
health:
show-details: ALWAYS
health:
ratelimiters:
enabled: true
endpoints:
web:
exposure:
include: "*"
resilience4j.ratelimiter:
instances:
simpleRateLimit:
limitForPeriod: 2
limitRefreshPeriod: 15s
timeoutDuration: 5s
registerHealthIndicator: true
subscribeForEvents: true
动态修改配置
可以通过动态修改属性来更新 Resilience4j 中现有实例的配置。
@Autowired
private RateLimiterRegistry rateLimiterRegistry;
......
void updateRateLimiterConfiguration(String name, int limitForPeriod, Duration timeoutDuration) {
RateLimiter limiter = rateLimiterRegistry.rateLimiter(name);
limiter.changeLimitForPeriod(limitForPeriod);
limiter.changeTimeoutDuration(timeoutDuration);
}
自定义方法updateRateLimiterConfiguration
允许您更改速率限制器的周期限制并调整超时持续时间。通过提供速率限制器的名称以及所需的新限制和超时值,您可以轻松调整速率限制行为,以更好地满足您的应用程序的需求。这种灵活性使您能够实时微调速率限制器,确保高效的请求处理和最佳性能。
断路器
如果微服务“A”依赖于微服务“B”。由于某种原因,微服务“B”遇到错误。微服务“A”不应重复调用微服务“B”,而应暂停(不调用),直到微服务“B”完全或部分恢复。使用**断路器,**我们可以消除故障流向下游/上游。
关闭
当一个微服务不断调用依赖的微服务时,我们称该电路处于关闭状态。
打开
当微服务不调用依赖的微服务时,它会调用为容忍故障而实现的回退方法。我们称这种状态为开放状态。当一定比例的请求失败时,比如说 90%,我们会将状态从 Closed 更改为 Open。
半开
当微服务将一定比例的请求发送到依赖的微服务,并将其余请求发送到 Fallback 方法时。我们称这种状态为半开。
在打开状态下,我们可以配置等待时长。一旦等待时间结束,断路器将进入半开状态。在此状态下,断路器会检查相关服务是否已启动。为了实现这一点,它会向我们可以配置的依赖服务发送一定比例的请求。如果它得到依赖服务的肯定响应,它将切换到关闭状态,否则它将再次返回到打开状态。
断路器可以是基于计数的或基于时间的。如果最后 N 次调用失败或速度缓慢,基于计数的断路器会将状态从关闭切换为打开。如果最后 N 秒内的响应失败或缓慢,基于时间的断路器将切换到断开状态。在这两个断路器中,我们还可以指定失败或缓慢调用的阈值。
例如,我们可以配置一个基于计数的断路器,如果最后 25 个调用中有 70% 失败或花费超过 2 秒才能完成,则“断开电路”。同样,如果过去 30 秒内 80% 的调用失败或耗时超过 5 秒,我们可以告诉基于时间的断路器断开电路。
注解式
@Service
@Slf4j
public class UserService {
// 制定策略 和 降级方法
@SneakyThrows
@CircuitBreaker(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
public String getUserInfoById(String id) {
log.info("开始请求远程服务");
log.info("结束请求远程服务");
return id;
}
// 降级方法
private String getUserInfoFallback(String id, Exception exception) {
if (exception instanceof RequestNotPermitted){
log.error("启用降级方法,id:{}", id, exception);
}
return "getUserInfoFallbackFromLocalCache";
}
}
配置
resilience4j.circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowSize: 10
slidingWindowType: COUNT_BASED
minimumNumberOfCalls: 5
permittedNumberOfCallsInHalfOpenState: 4
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 5s
failureRateThreshold: 80
eventConsumerBufferSize: 50
instances:
externalService:
baseConfig: default
- failure-rate-threshold=80表示如果 80% 的请求失败,则断开电路,即。使断路器状态为“打开”。
- sliding-window-size=10表示如果 10 个请求中有 80%(即 8 个)失败,则断开电路。
- sliding-window-type=COUNT_BASED表示我们正在使用 COUNT_BASED 滑动窗口。另一种类型是TIME_BASED。
- minimum-number-of-calls=5表示我们至少需要5次调用来计算失败率阈值。
- automatic-transition-from-open-to-half-open-enabled=true表示不要直接从打开状态切换到关闭状态,也考虑半开状态。
- permissed-number-of-calls-in-half-open-state=4表示处于half-open状态时,考虑发送4个请求。如果其中 80% 出现故障,请将断路器切换至断开状态。
- wait-duration-in-open-state=5s表示从打开状态切换到关闭状态时的等待时间间隔。
配置
配置参数 | 默认值 | 描述 |
---|---|---|
failureRateThreshold | 50 | 熔断器关闭状态和半开状态使用的同一个失败率阈值 |
ringBufferSizeInHalfOpenState | 10 | 熔断器半开状态的缓冲区大小,会限制线程的并发量,例如缓冲区为10则每次只会允许10个请求调用后端服务 |
ringBufferSizeInClosedState | 100 | 熔断器关闭状态的缓冲区大小,不会限制线程的并发量,在熔断器发生状态转换前所有请求都会调用后端服务 |
waitDurationInOpenState | 60(s) | 熔断器从打开状态转变为半开状态等待的时间 |
automaticTransitionFromOpenToHalfOpenEnabled | false | 如果置为true,当等待时间结束会自动由打开变为半开,若置为false,则需要一个请求进入来触发熔断器状态转换 |
recordExceptions | empty | 需要记录为失败的异常列表 |
ignoreExceptions | empty | 需要忽略的异常列表 |
recordFailure | throwable -> true | 自定义的谓词逻辑用于判断异常是否需要记录或者需要忽略,默认所有异常都进行记录 |
resilience4j:
circuitbreaker:
configs:
default:
ringBufferSizeInClosedState: 5 # 熔断器关闭时的缓冲区大小
ringBufferSizeInHalfOpenState: 2 # 熔断器半开时的缓冲区大小
waitDurationInOpenState: 10000 # 熔断器从打开到半开需要的时间
failureRateThreshold: 60 # 熔断器打开的失败阈值
eventConsumerBufferSize: 10 # 事件缓冲区大小
registerHealthIndicator: true # 健康监测
automaticTransitionFromOpenToHalfOpenEnabled: false # 是否自动从打开到半开,不需要触发
recordFailurePredicate: com.example.resilience4j.exceptions.RecordFailurePredicate # 谓词设置异常是否为失败
recordExceptions: # 记录的异常
- com.example.resilience4j.exceptions.BusinessBException
- com.example.resilience4j.exceptions.BusinessAException
ignoreExceptions: # 忽略的异常
- com.example.resilience4j.exceptions.BusinessAException
instances:
backendA:
baseConfig: default
waitDurationInOpenState: 5000
failureRateThreshold: 20
backendB:
baseConfig: default
可以配置多个熔断器实例,使用不同配置或者覆盖配置。
舱壁
Bulkhead 是另一个有趣且重要的容错模式,因 Michael T Nygard 在他的书《Release It》中而闻名。在船舶中,舱壁是隔板,如果密封,可形成单独的水密隔间。如果船舶的某一部分发生损坏或漏水,舱壁会阻止水到达其他舱室,最终限制船舶的损坏并防止其下沉。
隔板提供的隔离同样可以应用于软件。在此模式中,依赖项是隔离的,这样一个依赖项中的资源限制不会影响其他依赖项或整个系统。这种隔离可以通过为每个依赖项分配一个线程池来实现。
这里我们分配了一个由 10 个线程组成的池来调用 Service1,5 个线程来调用 Service2,4 个线程来调用 Service3。它提供了两个重要的好处。
- 如果依赖线程花费更多时间来响应,主请求线程可能会离开。如果没有,我们将耗尽请求线程。
- 我们可以控制有多少并发线程调用服务,从而通过限制请求来保护服务的使用者和提供者。
它还提供了一种使用信号量控制并发线程计数的方法。因此,resilience4j 提供了带有线程池和信号量的舱壁模式。
注1:基于信号量的舱壁将使用相同的用户请求线程,并且不会创建新线程。而线程池块头会创建新的线程进行处理。
注2: ThreadPool Bulkhead仅适用于Completable Future。
注 3: Semaphore Bulkhead 是默认设置。
注解式
@Bulkhead(name="my-service1", type=Bulkhead.Type.THREADPOOL, fallbackMethod = "fallback")
public CompletableFuture<String> callService() {
return CompletableFuture.completedFuture(sampleFeignClient.callHelpdesk());
}
@Bulkhead(name="my-service2", fallbackMethod = "fallback")
public String callService2() {
return sampleFeignClient.callHelpdesk();
}
配置
#Threadpool bulkhead
resilience4j.thread-pool-bulkhead:
instances:
my-service1:
maxThreadPoolSize: 3
coreThreadPoolSize: 2
queueCapacity: 1
keepAliveDuration: 20ms
#Semaphore bulkhead
resilience4j.bulkhead:
instances:
my-service2:
maxConcurrentCalls: 2 #Max Amount of parallel execution allowed by bulkhead.
maxWaitDuration: 5s
时间限制器
时间限制是为微服务响应设置时间限制的过程。假设微服务“A”向微服务“B”发送请求,它为微服务“B”设置响应时间限制。如果微服务“B”在该时限内没有响应,则将被认为存在故障。
与Hystrix不同,Resilience4j将超时控制器从熔断器中独立出来,成为了一个单独的组件,主要的作用就是对方法调用进行超时控制。实现的原理和Hystrix相似,都是通过调用Future的get方法来进行超时控制。
Timelimiter 方面仅适用于反应式方法或 Completable future。返回的方法CompletableFuture
也应该在线程池中运行。
@TimeLimiter(name="service1-tl")
public CompletableFuture<String> callService() {
return CompletableFuture.completedFuture(sampleFeignClient.callHelpdesk());
}
配置
resilience4j.timelimiter:
instances:
service1-tl:
# 超时时长
timeoutDuration: 2s
# 发生异常是否关闭线程
cancelRunningFuture: true
多组件配合使用
回退方法机制的工作原理类似于 try/catch 块。如果配置了后备方法,则每个执行都会转发到后备方法执行器。回退方法执行器正在搜索可以处理异常的最佳匹配回退方法。类似于 catch 块。回退的执行与断路器的当前状态无关。
后备方法应放置在同一个类中,并且必须具有相同的方法签名,只有一个额外的目标异常参数。
如果有多个fallbackMethod方法,则将调用最接近匹配的方法,例如:
如果您尝试从 恢复,则会调用带签名的方法。NumberFormatException``String fallback(String parameter, NumberFormatException exception)}
仅当多个方法具有相同的返回类型并且您希望为它们一劳永逸地定义相同的回退方法时,您才可以定义一个带有异常参数的全局回退方法。
@CircuitBreaker(name = BACKEND, fallbackMethod = "fallback")
@RateLimiter(name = BACKEND)
@Bulkhead(name = BACKEND, fallbackMethod = "fallback")
@Retry(name = BACKEND)
@TimeLimiter(name = BACKEND)
public Mono<String> method(String param1) {
return Mono.error(new NumberFormatException());
}
private Mono<String> fallback(String param1, CallNotPermittedException e) {
return Mono.just("Handled the exception when the CircuitBreaker is open");
}
private Mono<String> fallback(String param1, BulkheadFullException e) {
return Mono.just("Handled the exception when the Bulkhead is full");
}
private Mono<String> fallback(String param1, NumberFormatException e) {
return Mono.just("Handled the NumberFormatException");
}
private Mono<String> fallback(String param1, Exception e) {
return Mono.just("Handled any other exception");
}
resilience4j默认的Aspect顺序是:
Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )
首先Bulkhead 创建一个线程池。那么TimeLimiter就可以限制线程的时间。
RateLimiter 限制可配置时间窗口内该函数的调用次数。
TimeLimiter 或 RateLimiter 抛出的任何异常都将被 CircuitBreaker 记录。然后会执行重试。
这个默认是有道理的。但出于任何原因,如果我们想要更改方面的顺序,我们可以在 application.yml 中指定,如下所示。数字越大优先级越高。根据下面的配置,重试将在断路器之前完成。
resilience4j:
circuitbreaker:
circuitBreakerAspectOrder: 1
retry:
retryAspectOrder: 2
- resilience4j.retry.retryAspectOrder
- resilience4j.circuitbreaker.circuitBreakerAspectOrder
- resilience4j.ratelimiter.rateLimiterAspectOrder
- resilience4j.timelimiter.timeLimiterAspectOrder
- resilience4j.bulkhead.bulkheadAspectOrder
最佳实践
断路器 重试 限流器 时间限制器 舱壁,这几个组件一般的顺序是什么?
在一般情况下,将断路器、重试、限流器、时间限制器和舱壁这几个组件结合在一起时,可以考虑以下顺序作为一个基本的指导:
重试(Retry):
将重试作为第一个组件是有意义的,因为它可以尝试在短时间内恢复调用,特别是在短暂的故障情况下。设置适当的重试次数、重试间隔和退避策略,以及合理的超时设置,确保调用能够在失败后快速重试,而不会阻塞太长时间。
断路器(Circuit Breaker):
在重试尝试失败后,断路器是下一个应该考虑的组件。如果一个调用经过多次重试仍然失败,那么很可能后端服务出现了持续性的问题。在这种情况下,打开断路器可以避免频繁调用失败,节省资源,同时可以在一段时间后尝试重启。
限流器(Rate Limiter):
在重试和断路器之后,限流器是可以添加的另一个保护层。它可以控制请求的速率,防止大量的请求涌入后端服务。通过限制请求速率,可以减轻服务的负载,防止因为过多的请求导致服务不稳定。
时间限制器(Timeout):
时间限制器可以放在限流器之后,确保请求不会耗费过长时间。设置适当的超时时间,以避免因为某个请求占用资源太长时间而影响其他请求的处理。
舱壁(Bulkhead):
舱壁通常作为最后一个组件考虑。它可以帮助限制并发请求数量,确保资源在不同任务之间得到合理分配。舱壁可以防止一个失败的请求影响到其他请求的执行,从而提高系统的稳定性。
需要强调的是,这只是一种一般的顺序建议。在实际应用中,您可能需要根据系统的需求和性能目标进行调整。同时,也可以根据实际情况将不同组件灵活组合,以构建适合您应用的弹性和容错机制。最重要的是通过测试和监控,确保您的系统在各种异常情况下都能够保持稳定和可用。
配置
您可以在 Spring Boot 的配置文件中配置 CircuitBreaker、Retry、RateLimiter、Bulkhead、Thread pool Bulkhead 和 TimeLimiter 实例。 例如application.yml
resilience4j.circuitbreaker:
instances:
backendA:
registerHealthIndicator: true
slidingWindowSize: 100
backendB:
registerHealthIndicator: true
slidingWindowSize: 10
permittedNumberOfCallsInHalfOpenState: 3
slidingWindowType: TIME_BASED
minimumNumberOfCalls: 20
waitDurationInOpenState: 50s
failureRateThreshold: 50
eventConsumerBufferSize: 10
recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate
resilience4j.retry:
instances:
backendA:
maxAttempts: 3
waitDuration: 10s
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
ignoreExceptions:
- io.github.robwin.exception.BusinessException
backendB:
maxAttempts: 3
waitDuration: 10s
retryExceptions:
- org.springframework.web.client.HttpServerErrorException
- java.io.IOException
ignoreExceptions:
- io.github.robwin.exception.BusinessException
resilience4j.bulkhead:
instances:
backendA:
maxConcurrentCalls: 10
backendB:
maxWaitDuration: 10ms
maxConcurrentCalls: 20
resilience4j.thread-pool-bulkhead:
instances:
backendC:
maxThreadPoolSize: 1
coreThreadPoolSize: 1
queueCapacity: 1
writableStackTraceEnabled: true
resilience4j.ratelimiter:
instances:
backendA:
limitForPeriod: 10
limitRefreshPeriod: 1s
timeoutDuration: 0
registerHealthIndicator: true
eventConsumerBufferSize: 100
backendB:
limitForPeriod: 6
limitRefreshPeriod: 500ms
timeoutDuration: 3s
resilience4j.timelimiter:
instances:
backendA:
timeoutDuration: 2s
cancelRunningFuture: true
backendB:
timeoutDuration: 1s
cancelRunningFuture: false
您还可以覆盖默认配置、定义共享配置并在 Spring Boot 的配置文件中覆盖它们。 例如:application.yml
resilience4j.circuitbreaker:
configs:
default:
slidingWindowSize: 100
permittedNumberOfCallsInHalfOpenState: 10
waitDurationInOpenState: 10000
failureRateThreshold: 60
eventConsumerBufferSize: 10
registerHealthIndicator: true
someShared:
slidingWindowSize: 50
permittedNumberOfCallsInHalfOpenState: 10
instances:
backendA:
baseConfig: default
waitDurationInOpenState: 5000
backendB:
baseConfig: someShared
您还可以使用特定实例名称的定制器来覆盖特定 CircuitBreaker、Bulkhead、Retry、RateLimiter 或 TimeLimiter 实例的配置。下面显示了如何在 YAML 文件中覆盖上面配置的 CircuitBreaker backendA的示例:
@Bean
public CircuitBreakerConfigCustomizer testCustomizer() {
return CircuitBreakerConfigCustomizer
.of("backendA", builder -> builder.slidingWindowSize(100));
}
Resilienc4j Type | Instance Customizer class |
---|---|
Circuit breaker 断路器 | CircuitBreakerConfigCustomizer |
Retry 重试 | RetryConfigCustomizer |
Rate limiter 速率限制器 | RateLimiterConfigCustomizer |
Bulkhead 舱壁 | BulkheadConfigCustomizer |
ThreadPoolBulkhead 线程池隔板 | ThreadPoolBulkheadConfigCustomizer |
Time Limiter 时间限制器 | TimeLimiterConfigCustomizer |
参考:
- https://bootcamptoprod.com/resilience4j-rate-limiter/
- https://resilience4j.readme.io/v1.7.0/docs/getting-started-3