28

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

 4 years ago
source link: https://www.infoq.cn/article/GooGfJwqytt8ujs1QOOs
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文要点

  • 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<Book> 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
publicList<Book>retrieveBookList(){
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<Book> 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<Book> getBookList(){
returnrestTemplate.getForObject("/books", List.class);
}

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

复制代码

   @Override
   @Bulkhead(name ="get", type = Bulkhead.Type.SEMAPHORE, fallbackMethod ="fallbackBulkhead")
    public List<Book> 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<Book> getSellersList() {
returnlibrarymanagementService.getBookList();
}
@GetMapping("/bulkhead")
publicList<Book> 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 ) 来测试该应用程序了。

Rv2Enq7.png!web

图 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 依赖。

naYZj2v.png!web

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

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

qUvQfqu.png!web

图 3 限流配置

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

NrmMn2q.png!web

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

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

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

Eva6JjF.png!web

图 5 Jmeter 配置

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

6vQBFzA.png!web

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

总结

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

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

作者介绍

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

原文链接:

The Future of Spring Cloud’s Hystrix Project


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK