使用Resilience4j實施反應式斷路器 - Wenqi

banq發表於2022-02-09

本文將重點介紹使用 Spring Cloud 斷路器庫 Resilience4j 實現反應式斷路器。

 

為什麼選擇 Resilience4j?

我們可以使用兩個主要庫來實現斷路器。Netflix Hystrix,它採用物件導向的設計,其中對外部系統的呼叫必須包含在HystrixCommand提供的多種功能中。但是,在 SpringOne 2019 中,Spring 宣佈 Hystrix Dashboard 將從 Spring Cloud 3.1 版本中刪除,這使其正式棄用。使用已棄用的庫不是一個好主意。所以選擇很明確,就是 Resilience4j!

Resilience4j 是一個受 Hystrix 啟發的獨立庫,但建立在函數語言程式設計的原則之上。Resilience4J 提供了高階函式(裝飾器),以通過斷路器、速率限制器或隔板來增強任何功能介面、lambda 表示式或方法引用。

Resilience4J 的其他優點包括更精細的配置選項(例如關閉斷路器模式所需的成功執行次數)和更輕的依賴項佔用空間。

我們將使用兩個 Spring Boot 微服務來演示如何實現響應式斷路器:

  • 客戶服務,充當 REST API 提供者,提供客戶 CRUD 端點。
  • customer-service-client,它WebClient通過 Spring Boot Starter Webflux 庫來呼叫 REST API。

 

步驟 1. 新增 POM 依賴項

由於我們選擇WebClient使用 REST API,因此我們需要將 Spring Cloud Circuit Breaker Reactor Resilience4J 依賴項新增到我們的 REST 客戶端應用程式中。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
 

第2步。新增斷路器配置bean

CircuitBreakerConfig類帶有一組預設的斷路器配置值,如果我們選擇對所有的斷路器使用預設的配置值,我們可以建立一個Customize bean,它被傳遞給ReactiveResilience4JCircuitBreakerFactory。該工廠的configureDefault方法可用於提供預設配置。示例片段如下。

@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() {
    return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
            .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
            .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build()).build());
}

如果我們選擇使用自定義的配置值,我們將需要如下定義我們的bean("customer-service "只是一個REST客戶端例項的樣本,你可以使用你給你的REST客戶端應用程式的任何例項名稱)。

@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> customerServiceCusomtizer() {
  return factory -> {
    factory.configure(builder -> builder
      .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(2)).build())
      .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()), "customer-service");
  };
}

 

第3步. 為斷路器屬性新增配置

如果我們定義了我們的自定義配置豆,我們還需要在application.yml中新增斷路器的配置,例如(僅是示例值,數字應根據應用的使用場景進行調整)。

resilience4j.circuitbreaker:
  instances:
    customer-service:
      failureRateThreshold: 50
      minimumNumberOfCalls: 10
      slidingWindowType: TIME_BASED
      slidingWindowSize: 10
      waitDurationInOpenState: 50s
      permittedNumberOfCallsInHalfOpenState: 3

  • failureRateThreshold。當故障率等於或大於閾值時,斷路器過渡到開放狀態並開始短路呼叫。在我們的例子中,這個值是50%,這意味著如果2個請求中有1個失敗,就會達到閾值,這將使斷路器進入開放狀態。
  • MinimumNumberOfCalls: 這個屬性確保一旦執行了最低數量的呼叫,就能計算出故障率。在我們的例子中,在開始計算故障率之前必須執行10個請求。
  • slidingWindowType(滑動視窗型別)。配置滑動視窗的型別,用於記錄斷路器關閉時的呼叫結果。滑動視窗可以是基於計數的,也可以是基於時間的。
  • slidingWindowSize(滑動視窗尺寸)。配置滑動視窗的大小,用於記錄斷路器關閉時的呼叫結果。
  • waitDurationInOpenState: 斷路器在從開放狀態過渡到半開放狀態之前應該等待的時間。在我們的例子中,它是50秒。
  • permittedNumberOfCallsInHalfOpenState: 配置斷路器處於半開狀態時允許的呼叫數量。在我們的例子中,限制是3,這意味著在10秒的視窗中只處理3個請求。

 

第4步。實施Circuit Breaker

現在所有的配置都到位了,我們可以開始使用Circuit Breaker從客戶端裝飾我們的REST API呼叫。在下面的例子中,我們通過建構函式注入WebClient和ReactiveCircuitBreakerFactory到CustomerCientController。然後,我們使用webClient來觸發對傳遞進來的CustomerVO和/或customerId的CRUD呼叫。注意 "轉換 "部分,我們在ReactiveCircuitBreakerFactory的幫助下為 "customer-service "建立ReactiveCircuitBreaker例項(Rcb為ReactiveCircuitBreaker型別)。執行斷路器的行是rcb.run(...)。在下面的示例控制器中,當異常被丟擲時,我們為POST/GET/PUT呼叫返回一個空白的CustomerVO物件作為後退響應。對於DELETE呼叫,我們將返回傳入的customerId作為回退。因此,在REST API供應商停機的情況下,我們不會得到500內部伺服器錯誤,而是通過正確的實施Circuit Breaker,收到後備響應。

@RestController
@Slf4j
@RequiredArgsConstructor
public class CustomerClientController {

    private final WebClient webClient;
    private final ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory;

    @PostMapping("/customers")
    public Mono<CustomerVO> createCustomer(CustomerVO customerVO){
        return webClient.post()
                .uri("/customers")
                //.header("Authorization", "Bearer MY_SECRET_TOKEN")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(customerVO), CustomerVO.class)
                .retrieve()
                .bodyToMono(CustomerVO.class)
                .timeout(Duration.ofMillis(10_000))
                .transform(it -> {
                    ReactiveCircuitBreaker rcb = reactiveCircuitBreakerFactory.create("customer-service");
                    return rcb.run(it, throwable -> Mono.just(CustomerVO.builder().build()));
                });
    }

    @GetMapping("/customers/{customerId}")
    public Mono<CustomerVO> getCustomer(@PathVariable String customerId) {
        return webClient
                .get().uri("/customers/" + customerId)
                .retrieve()
                .bodyToMono(CustomerVO.class)
                .transform(it -> {
                    ReactiveCircuitBreaker rcb = reactiveCircuitBreakerFactory.create("customer-service");
                    return rcb.run(it, throwable -> Mono.just(CustomerVO.builder().build()));
                });
    }

    @PutMapping("/customers/{customerId}")
    public Mono<CustomerVO> updateCustomer(@PathVariable String customerId, CustomerVO customerVO){
        return webClient.put()
                .uri("/customers/" + customerVO.getCustomerId())
                //.header("Authorization", "Bearer MY_SECRET_TOKEN")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(customerVO), CustomerVO.class)
                .retrieve()
                .bodyToMono(CustomerVO.class)
                .transform(it -> {
                    ReactiveCircuitBreaker rcb = reactiveCircuitBreakerFactory.create("customer-service");
                    return rcb.run(it, throwable -> Mono.just(CustomerVO.builder().build()));
                });
    }

    @DeleteMapping("/customers/{customerId}")
    public Mono<String> deleteCustomer(@PathVariable String customerId){
        return webClient.delete()
                .uri("/customers/" + customerId)
                .retrieve()
                .bodyToMono(String.class)
                .transform(it -> {
                    ReactiveCircuitBreaker rcb = reactiveCircuitBreakerFactory.create("customer-service");
                    return rcb.run(it, throwable -> Mono.just(customerId));
                });
    }
}

 

 問題1:Swagger使用者介面在Webflux中不工作

由於我們引入了Webflux庫來使用WebClient,你可能會注意到你的swagger UI最初並不工作。為了讓它工作,請確保以下步驟得到執行。

在pom中新增以下依賴項。

<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-boot-starter</artifactId>
  <version>${springfox.version}</version>
</dependency>

如果你已經實現了註釋@EnableSwagger2WebFlux,請刪除該註釋。

現在訪問swagger的URL應該是:http://<YOUR_APP_SERVER>:<YOUR_APP_PORT>/swagger-ui/,一定要加上結尾"/"。例如,http://localhost:8100/swagger-ui/。

 

如何為沒有返回型別的端點實現斷路?

對於在其響應體中沒有返回內容的端點,如REST API提供商中的以下端點,在REST客戶端,如果我們將呼叫該端點的相應方法標記為返回Mono<Void>,ReactiveCircuitBreaker將無法工作。在REST API提供者關閉的情況下,你會看到500伺服器錯誤,這完全違背了擁有Circuit Breaker的目的。

@DeleteMapping(value = "/{customerId}")
public ResponseEntity deleteCustomer(@PathVariable String customerId) throws Exception {
    customerService.deleteCustomer(customerId);
    return ResponseEntity.noContent().build();
}

在非反應式斷路器的實現中,對於沒有返回型別的方法,我們可以使用 "CheckedRunnable",做如下工作(示例)。

CircuitBreaker circuitBreaker = circuitBreakerFactory.create("customer-service");
CheckedRunnable runnable = () -> customerClient.deleteCustomer(customerId);
Try.run(circuitBreaker.decorateCheckedRunnable(runnable)).get();

但是,在反應式斷路器中,ReactiveCircuitBreaker沒有這樣的介面來裝飾CheckedRunnable,那麼我們該怎麼做?經過一些調查和實驗,我注意到我們可以操縱這種端點的返回型別,使其返回一個一般的型別,如String。簡單地說,如果一個端點,如DELETE呼叫在伺服器端返回Void,我們仍然可以在客戶端操縱該DELETE呼叫的返回型別,以返回一個簡單的型別,如String,只是傳回輸入到該端點的String。例如,在客戶端我們可以這樣實現DELETE呼叫。

public Mono<String> deleteCustomer(@PathVariable String customerId){
    return webClient.delete()
            .uri("/customers/" + customerId)
            .retrieve()
            .bodyToMono(String.class)
            .transform(it -> {
                ReactiveCircuitBreaker rcb = reactiveCircuitBreakerFactory.create("customer-service");
                return rcb.run(it, throwable -> Mono.just(customerId));
            });
}

注意我們正在返回Mono<String>而不是Mono<Void>,而且我們在.bodyToMono(String.class)行指定了返回型別為String,這就是為什麼我們可以簡單地呼叫ReactiveCircuitBreaker的run方法來呼叫裝飾過的反應式斷路器函式。這是我能想到的唯一方法,可以解決ReactiveCircuitBreaker沒有decorateCheckedRunnable方法來處理沒有返回型別的方法。

完整原始碼:GitHub repository

 

相關文章