使用 Spring Boot 的 Resilience4j 指南
一、概述
Resilience4j 是一個輕量級的容錯庫,它為 Web 應用程序提供了多種容錯和穩定性模式。在本教程中,我們將學習如何在一個簡單的 Spring Boot 應用程序中使用這個庫。
2. 設置
在本節中,讓我們專注於為 Spring Boot 項目設置關鍵方面。
2.1。 Maven 依賴項
首先,我們需要添加[spring-boot-starter-web](https://search.maven.org/search?q=g:org.springframework.boot%20AND%20a:spring-boot-starter-web)
依賴項來引導一個簡單的 Web 應用程序:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<dependency>
接下來,我們需要resilience4j-spring-boot2
和[spring-boot-starter-aop](https://search.maven.org/search?q=spring-boot-starter-aop)
依賴項,以便在 Spring Boot 應用程序中使用註釋使用 Resilience-4j 庫中的功能:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
此外,我們還需要添加[spring-boot-starter-actuator](https://search.maven.org/artifact/org.springframework.boot/spring-boot-starter-actuator)
依賴項,以通過一組公開的端點監控應用程序的當前狀態:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
最後,讓我們添加[wiremock-jre8](https://search.maven.org/search?q=g:com.github.tomakehurst%20AND%20a:wiremock-jre8)
依賴項,因為它將幫助我們使用模擬 HTTP 服務器測試我們的 REST API:
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
2.2. RestController
和外部 API 調用者
在使用 Resilience4j 庫的不同功能時,我們的 Web 應用程序需要與外部 API 交互。所以,讓我們繼續為RestTemplate
添加一個 bean,這將幫助我們進行 API 調用。
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder().rootUri("http://localhost:9090")
.build();
}
接下來,讓我們將ExternalAPICaller
類定義為Component
並使用restTemplate bean 作為成員:
@Component
public class ExternalAPICaller {
private final RestTemplate restTemplate;
@Autowired
public ExternalAPICaller(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
}
在此之後,我們可以定義ResilientAppController
類,該類公開 REST API 端點並在內部使用ExternalAPICaller
bean 調用外部 API :
@RestController
@RequestMapping("/api/")
public class ResilientAppController {
private final ExternalAPICaller externalAPICaller;
}
2.3.執行器端點
我們可以通過 Spring Boot 執行器公開健康端點,以了解應用程序在任何給定時間的確切狀態。
因此,讓我們將配置添加到application.properties
文件並啟用端點:
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.health.circuitbreakers.enabled=true
management.health.ratelimiters.enabled=true
此外,當我們需要時,我們將在同一個application.properties
文件中添加特定於功能的配置。
2.4.單元測試
我們的 Web 應用程序將在真實場景中調用外部服務。但是,我們可以通過使用WireMockExtension
類啟動外部服務來模擬這種正在運行的服務的存在。
因此,讓我們將EXTERNAL_SERVICE
定義為ResilientAppControllerUnitTest
類中的靜態成員:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ResilientAppControllerUnitTest {
@RegisterExtension
static WireMockExtension EXTERNAL_SERVICE = WireMockExtension.newInstance()
.options(WireMockConfiguration.wireMockConfig()
.port(9090))
.build();
此外,讓我們添加一個TestRestTemplate
實例來調用 API:
@Autowired
private TestRestTemplate restTemplate;
2.5.異常處理程序
Resilience4j 庫將根據上下文中的容錯模式拋出異常來保護服務資源。但是,這些異常應轉換為帶有對客戶端有意義的狀態代碼的 HTTP 響應。
所以,讓我們定義ApiExceptionHandler
類來保存不同異常的處理程序:
@ControllerAdvice
public class ApiExceptionHandler {
}
當我們探索不同的容錯模式時,我們將在這個類中添加處理程序。
3. 斷路器
斷路器模式通過限制上游服務在部分或完全停機期間調用下游服務來保護下游服務。
讓我們首先公開/api/circuit-breaker
端點並添加@CircuitBreaker
註釋:
@GetMapping("/circuit-breaker")
@CircuitBreaker(name = "CircuitBreakerService")
public String circuitBreakerApi() {
return externalAPICaller.callApi();
}
根據需要,我們還需要在ExternalAPICaller
類中定義callApi()
方法來調用外部端點/api/external
:
public String callApi() {
return restTemplate.getForObject("/api/external", String.class);
}
接下來,讓我們在application.properties
文件中添加斷路器的配置:
resilience4j.circuitbreaker.instances.CircuitBreakerService.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.CircuitBreakerService.minimum-number-of-calls=5
resilience4j.circuitbreaker.instances.CircuitBreakerService.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.CircuitBreakerService.wait-duration-in-open-state=5s
resilience4j.circuitbreaker.instances.CircuitBreakerService.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-size=10
resilience4j.circuitbreaker.instances.CircuitBreakerService.sliding-window-type=count_based
本質上,該配置將允許 50% 的失敗調用處於關閉狀態的服務,之後它將打開電路並開始拒絕帶有CallNotPermittedException
的請求。因此,最好在ApiExceptionHandler
類中為此異常添加處理程序:
@ExceptionHandler({CallNotPermittedException.class})
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public void handleCallNotPermittedException() {
}
最後,讓我們通過使用EXTERNAL_SERVICE:
模擬下游服務停機的場景來測試/api/circuit-breaker
API 端點:
@Test
public void testCircuitBreaker() {
EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
.willReturn(serverError()));
IntStream.rangeClosed(1, 5)
.forEach(i -> {
ResponseEntity response = restTemplate.getForEntity("/api/circuit-breaker", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
});
IntStream.rangeClosed(1, 5)
.forEach(i -> {
ResponseEntity response = restTemplate.getForEntity("/api/circuit-breaker", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
});
EXTERNAL_SERVICE.verify(5, getRequestedFor(urlEqualTo("/api/external")));
}
我們可以注意到前五個調用失敗,因為下游服務已關閉。之後,電路切換到開路狀態。因此,隨後的五次嘗試被拒絕並使用503
HTTP 狀態代碼,而沒有實際調用底層 API。
4. 重試
重試模式通過從暫時性問題中恢復來為系統提供彈性。讓我們從添加帶有@Retry
註釋的/api/retry
API 端點開始:
@GetMapping("/retry")
@Retry(name = "retryApi", fallbackMethod = "fallbackAfterRetry")
public String retryApi() {
return externalAPICaller.callApi();
}
此外,我們可以選擇在所有重試嘗試失敗時提供回退機制。在這種情況下,我們提供了fallbackAfterRetry
作為回退方法:
public String fallbackAfterRetry(Exception ex) {
return "all retries have exhausted";
}
接下來,讓我們更新application.properties
文件以添加將控制重試行為的配置:
resilience4j.retry.instances.retryApi.max-attempts=3
resilience4j.retry.instances.retryApi.wait-duration=1s
resilience4j.retry.metrics.legacy.enabled=true
resilience4j.retry.metrics.enabled=true
因此,我們計劃重試最多 3 次嘗試,每次延遲1s
。
最後,讓我們測試/api/retry
API 端點的重試行為:
@Test
public void testRetry() {
EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
.willReturn(ok()));
ResponseEntity<String> response1 = restTemplate.getForEntity("/api/retry", String.class);
EXTERNAL_SERVICE.verify(1, getRequestedFor(urlEqualTo("/api/external")));
EXTERNAL_SERVICE.resetRequests();
EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
.willReturn(serverError()));
ResponseEntity<String> response2 = restTemplate.getForEntity("/api/retry", String.class);
Assert.assertEquals(response2.getBody(), "all retries have exhausted");
EXTERNAL_SERVICE.verify(3, getRequestedFor(urlEqualTo("/api/external")));
}
我們可以注意到,在第一種情況下,沒有任何問題,因此一次嘗試就足夠了。另一方面,當出現問題時,嘗試了 3 次,之後 API 通過回退機製做出響應。
5.時間限制
我們可以使用時間限制器模式來設置對外部系統的異步調用的閾值超時值。
讓我們添加內部調用慢速 API 的/api/time-limiter
API 端點:
@GetMapping("/time-limiter")
@TimeLimiter(name = "timeLimiterApi")
public CompletableFuture<String> timeLimiterApi() {
return CompletableFuture.supplyAsync(externalAPICaller::callApiWithDelay);
}
此外,讓我們通過在callApiWithDelay()
方法中添加休眠時間來模擬外部 API 調用的延遲:
public String callApiWithDelay() {
String result = restTemplate.getForObject("/api/external", String.class);
try {
Thread.sleep(5000);
} catch (InterruptedException ignore) {
}
return result;
}
接下來,我們需要在application.properties
文件中提供timeLimiterApi
的配置:
resilience4j.timelimiter.metrics.enabled=true
resilience4j.timelimiter.instances.timeLimiterApi.timeout-duration=2s
resilience4j.timelimiter.instances.timeLimiterApi.cancel-running-future=true
我們可以注意到閾值設置為 2s。之後,Resilience4j 庫在內部使用TimeoutException
取消異步操作。所以,讓我們在ApiExceptionHandler
類中為這個異常添加一個處理程序,以返回一個帶有408
HTTP 狀態碼的 API 響應:
@ExceptionHandler({TimeoutException.class})
@ResponseStatus(HttpStatus.REQUEST_TIMEOUT)
public void handleTimeoutException() {
}
最後,讓我們驗證為/api/time-limiter
API 端點配置的時間限制器模式:
@Test
public void testTimeLimiter() {
EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external").willReturn(ok()));
ResponseEntity<String> response = restTemplate.getForEntity("/api/time-limiter", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.REQUEST_TIMEOUT);
EXTERNAL_SERVICE.verify(1, getRequestedFor(urlEqualTo("/api/external")));
}
正如預期的那樣,由於下游 API 調用被設置為需要超過 5 秒才能完成,我們目睹了 API 調用超時。
6. 隔板
隔板模式限制了對外部服務的最大並發調用數。
讓我們從添加帶有@Bulkhead
註釋的/api/bulkhead
API 端點開始:
@GetMapping("/bulkhead")
@Bulkhead(name="bulkheadApi")
public String bulkheadApi() {
return externalAPICaller.callApi();
}
接下來,讓我們在application.properties
文件中定義配置來控制隔板功能:
resilience4j.bulkhead.metrics.enabled=true
resilience4j.bulkhead.instances.bulkheadApi.max-concurrent-calls=3
resilience4j.bulkhead.instances.bulkheadApi.max-wait-duration=1
通過這種方式,我們希望將最大並發調用數限制為3
,因此如果艙壁已滿,每個線程只能等待1ms
。之後,請求會因BulkheadFullException
異常而被拒絕。此外,我們希望向客戶端返回一個有意義的 HTTP 狀態代碼,所以讓我們添加一個異常處理程序:
@ExceptionHandler({ BulkheadFullException.class })
@ResponseStatus(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)
public void handleBulkheadFullException() {
}
最後,讓我們通過並行調用五個請求來測試艙壁行為:
@Test
public void testBulkhead() throws InterruptedException {
EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
.willReturn(ok()));
Map<Integer, Integer> responseStatusCount = new ConcurrentHashMap<>();
IntStream.rangeClosed(1, 5)
.parallel()
.forEach(i -> {
ResponseEntity<String> response = restTemplate.getForEntity("/api/bulkhead", String.class);
int statusCode = response.getStatusCodeValue();
responseStatusCount.put(statusCode, responseStatusCount.getOrDefault(statusCode, 0) + 1);
});
assertEquals(2, responseStatusCount.keySet().size());
assertTrue(responseStatusCount.containsKey(BANDWIDTH_LIMIT_EXCEEDED.value()));
assertTrue(responseStatusCount.containsKey(OK.value()));
EXTERNAL_SERVICE.verify(3, getRequestedFor(urlEqualTo("/api/external")));
}
我們注意到只有三個請求成功,而其他請求被BANDWIDTH_LIMIT_EXCEEDED
HTTP 狀態代碼拒絕。
7.速率限制器
速率限制器模式限制對資源的請求速率。
讓我們從添加帶有@RateLimiter
註釋的/api/rate-limiter
API 端點開始:
@GetMapping("/rate-limiter")
@RateLimiter(name = "rateLimiterApi")
public String rateLimitApi() {
return externalAPICaller.callApi();
}
接下來,讓我們在application.properties
文件中定義速率限制器的配置:
resilience4j.ratelimiter.metrics.enabled=true
resilience4j.ratelimiter.instances.rateLimiterApi.register-health-indicator=true
resilience4j.ratelimiter.instances.rateLimiterApi.limit-for-period=5
resilience4j.ratelimiter.instances.rateLimiterApi.limit-refresh-period=60s
resilience4j.ratelimiter.instances.rateLimiterApi.timeout-duration=0s
resilience4j.ratelimiter.instances.rateLimiterApi.allow-health-indicator-to-fail=true
resilience4j.ratelimiter.instances.rateLimiterApi.subscribe-for-events=true
resilience4j.ratelimiter.instances.rateLimiterApi.event-consumer-buffer-size=50
使用此配置,我們希望將 API 調用速率限制為5
req/min
而無需等待。達到允許速率的閾值後,請求將被拒絕並出現RequestNotPermitted
異常。因此,讓我們在ApiExceptionHandler
類中定義一個處理程序,以將其轉換為有意義的 HTTP 狀態響應代碼:
@ExceptionHandler({ RequestNotPermitted.class })
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public void handleRequestNotPermitted() {
}
最後,讓我們用50
請求測試我們的限速 API 端點:
@Test
public void testRatelimiter() {
EXTERNAL_SERVICE.stubFor(WireMock.get("/api/external")
.willReturn(ok()));
Map<Integer, Integer> responseStatusCount = new ConcurrentHashMap<>();
IntStream.rangeClosed(1, 50)
.parallel()
.forEach(i -> {
ResponseEntity<String> response = restTemplate.getForEntity("/api/rate-limiter", String.class);
int statusCode = response.getStatusCodeValue();
responseStatusCount.put(statusCode, responseStatusCount.getOrDefault(statusCode, 0) + 1);
});
assertEquals(2, responseStatusCount.keySet().size());
assertTrue(responseStatusCount.containsKey(TOO_MANY_REQUESTS.value()));
assertTrue(responseStatusCount.containsKey(OK.value()));
EXTERNAL_SERVICE.verify(5, getRequestedFor(urlEqualTo("/api/external")));
}
正如預期的那樣,只有五個請求成功,而所有其他請求都以TOO_MANY_REQUESTS
HTTP 狀態代碼失敗。
8. 執行器端點
我們已將我們的應用程序配置為支持執行器端點以進行監控。使用這些端點,我們可以使用一種或多種配置的容錯模式來確定應用程序隨時間的行為方式。
首先,我們通常可以使用對/actuator
端點的 GET 請求找到所有暴露的端點:
http://localhost:8080/actuator/
{
"_links" : {
"self" : {...},
"bulkheads" : {...},
"circuitbreakers" : {...},
"ratelimiters" : {...},
...
}
}
我們可以看到 JSON 響應,其中包含諸如bulkheads
、斷路器、速率限制器等字段。每個字段根據其與容錯模式的關聯爲我們提供特定信息。
接下來,我們看一下與重試模式相關的字段:
"retries": {
"href": "http://localhost:8080/actuator/retries",
"templated": false
},
"retryevents": {
"href": "http://localhost:8080/actuator/retryevents",
"templated": false
},
"retryevents-name": {
"href": "http://localhost:8080/actuator/retryevents/{name}",
"templated": true
},
"retryevents-name-eventType": {
"href": "http://localhost:8080/actuator/retryevents/{name}/{eventType}",
"templated": true
}
繼續,讓我們檢查應用程序以查看重試實例列表:
http://localhost:8080/actuator/retries
{
"retries" : [ "retryApi" ]
}
正如預期的那樣,我們可以在配置的重試實例列表中看到retryApi
實例。
最後,讓我們通過瀏覽器向/api/retry
API 端點發出 GET 請求,並使用/actuator/retryevents
端點觀察重試事件:
{
"retryEvents": [
{
"retryName": "retryApi",
"type": "RETRY",
"creationTime": "2022-10-16T10:46:31.950822+05:30[Asia/Kolkata]",
"errorMessage": "...",
"numberOfAttempts": 1
},
{
"retryName": "retryApi",
"type": "RETRY",
"creationTime": "2022-10-16T10:46:32.965661+05:30[Asia/Kolkata]",
"errorMessage": "...",
"numberOfAttempts": 2
},
{
"retryName": "retryApi",
"type": "ERROR",
"creationTime": "2022-10-16T10:46:33.978801+05:30[Asia/Kolkata]",
"errorMessage": "...",
"numberOfAttempts": 3
}
]
}
由於下游服務已關閉,我們可以看到 3 次重試嘗試,任何兩次嘗試之間的等待時間為1s
秒。就像我們配置它一樣。
9. 結論
在本文中,我們了解瞭如何在 Sprint Boot 應用程序中使用 Resilience4j 庫。此外,我們深入研究了幾種容錯模式,例如斷路器、速率限制器、時間限制器、隔板和重試。
與往常一樣,本教程的完整源代碼可在 GitHub 上獲得。