SpringCloud升級之路2020.0.x版-30. FeignClient 實現重試

乾貨滿滿張雜湊發表於2021-11-09

本系列程式碼地址:https://github.com/JoJoTec/spring-cloud-parent

需要重試的場景

微服務系統中,會遇到線上釋出,一般的釋出更新策略是:啟動一個新的,啟動成功之後,關閉一箇舊的,直到所有的舊的都被關閉。Spring Boot 具有優雅關閉的功能,可以保證請求處理完再關閉,同時會拒絕新的請求。對於這些拒絕的請求,為了保證使用者體驗不受影響,是需要重試的。

雲上部署的微服務,對於同一個服務,同一個請求,很可能不會所有例項都同時異常,例如:

  1. Kubernetes 叢集部署的例項,可能同一個虛擬機器 Node 在閒時部署了多個不同微服務例項,當壓力變大時,就需要遷移和擴容。這時候由於不同的微服務壓力不同,當時處於哪一個 Node 也說不定,有的可能處於壓力大的,有的可能處於壓力小的。對於同一個微服務,可能並不會所有例項位於的 Node 壓力都大。
  2. 雲上部署一般會跨可用區部署,如果有一個可用區異常,另一個可用區還可以繼續提供服務。
  3. 某個業務觸發了 Bug,導致例項一直在 GC,但是這種請求一般很不常見,不會發到所有例項上。

這時候,就需要我們對請求進行無感知的重試。

重試需要考慮的問題

  1. 重試需要重試與之前不同的例項,甚至是不處於同一個虛擬機器 Node 的例項,這個主要通過 LoadBalancer 實現,可以參考之前的 LoadBalancer 部分。後面的文章,我們還會改進 LoadBalancer
  2. 重試需要考慮到底什麼請求能重試,以及什麼異常能重試:
  • 假設我們有查詢介面,和沒有做冪等性的扣款介面,那麼很直觀的就能感覺出查詢介面是可以重試的,沒有做冪等性的扣款介面是不能重試的
  • 業務上不能重試的介面,對於特殊的異常(其實是表示請求並沒有發出去的異常),我們是可以重試的。雖然是沒有做冪等性的扣款介面,但是如果丟擲的是原因是 Connect Timeout 的 IOException,這樣的異常代表請求還沒有發出去,是可以重試的
  1. 重試策略:重試幾次,重試間隔。類比多處理器程式設計模式中的 Busy Spin 策略會造成很大的匯流排通量從而降低效能這個現象,如果失敗立刻重試,那麼在某一個例項異常導致超時的時候,會在同一時間有很多請求重試到其他例項。最好加上一定延遲。

使用 resilience4j 實現 FeignClient 重試

FeignClient 本身帶重試,但是重試策略相對比較簡單,同時我們還想使用斷路器以及限流器還有執行緒隔離,resilience4j 就包含這些元件。

原理簡介

Resilience4J 提供了 Retryer 重試器,官方文件地址:https://resilience4j.readme.io/docs/retry

從配置上就能理解其中的原理,但是官方文件配置並不全面,如果想看所有的配置,最好還是通過原始碼:

RetryConfigurationProperties.java

//重試間隔,預設 500ms
@Nullable
private Duration waitDuration;

//重試間隔時間函式,和 waitDuration 只能設定一個,預設就是 waitDuration
@Nullable
private Class<? extends IntervalBiFunction<Object>> intervalBiFunction;

//最大重試次數,包括本身那次呼叫
@Nullable
private Integer maxAttempts;

//通過丟擲的異常判斷是否重試,預設是隻要有異常就會重試
@Nullable
private Class<? extends Predicate<Throwable>> retryExceptionPredicate;

//通過結果判斷是否重試,預設是隻要獲取到結果就不重試
@Nullable
private Class<? extends Predicate<Object>> resultPredicate;

//配置丟擲這些異常以及子類則會重試
@SuppressWarnings("unchecked")
@Nullable
private Class<? extends Throwable>[] retryExceptions;

//配置丟擲這些異常以及子類則不會重試
@SuppressWarnings("unchecked")
@Nullable
private Class<? extends Throwable>[] ignoreExceptions;

//啟用 ExponentialBackoff 延遲演算法,初次重試延遲時間為 waitDuration,之後每次重試延遲時間都乘以 exponentialBackoffMultiplier,直到 exponentialMaxWaitDuration
@Nullable
private Boolean enableExponentialBackoff;

private Double exponentialBackoffMultiplier;

private Duration exponentialMaxWaitDuration;

//啟用隨機延遲演算法,範圍是 waitDuration - randomizedWaitFactor*waitDuration ~ waitDuration + randomizedWaitFactor*waitDuration
@Nullable
private Boolean enableRandomizedWait;

private Double randomizedWaitFactor;

@Nullable
private Boolean failAfterMaxAttempts;

引入 resilience4j-spring-boot2 的依賴,就可以通過 Properties 配置的方式去配置 Retryer 等所有 resilience4j 元件,例如:

application.yml

resilience4j.retry:
  configs:
    default:
      ## 最大重試次數,包括第一次呼叫
      maxRetryAttempts: 2
      ## 重試等待時間
      waitDuration: 500ms
      ## 啟用隨機等待時間,範圍是 waitDuration - randomizedWaitFactor*waitDuration ~ waitDuration + randomizedWaitFactor*waitDuration
      enableRandomizedWait: true
      randomizedWaitFactor: 0.5
    test-client1:
      ## 最大重試次數,包括第一次呼叫
      maxRetryAttempts: 3
      ## 重試等待時間
      waitDuration: 800ms
      ## 啟用隨機等待時間,範圍是 waitDuration - randomizedWaitFactor*waitDuration ~ waitDuration + randomizedWaitFactor*waitDuration
      enableRandomizedWait: true
      randomizedWaitFactor: 0.5  

這樣,我們就可以通過如下程式碼,獲取到配置對應的 Retryer:

@Autowired
RetryRegistry retryRegistry;
//讀取 resilience4j.retry.configs.test-client1 下的配置,構建 Retry,這個 Retry 命名為 retry1
Retry retry1 = retryRegistry.retry("retry1", "test-client1");
//讀取 resilience4j.retry.configs.default 下的配置,構建 Retry,這個 Retry 命名為 retry1
//不指定配置名稱即使用預設的 default 下的配置
Retry retry2 = retryRegistry.retry("retry2");

引入 resilience4j-spring-cloud2 的依賴,就相當於引入了 resilience4j-spring-boot2 的依賴。並在其基礎上,針對 spring-cloud-config 的動態重新整理 RefreshScope 機制,增加了相容。

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-cloud2</artifactId>
</dependency>

使用 resilience4j-feign 給 OpenFeign 新增重試

官方提供了粘合 OpenFeign 的依賴庫,即 resilience4j-feign

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-feign</artifactId>
</dependency>

接下來,我們使用這個依賴,給 OpenFeign 新增重試,首先啟用 OpenFeign Client 並指定預設配置:

OpenFeignAutoConfiguration

@EnableFeignClients(value = "com.github.jojotech", defaultConfiguration = DefaultOpenFeignConfiguration.class)

在這個預設配置中,通過覆蓋預設的 Feign.Builder 的方式粘合 resilience4j 新增重試:

@Bean
public Feign.Builder resilience4jFeignBuilder(
        List<FeignDecoratorBuilderInterceptor> feignDecoratorBuilderInterceptors,
        FeignDecorators.Builder builder
) {
    feignDecoratorBuilderInterceptors.forEach(feignDecoratorBuilderInterceptor -> feignDecoratorBuilderInterceptor.intercept(builder));
    return Resilience4jFeign.builder(builder.build());
}

@Bean
public FeignDecorators.Builder defaultBuilder(
        Environment environment,
        RetryRegistry retryRegistry
) {
    String name = environment.getProperty("feign.client.name");
    Retry retry = null;
    try {
        retry = retryRegistry.retry(name, name);
    } catch (ConfigurationNotFoundException e) {
        retry = retryRegistry.retry(name);
    }

    //覆蓋其中的異常判斷,只針對 feign.RetryableException 進行重試,所有需要重試的異常我們都在 DefaultErrorDecoder 以及 Resilience4jFeignClient 中封裝成了 RetryableException
    retry = Retry.of(name, RetryConfig.from(retry.getRetryConfig()).retryOnException(throwable -> {
        return throwable instanceof feign.RetryableException;
    }).build());

    return FeignDecorators.builder().withRetry(
            retry
    );
}

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章