Spring Cloud Hystrix 项目展望:为云原生 Java 项目提供持续支持

本文要点

  • Spring Cloud Hystrix 项目已弃用,因此,新应用程序不应该再使用该项目了。
  • 对于 Spring 开发人员来说, Resilience4j 是实现断路器模式的一个新选择。
  • 除了断路器模式之外, Resilience4j 还提供了限流器、重试、舱壁隔离等特性。
  • Resilience4j 可以很好地与 Spring Boot 配合使用,并且还可以使用 micrometer 库来发送监控度量指标。
  • 由于 Spring 没有为 Hystrix Dashboard 引入替代品,所以用户需要使用 Prometheus 或 NewRelic 来搭建监控。

Spring Cloud Hystrix 项目是由 Netflix Hystrix 库包装而成的。自从那时起,许多企业和开发人员就开始采用它来实现断路器( CircuitBreaker )模式了。

2018 年 11 月,Netflix 宣布将这个项目置于维护模式,它也促使 Spring Cloud 宣布了同样的消息。从那时起,就再没有对这个 Netflix 库进行进一步的增强了。在 2019 年的 SpringOne 上,Spring 宣布将从 Spring Cloud 3.1 版本中移除 Hystrix Dashboard,这促成了它的 官方死亡

由于已经大肆宣传过断路器模式了,许多开发人员不是已经使用它了,就是正想使用它,因此现在需要一个替代品。 Resilience4j 的引入填补这一空白,并且它也为 Hystrix 用户提供迁移路径。

Resilience4j

Resilience4j 的灵感来自 Netflix Hystrix,但它是专为 Java 8 和函数式编程而设计的。与 Hystrix 相比,它是轻量级的,因为它仅需依赖 Vavr 库。相比之下,Netflix Hystrix 依赖于 Archaius,而 Archaius 还需依赖其他几个外部库,比如 Guava 和 ApacheCommons。

与老库相比,新库始终有一个优势,即它可以从以前的错误中吸取教训。Resilience4j 还提供了许多新特性:

断路器(CircuitBreaker)

当一个服务调用另一个服务时,另一个服务总有可能停机或具有高延迟。由于服务可能正在等待其他请求完成,所以这可能会导致线程耗尽。 断路器 模式的功能与电路的熔断器类似:

  • 当多个连续失败超过规定阈值时,断路器跳闸。
  • 在超时期间,所有调用远程服务的请求都将立即失败。
  • 超时到期后,断路器允许有限数量的测试请求通过。
  • 如果测试请求成功,断路器将恢复正常工作状态。
  • 否则,如果还是失败,超时时间将再次开启。

限流器(RateLimiter)

限流 模式可以确保服务在窗口期间只能接收设定的最大请求数。这样重点资源可以限量使用,并且能保证其不会被耗尽。

重试(Retry)

重试 模式可以使应用程序在调用外部服务时处理瞬态失败。它可以确保对外部资源进行特定次数的重试操作。如果所有重试都尝试之后仍没有成功,那么它才应失败,并且应用程序应该能优雅地处理响应。

舱壁隔离(Bulkhead )

舱壁隔离 可以确保发生在系统中某一部分的故障不会导致整个系统瘫痪。它能控制并发调用组件的数量。这样,等待来自该组件的响应资源的数量将会受到限制。舱壁隔离的实现方式有两种:

  • 信号量隔离方式限制了对服务的并发请求数。一旦达到限制,它会立即拒绝请求。
  • 线程池隔离方式使用线程池将服务与调用方分离,并将其包含到系统资源的子集中。

线程池方式还提供了一个等待队列,仅当线程池和队列都满时才会拒绝请求。管理线程池增加了一些开销,且与信号量方式相比,它会稍微降低性能,但它允许挂起的线程超时。

使用 Resilience4j 构建 Spring Boot 应用程序

在本文中,我们将构建 2 个服务:“图书管理”和“图书馆管理”。

在这个系统中,“图书馆管理”调用“图书管理”。我们需要操作“图书管理”服务的上线和下线,以模拟断路器、限流、重试和舱壁隔离等特性的不同场景。

先决条件

  • JDK 8
  • Spring Boot 2.1.x
  • resilience4j 1.1.x ( resilience4j 最新版本是 1.3,但是 resilience4j-spring-boot2 仅有 1.1.x 的最新版本 )
  • 诸如 Eclipse、VSC、 intelliJ 之类的 IDE(最好使用 VSC,因为它非常轻量,与 Eclipse 和 intelliJ 相比,我更喜欢它)
  • Gradle
  • NewRelic APM 工具(也可以使用带有 Grafana 的 Prometheus)

“图书管理”服务

  1. Gradle 依赖

该服务是一个简单的基于 REST 的 API,并且需要依赖一些 Web 和测试相关的标准 spring-boot starter jar 包。我们还会使用 Swagger 来测试该 API:

复制代码

dependencies {
//REST
implementation'org.springframework.boot:spring-boot-starter-web'
//swagger
compilegroup:'io.springfox',name:'springfox-swagger2',version:'2.9.2'
implementationgroup:'io.springfox',name:'springfox-swagger-ui',version:'2.9.2'
testImplementation'org.springframework.boot:spring-boot-starter-test'
}
  1. 配置

只需详细配置一个端口即可:

复制代码

server:
port:8083
  1. 服务实现

服务中包含两个方法 addBook 和 retrieveBookList 。由于它仅是一个演示示例,所以我们使用了一个 ArrayList 对象来存储图书信息:

复制代码

@Service
publicclassBookServiceImplimplementsBookService{

List bookList =newArrayList();

@Override
publicStringaddBook(Book book){
String message ="";
booleanstatus = bookList.add(book);
if(status){
message="Book is added successfully to the library.";
}
else{
message="Book could not be added in library due to some technical issue. Please try later!";
}
returnmessage;
}

@Override
publicListretrieveBookList(){
returnbookList;
}
}
  1. 控制器

Rest Controller 发布了两个 API,一个是用于添加图书的 POST API,另一个是用于检索图书详细信息的 GET API:

复制代码

@RestController
@RequestMapping("/books")
publicclassBookController{

@Autowired
privateBookService bookService ;

@PostMapping
publicString addBook(@RequestBodyBook book){
returnbookService.addBook(book);
}

@GetMapping
publicList retrieveBookList(){
returnbookService.retrieveBookList();
}
}
  1. 测试“图书管理”服务

通过如下命令构建并启动应用程序:

复制代码

// 构建应用成功
gradlew build

// 启动应用成功
java -jar build/libs/bookmanangement-0.0.1-SNAPSHOT.jar

// 端点的 url 链接
http://localhost:8083/books

现在我们可以使用 Swagger UI ( http://localhost:8083/swagger-ui.html )来测试该应用程序了。

在开始构建“图书馆管理”服务之前,请先确保该服务已经启动并处于运行状态。

“图书馆管理”服务

在这个服务中,我们将使用 Resilience4j 的全部特性。

  1. Gradle 依赖

该服务也是一个简单的基于 REST 的 API,并且也需要依赖一些 Web 和测试相关的 spring-boot starter jar 包。为了在该 API 中使用断路器和其他 Resilience4j 特性,我们还依赖了一些其他包,比如 resilience4j-spring-boot2、spring-boot starter-actuato r、spring-bootstarter-aop。此外,我们还添加了 micrometer 相关的依赖(micrometer-registry-prometheus,micrometer-registry-new-relic)来启用监控度量。最后,我们使用了 Swagger 来测试该 API:

复制代码

dependencies {

compile'org.springframework.boot:spring-boot-starter-web'

//resilience
compile"io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}"
compile'org.springframework.boot:spring-boot-starter-actuator'
compile('org.springframework.boot:spring-boot-starter-aop')

//swagger
compilegroup:'io.springfox',name:'springfox-swagger2', version:'2.9.2'
implementationgroup:'io.springfox',name:'springfox-swagger-ui', version:'2.9.2'

// monitoring
compile"io.micrometer:micrometer-registry-prometheus:${resilience4jVersion}"
compile'io.micrometer:micrometer-registry-new-relic:latest.release'

testImplementation'org.springframework.boot:spring-boot-starter-test'
}
  1. 配置

此处,我们需要设置的一些配置项。

默认情况下,在 Spring 2.1.x 中,断路器和限流的执行器 API 是禁用的。我们需要使用管理属性来启用它们。在本文末给出的源码链接中可以查看这些属性。此外,我们还需要配置如下属性:

  • 配置 NewRelic 观察 API 的密钥和账户 ID

复制代码

management:
metrics:
export:
newrelic:
api-key: xxxxxxxxxxxxxxxxxxxxx
account-id: xxxxx
step:1m
  • 为 “add”和“get”服务 API 配置 Resilience4j 断路器属性。

复制代码

resilience4j.circuitbreaker:
instances:
add:
registerHealthIndicator:true
ringBufferSizeInClosedState: 5
ringBufferSizeInHalfOpenState: 3
waitDurationInOpenState: 10s
failureRateThreshold: 50
recordExceptions:
-org.springframework.web.client.HttpServerErrorException
-java.io.IOException
-java.util.concurrent.TimeoutException
-org.springframework.web.client.ResourceAccessException
-org.springframework.web.client.HttpClientErrorException
ignoreExceptions:
  • 为“add”服务 API 配置 Resilience4j 限流器属性。

复制代码

resilience4j.ratelimiter:
instances:
add:
limitForPeriod:5
limitRefreshPeriod:100000
timeoutDuration:1000ms
  • 为“get”服务 API 配置 Resilience4j 重试属性。

复制代码

resilience4j.retry:
 instances:
   get:
     maxRetryAttempts:3
     waitDuration:5000
  • 为“get”服务 API 配置 Resilience4j 舱壁隔离属性。

复制代码

resilience4j.bulkhead:
instances:
get:
maxConcurrentCall:10
maxWaitDuration:10ms

现在,我们将创建一个 LibraryConfig 类来为 RestTemplate 定义一个 bean,以调用“图书管理”服务。我们还在此处对“图书管理”服务的端点 URL 进行了硬编码。对于要上线运行的应用程序来说,这不是一个好主意,但是此演示示例的目的只是为了展示 Resilience4j 的特性。对于线上的应用程序,我们可能要使用服务发现(service-discovery)服务。

复制代码

@Configuration
publicclassLibraryConfig{
    Logger logger = LoggerFactory.getLogger(LibrarymanagementServiceImpl.class);
   privatestaticfinalString baseUrl ="https://bookmanagement-service.apps.np.sdppcf.com";


   @Bean
   RestTemplaterestTemplate(RestTemplateBuilder builder){
        UriTemplateHandler uriTemplateHandler =newRootUriTemplateHandler(baseUrl);
       returnbuilder
                .uriTemplateHandler(uriTemplateHandler)
                .build();
   }
 
}
  1. 服务

服务实现包含一些方法,这些方法使用 @CircuitBreaker 、@RateLimiter、@Retry 和 @Bulkhead 注解封装,所有这些注释都支持 fallbackMethod 属性,并且每个模式在观察到失败时,都会将调用重定向到对应的回退(fallback)方法。我们需要定义这些回退方法的实现:

下面这个方法通过 @CircuitBreaker 注解启用了断路器功能。因此,如果 /books 端点无法返回响应,它将会调用 fallbackForaddBook() 方法。

复制代码

@Override
@CircuitBreaker(name ="add", fallbackMethod ="fallbackForaddBook")
publicString addBook(Book book){
logger.error("Inside addbook call book service. ");
String response = restTemplate.postForObject("/books", book, String.class);
returnresponse;
}

下面这个方法通过 @RateLimiter 注解启用了限流功能。如果 /books 端点达到上面配置中定义的阈值,它将调用 fallbackForRatelimitBook() 方法。

复制代码

@Override
@RateLimiter(name ="add", fallbackMethod ="fallbackForRatelimitBook")
publicString addBookwithRateLimit(Book book){
String response = restTemplate.postForObject("/books", book, String.class);
logger.error("Inside addbook, cause ");
returnresponse;
}

下面这个方法通过 @Retry 注解启用了重试功能。如果 /books 端点达到上面配置中定义的阈值,它也将调用 fallbackRetry() 方法。

复制代码

@Override
@Retry(name ="get", fallbackMethod ="fallbackRetry")
public List getBookList(){
returnrestTemplate.getForObject("/books", List.class);
}

下面这个方法通过 @Bulkhead 注释启用了隔离功能。如果 /books 端点达到上面配置中定义的阈值,它也将调用 fallbackBulkhead() 方法。

复制代码

   @Override
   @Bulkhead(name ="get", type = Bulkhead.Type.SEMAPHORE, fallbackMethod ="fallbackBulkhead")
    public List getBookListBulkhead() {
       logger.error("Inside getBookList bulk head");
       try{
           Thread.sleep(100000);
        }catch(InterruptedException e) {
           // TODO Auto-generated catch block
           e.printStackTrace();
        }
       returnrestTemplate.getForObject("/books", List.class);
    }

搭建完服务层之后,我们需要公开每个方法所对应的 REST API,以便我们对其进行测试。为此,我们需要创建 RestController 类。

  1. 控制器

Rest Controller 公开了 4 个 API:

  • 第一个是一个 POST API,用于添加一本图书
  • 第二个也是一个 POST API,但它用于在限流情况下添加图书
  • 第三个是一个 GET API,用于检索图书的详细信息
  • 第四个也是一个 GET API,用于在舱壁隔离情况下检索图书的详细信息

复制代码

@RestController
@RequestMapping("/library")
publicclassLibrarymanagementController{

@Autowired
privateLibrarymanagementService librarymanagementService;
@PostMapping
publicString addBook(@RequestBodyBook book){
returnlibrarymanagementService.addBook(book);
}

@PostMapping("/ratelimit")
publicString addBookwithRateLimit(@RequestBodyBook book){
returnlibrarymanagementService.addBookwithRateLimit(book);
}

@GetMapping
publicList getSellersList() {
returnlibrarymanagementService.getBookList();
}
@GetMapping("/bulkhead")
publicList getSellersListBulkhead() {
returnlibrarymanagementService.getBookListBulkhead();
}
}

现在,代码已经准备好了。但我们必须构建并启动它。

  1. 构建并测试“图书馆管理”服务

通过如下命令构建并启动应用程序:

复制代码

// 构建
gradlew build

// 启动应用程序
java -jar build/libs/librarymanangement-0.0.1-SNAPSHOT.jar

// 端点 Url
http://localhost:8084/library

现在我们可以使用 Swagger UI( http://localhost:8084/swagger-ui.html ) 来测试该应用程序了。

图 1

运行断路器、限流器、重试、舱壁隔离等测试场景

断路器:断路器已被应用于 addBook API。为了测试它是否有效,我们将停止“图书管理”服务。

  • 首先,通过访问 http://localhost:8084/actuator/health URL 来观察应用程序的运行状况。
  • 现在停止“图书管理”服务,并使用 Swagger UI 点击“图书馆管理”服务的 addBook API

在第一步时,Prometheus 量度应该显示断路器的状态为“CLOSED”。我们是通过 micrometer 依赖来启用 Prometheus 量度的。

执行第二步后,调用将开始失败并重定向到对应回退方法。

一旦超过了阈值(在本例中为 5),它将使触发断路。并且,之后的每个调用都将直接定向到回退方法,而不会尝试使用“图书管理”服务。(您可以到日志文件下观察日志输出语句来验证这一点。现在,我们观察 /health 端点,会发现断路器的状态为“OPEN”。

复制代码

{
"status":"DOWN",
"details": {
"circuitBreakers": {
"status":"DOWN",
"details": {
"add": {
"status":"DOWN",
"details": {
"failureRate":"100.0%",
"failureRateThreshold":"50.0%",
"slowCallRate":"-1.0%",
"slowCallRateThreshold":"100.0%",
"bufferedCalls":5,
"slowCalls":0,
"slowFailedCalls":0,
"failedCalls":5,
"notPermittedCalls":0,
"state":"OPEN"
}
}
}
}
}
}

我们已经在 PCF(Pivotal Cloud Foundry)上部署了相同的代码,这样我们就可以将它与 NewRelic 集成来创建度量仪表盘。为此,我们使用了 micrometer-registry-new-relic 依赖。

图 2 NewRelic 观察到的断路器关闭图

限流器:我们创建了一个单独的 API( http://localhost:8084/library/ratelimit ),它具有和 addBook 相同的功能,但启用了限流功能。在这种情况下,我们需要启动并运行“图书管理”服务。使用如下限流的配置,每 10 秒最多可以有 5 个请求。

图 3 限流配置

一旦我们在 10 秒钟内命中了 API 5 次 ,它就会达到阈值并被限流。为了避免限流,它将调用回退方法,并根据回退方法的实现逻辑作出响应。下图显示,在过去一个小时内,它已经三次达到阈值限流:

图 4 NewRelic 观察到的限流器限流

重试:重试功能可以使 API 不断地重试失败事务,直到配置的最大次数。如果成功,则将计数刷新成零。如果达到阈值,它将重定向到对应的回退方法并执行相应地逻辑。为了模拟这种情况,我们需要在“图书管理”服务关闭时点击 GET API( http://localhost:8084/library )。观察日志会发现它正在打印来自回退方法的响应信息。

舱壁隔离:在这个例子中,我们是采用信号量的实现方式来实现舱壁隔离功能的。为了模拟并发调用,我们使用了 Jmeter 并在“线程”组中设置了 30 个用户调用。

图 5 Jmeter 配置

我们将点击一个启用了舱壁隔离功能的 GET API (),该 API 使用 @Bulkhead 注解,并且我们在其中设置了一段睡眠时间 ,以便它可以达到并发执行的极限。我们观察日志会发现,对于某些线程调用,它将转调对应的回退方法。API 的可用并发调用如下图所示:

图 6 舱壁隔离的可用并发调用指示盘

总结

在本文中,我们学习了一些现在微服务架构必备的特性,这些特性可以使用单个库 Resilience4j 实现。使用带有 Grafana 或 NewRelic 的 Prometheus,我们可以根据度量指标构建仪表盘,并能提高系统的稳定性。

与往常一样,代码可以通过 Github 上找到: spring-boot-resilience4j

作者介绍

Rajesh Bhojwani 是一名解决方案架构师,他帮助团队将应用程序从本地迁移到诸如 PCF 、 AWS 等云平台。他在应用程序开发、设计和运维方面拥有 15 年以上的经验。他是布道者、技术博主和微服务冠军。他的最新博客可以在 这里 找到。

原文链接:

The Future of Spring Cloud’s Hystrix Project