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)
“图书管理”服务
- 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' }
- 配置
只需详细配置一个端口即可:
复制代码
server: port:8083
- 服务实现
服务中包含两个方法 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; } }
- 控制器
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(); } }
- 测试“图书管理”服务
通过如下命令构建并启动应用程序:
复制代码
// 构建应用成功 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 的全部特性。
- 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' }
- 配置
此处,我们需要设置的一些配置项。
默认情况下,在 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(); } }
- 服务
服务实现包含一些方法,这些方法使用 @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 类。
- 控制器
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(); } }
现在,代码已经准备好了。但我们必须构建并启动它。
- 构建并测试“图书馆管理”服务
通过如下命令构建并启动应用程序:
复制代码
// 构建 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 年以上的经验。他是布道者、技术博主和微服务冠军。他的最新博客可以在 这里 找到。