分布式系统的三个断路器框架的原理和实践
springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)
随着微服务的流行,熔断作为其中一项很重要的技术也广为人知。当微服务的运行质量低于某个临界值时,启动熔断机制,暂停微服务调用一段时间,以保障后端的微服务不会因为持续过负荷而宕机。本文介绍了Hystrix、新一代熔断器Resilience4j以及阿里开源的Sentinel如何使用。如有错误欢迎指出。
1. 为什么需要断路器
断路器模式源于Martin Fowler的Circuit Breaker 一文。“断路器”本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生短路时,“断路器”能够及时切断故障电路,防止发生过载、发热甚至起火等严重后果。
在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
针对上述问题,断路器是进行实现了断路、线程隔离、流量控制等一系列服务保护功能的框架。系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。
2. Hystrix
2.1什么是Hystrix
Hystrix是一款Netfix开源的框架,具有依赖隔离,系统容错降级等功能,这也是其最重要的两种用途,还有请求合并等功能。
2.2 Hystrix简单案例
2.2.1 新建一个hystrix工程引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.2.2 在启动类的上加上注解@EnableCircuitBreaker //启用断路器
@EnableCircuitBreaker
public class TestApplication extends SpringBootServletInitializer{
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
}
2.2.3 在 TestProductController中加入断路逻辑
@RequestMapping("/get/{id}")
@HystrixCommand(fallbackMethod="errorCallBack") //测试没有这个数据时,服务降级
public Object get(@PathVariable("id") long id){
Product p= productService.findById(id);
if( p==null){
throw new RuntimeException("查无此商品");
}
return p;
}
//指定一个降级的方法
public Object errorCallBack( @PathVariable("id") long id ){
return id+"不存在,error";
}
2.3 总结
简单介绍了Hystrix工作原理以及简单案例,不过Hystrix官方已经停止开发,就不深入介绍了。
3. Resilience4j
3.1 简介
在Hystrix官方已经停止开发后,Hystrix官方推荐使用新一代熔断器为Resilience4j。Resilience4j是一款轻量级,易于使用的容错库,其灵感来自于Netflix Hystrix,但是专为Java 8和函数式编程而设计。因为库只使用了Vavr(以前称为 Javaslang ),它没有任何其他外部依赖下。相比之下,Netflix Hystrix对Archaius具有编译依赖性,Archaius具有更多的外部库依赖性,例如Guava和Apache Commons Configuration,如果需要使用Resilience4j,也无需引入所有依赖,只需选择你需要的功能模块即可。
3.2 模块构成
Resilience4j提供了几个核心模块:
resilience4j-circuitbreaker:电路断开
resilience4j-ratelimiter:速率限制
resilience4j-bulkhead:隔板
resilience4j-retry:自动重试(同步和异步)
resilience4j-timelimiter:超时处理
resilience4j-cache:结果缓存
3.3 设置Maven
引入依赖
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.13.2</version>
</dependency>
3.4 断路器(CircuitBreaker)
请注意,使用此功能,我们需要引入上文的resilience4j-circuitbreaker依赖。
该熔断器模式下可以帮助我们在远程服务出故障时防止故障级联。
在多次请求失败后,我们就认为服务不可用/超载,并且对之后的所有请求进行短路处理,这样我们就能节约系统资源。让我们看看如何通过Resilience4j实现这一目标。
首先,我们需要定义要使用的设置。最简单的方法是使用默认设置:
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
同样也可以使用自定义参数:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(20)
.ringBufferSizeInClosedState(5)
.build();
在这里,我们将ratethreshold设置为20%,并且最少重试5次。
然后,我们创建一个 CircuitBreaker对象,并通过它调用远程服务:
interface RemoteService {
int process(int i);
}
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("my");
Function<Integer, Integer> decorated = CircuitBreaker
.decorateFunction(circuitBreaker, service::process);
最后,让我们看看它如何通过JUnit测试。
我们调用服务10次。可以验证服务至少调用5次,如果有20%的失败的情况下,会停止调用。
when(service.process(any(Integer.class))).thenThrow(new RuntimeException());
for (int i = 0; i < 10; i++) {
try {
decorated.apply(i);
} catch (Exception ignore) {}
}
verify(service, times(5)).process(any(Integer.class));
断路器的三种状态:
- 关闭— 服务正常,不涉及短路
- 打开— 远程服务宕机,所有请求都短路
- 半开— 进入打开状态一段时间(根据已配置的时间量)后,熔断器允许检查远程服务是否恢复
我们可以配置以下设置:
- 故障率阈值,高于该阈值时CircuitBreaker 打开
- 等待时间,用于定义CircuitBreaker切换为半开状态之前应保持打开状态的时间
- 当CircuitBreaker半开或闭合时,环形缓冲区的大小
- 处理自定义事件的的监听器CircuitBreakerEventListener,它处理CircuitBreaker事件
- 自定义谓词,用于评估异常是否为失败,从而提高失败率
3.5 限流器
此功能需要使用resilience4j-ratelimiter依赖性。
简单示例:
RateLimiterConfig config = RateLimiterConfig.custom().limitForPeriod(2).build();
RateLimiterRegistry registry = RateLimiterRegistry.of(config);
RateLimiter rateLimiter = registry.rateLimiter("my");
Function<Integer, Integer> decorated
= RateLimiter.decorateFunction(rateLimiter, service::process);
现在所有对decorateFunction的调用都符合rate limiter。
我们可以配置如下参数:
- 极限刷新时间
- 刷新期间的权限限制
- 默认等待许可期限
3.6 舱壁隔离
这里需要引入resilience4j-bulkhead依赖,可以限制对特定服务的并发调用数。
让我们看一个使用Bulkhead API配置并发调用的示例:
BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(1).build();
BulkheadRegistry registry = BulkheadRegistry.of(config);
Bulkhead bulkhead = registry.bulkhead("my");
Function<Integer, Integer> decorated
= Bulkhead.decorateFunction(bulkhead, service::process);
为了测试,我们可以调用一个mock服务的方法。这种情况下,我们就确保Bulkhead不允许其他任何调用:
CountDownLatch latch = new CountDownLatch(1);
when(service.process(anyInt())).thenAnswer(invocation -> {
latch.countDown();
Thread.currentThread().join();
return null;
});
ForkJoinTask<?> task = ForkJoinPool.commonPool().submit(() -> {
try {
decorated.apply(1);
} finally {
bulkhead.onComplete();
}
});
latch.await();
assertThat(bulkhead.isCallPermitted()).isFalse();
我们可以配置以下设置:
- 允许的最大并行数
- 进入饱和舱壁时线程将等待的最大时间
3.7 重试
需要引入resilience4j-retry库。可以使用Retry调用失败后自动重试:
RetryConfig config = RetryConfig.custom().maxAttempts(2).build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("my");
Function<Integer, Void> decorated
= Retry.decorateFunction(retry, (Integer s) -> {
service.process(s);
return null;
});
现在,让我们模拟在远程服务调用期间引发异常的情况,并确保库自动重试失败的调用:
when(service.process(anyInt())).thenThrow(new RuntimeException());
try {
decorated.apply(1);
fail("Expected an exception to be thrown if all retries failed");
} catch (Exception e) {
verify(service, times(2)).process(any(Integer.class));
}
我们还可以配置:
- 最大尝试次数
- 重试前的等待时间
- 自定义函数,用于修改失败后的等待间隔
- 自定义谓词,用于评估异常是否会导致重试调用
3.8 缓存
cache模块需要引入resilience4j-cache依赖。初始化代码如下:
javax.cache.Cache cache = ...; // Use appropriate cache here
Cache<Integer, Integer> cacheContext = Cache.of(cache);
Function<Integer, Integer> decorated
= Cache.decorateSupplier(cacheContext, () -> service.process(1));
这里的缓存是通过 JSR-107 Cache实现完成的,Resilience4j提供了操作缓存的方法。
请注意,没有用于装饰方法的API(例如Cache.decorateFunction(Function)),该API仅支持 Supplier和Callable类型。
3.9 限时器
对于此模块,我们需要引入resilience4j-timelimiter依赖,可以限制使用TimeLimiter调用远程服务所花费的时间。
我们设置一个TimeLimiter,配置的超时时间为1毫秒以方便测试:
long ttl = 1;
TimeLimiterConfig config
= TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(ttl)).build();
TimeLimiter timeLimiter = TimeLimiter.of(config);
接下来,让我们调用Future.get()验证Resilience4j是否如预期超时:
Future futureMock = mock(Future.class);
Callable restrictedCall
= TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock);
restrictedCall.call();
verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);
我们也可以将其与断路器(CircuitBreaker)结合使用:
Callable chainedCallable
= CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);
3.10 附加模块
Resilience4j还提供了许多附加的功能模块,可简化其与流行框架和库的集成。
一些比较常见的集成是:
- Spring Boot – resilience4j-spring-boot模块
- Ratpack – Resilience4j-ratpack模块
- Retrofit – resilience4j-Retrofit模块
- Vertx – Resilience4j-vertx模块
- Dropwizard – Resilience4j-metrics模块
- Prometheus – resilience4j-prometheus模块
3.11 总结
通过上文我们了解了Resilience4j库的各个方面的简单使用,以及如何使用它来解决服务器间通信中的各种容错问题。Resilience4j的源码可以在GitHub上找到。
4. Sentinel
4.1 什么是Sentinel?
Sentinel 是面向分布式服务架构的轻量级流量控制组件,由阿里开源,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护等多个维度来保障微服务的稳定性。
4.2 Sentinel 具有以下特性:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
4.3 工作机制:
- 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
- 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
- Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。
4.4 Sentinel总结:
Sentinel 是面向分布式服务架构的高可用流量防护组件,作为阿里的熔断中间件,Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,对于流量防护的高可用、稳定性方面是很突出的。
5.总结
三种主流熔断中间件的性能对比,如表所示:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。