分布式系统的三个断路器框架的原理和实践

springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)

java开源商城系统

​ 随着微服务的流行,熔断作为其中一项很重要的技术也广为人知。当微服务的运行质量低于某个临界值时,启动熔断机制,暂停微服务调用一段时间,以保障后端的微服务不会因为持续过负荷而宕机。本文介绍了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仅支持 SupplierCallable类型。

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模块
  • RatpackResilience4j-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.总结

三种主流熔断中间件的性能对比,如表所示:

图表

springboot实战电商项目mall4j (https://gitee.com/gz-yami/mall4j)

java开源商城系统


mall4j
26 声望0 粉丝