本文要点
Spring Cloud Hystrix 项目是由 Netflix Hystrix 库包装而成的。自从那时起,许多企业和开发人员就开始采用它来实现断路器(CircuitBreaker )模式了。
2018年11月,Netflix 宣布将这个项目置于维护模式,它也促使 Spring Cloud 宣布了同样的消息。从那时起,就再没有对这个 Netflix 库进行进一步的增强了。在2019年的 SpringOne 上,Spring 宣布将从 Spring Cloud 3.1 版本中移除 Hystrix Dashboard,这促成了它的官方死亡。
由于已经大肆宣传过断路器模式了,许多开发人员不是已经使用它了,就是正想使用它,因此现在需要一个替代品。Resilience4j 的引入填补这一空白,并且它也为 Hystrix 用户提供迁移路径。
Resilience4j 的灵感来自 Netflix Hystrix,但它是专为 Java 8 和函数式编程而设计的。与Hystrix 相比,它是轻量级的,因为它仅需依赖 Vavr 库。相比之下,Netflix Hystrix 依赖于Archaius,而 Archaius 还需依赖其他几个外部库,比如 Guava 和 ApacheCommons。
与老库相比,新库始终有一个优势,即它可以从以前的错误中吸取教训。Resilience4j 还提供了许多新特性:
当一个服务调用另一个服务时,另一个服务总有可能停机或具有高延迟。由于服务可能正在等待其他请求完成,所以这可能会导致线程耗尽。断路器模式的功能与电路的熔断器类似:
限流模式可以确保服务在窗口期间只能接收设定的最大请求数。这样重点资源可以限量使用,并且能保证其不会被耗尽。
重试模式可以使应用程序在调用外部服务时处理瞬态失败。它可以确保对外部资源进行特定次数的重试操作。如果所有重试都尝试之后仍没有成功,那么它才应失败,并且应用程序应该能优雅地处理响应。
舱壁隔离可以确保发生在系统中某一部分的故障不会导致整个系统瘫痪。它能控制并发调用组件的数量。这样,等待来自该组件的响应资源的数量将会受到限制。舱壁隔离的实现方式有两种:
线程池方式还提供了一个等待队列,仅当线程池和队列都满时才会拒绝请求。管理线程池增加了一些开销,且与信号量方式相比,它会稍微降低性能,但它允许挂起的线程超时。
在本文中,我们将构建2个服务:“图书管理”和“图书馆管理”。
在这个系统中,“图书馆管理”调用“图书管理”。我们需要操作“图书管理”服务的上线和下线,以模拟断路器、限流、重试和舱壁隔离等特性的不同场景。
该服务是一个简单的基于 REST 的 API,并且需要依赖一些 Web 和测试相关的标准 spring-boot starter jar 包。我们还会使用 Swagger 来测试该 API:
dependencies {
//REST
implementation 'org.springframework.boot:spring-boot-starter-web'
//swagger
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
implementation group: '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
public class BookServiceImpl implements BookService {
List<Book> bookList = new ArrayList<>();
@Override
public String addBook(Book book) {
String message = "";
boolean status = 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!";
}
return message;
}
@Override
public List<Book> retrieveBookList() {
return bookList;
}
}
Rest Controller 发布了两个 API,一个是用于添加图书的 POST API,另一个是用于检索图书详细信息的 GET API:
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService ;
@PostMapping
public String addBook(@RequestBody Book book){
return bookService.addBook(book);
}
@GetMapping
public List<Book> retrieveBookList(){
return bookService.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 的全部特性。
该服务也是一个简单的基于 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
compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
implementation group: '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 是禁用的。我们需要使用管理属性来启用它们。在本文末给出的源码链接中可以查看这些属性。此外,我们还需要配置如下属性:
management:
metrics:
export:
newrelic:
api-key: xxxxxxxxxxxxxxxxxxxxx
account-id: xxxxx
step: 1m
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:
resilience4j.ratelimiter:
instances:
add:
limitForPeriod: 5
limitRefreshPeriod: 100000
timeoutDuration: 1000ms
resilience4j.retry:
instances:
get:
maxRetryAttempts: 3
waitDuration: 5000
resilience4j.bulkhead:
instances:
get:
maxConcurrentCall: 10
maxWaitDuration: 10ms
现在,我们将创建一个 LibraryConfig 类来为 RestTemplate 定义一个bean,以调用“图书管理”服务。我们还在此处对“图书管理”服务的端点 URL 进行了硬编码。对于要上线运行的应用程序来说,这不是一个好主意,但是此演示示例的目的只是为了展示 Resilience4j 的特性。对于线上的应用程序,我们可能要使用服务发现(service-discovery)服务。
@Configuration
public class LibraryConfig {
Logger logger = LoggerFactory.getLogger(LibrarymanagementServiceImpl.class);
private static final String baseUrl = "https://bookmanagement-service.apps.np.sdppcf.com";
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
UriTemplateHandler uriTemplateHandler = new RootUriTemplateHandler(baseUrl);
return builder
.uriTemplateHandler(uriTemplateHandler)
.build();
}
}
服务实现包含一些方法,这些方法使用 @CircuitBreaker 、@RateLimiter、@Retry 和@Bulkhead 注解封装,所有这些注释都支持 fallbackMethod 属性,并且每个模式在观察到失败时,都会将调用重定向到对应的回退(fallback)方法。我们需要定义这些回退方法的实现:
下面这个方法通过 @CircuitBreaker 注解启用了断路器功能。因此,如果 /books 端点无法返回响应,它将会调用 fallbackForaddBook() 方法。
@Override
@CircuitBreaker(name = "add", fallbackMethod = "fallbackForaddBook")
public String addBook(Book book){
logger.error("Inside addbook call book service. ");
String response = restTemplate.postForObject("/books", book, String.class);
return response;
}
下面这个方法通过 @RateLimiter 注解启用了限流功能。如果 /books 端点达到上面配置中定义的阈值,它将调用 fallbackForRatelimitBook() 方法。
@Override
@RateLimiter(name = "add", fallbackMethod = "fallbackForRatelimitBook")
public String addBookwithRateLimit(Book book){
String response = restTemplate.postForObject("/books", book, String.class);
logger.error("Inside addbook, cause ");
return response;
}
下面这个方法通过 @Retry 注解启用了重试功能。如果 /books端点达到上面配置中定义的阈值,它也将调用 fallbackRetry() 方法。
@Override
@Retry(name = "get", fallbackMethod = "fallbackRetry")
public List<Book> getBookList(){
return restTemplate.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();
}
return restTemplate.getForObject("/books", List.class);
}
搭建完服务层之后,我们需要公开每个方法所对应的 REST API,以便我们对其进行测试。为此,我们需要创建 RestController 类。
Rest Controller 公开了4个API:
@RestController
@RequestMapping("/library")
public class LibrarymanagementController {
@Autowired
private LibrarymanagementService librarymanagementService;
@PostMapping
public String addBook(@RequestBody Book book){
return librarymanagementService.addBook(book);
}
@PostMapping ("/ratelimit")
public String addBookwithRateLimit(@RequestBody Book book){
return librarymanagementService.addBookwithRateLimit(book);
}
@GetMapping
public List<Book> getSellersList() {
return librarymanagementService.getBookList();
}
@GetMapping ("/bulkhead")
public List<Book> getSellersListBulkhead() {
return librarymanagementService.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。为了测试它是否有效,我们将停止“图书管理”服务。
在第一步时,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年以上的经验。他是布道者、技术博主和微服务冠军。他的最新博客可以在这里找到。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货