需要重試的場景
微服務系統中,會遇到線上釋出,一般的釋出更新策略是:啟動一個新的,啟動成功之後,關閉一箇舊的,直到所有的舊的都被關閉。Spring Boot 具有優雅關閉的功能,可以保證請求處理完再關閉,同時會拒絕新的請求。對於這些拒絕的請求,為了保證使用者體驗不受影響,是需要重試的。
雲上部署的微服務,對於同一個服務,同一個請求,很可能不會所有例項都同時異常,例如:
- Kubernetes 叢集部署的例項,可能同一個虛擬機器 Node 在閒時部署了多個不同微服務例項,當壓力變大時,就需要遷移和擴容。這時候由於不同的微服務壓力不同,當時處於哪一個 Node 也說不定,有的可能處於壓力大的,有的可能處於壓力小的。對於同一個微服務,可能並不會所有例項位於的 Node 壓力都大。
- 雲上部署一般會跨可用區部署,如果有一個可用區異常,另一個可用區還可以繼續提供服務。
- 某個業務觸發了 Bug,導致例項一直在 GC,但是這種請求一般很不常見,不會發到所有例項上。
這時候,就需要我們對請求進行無感知的重試。
重試需要考慮的問題
- 重試需要重試與之前不同的例項,甚至是不處於同一個虛擬機器 Node 的例項,這個主要通過 LoadBalancer 實現,可以參考之前的 LoadBalancer 部分。後面的文章,我們還會改進 LoadBalancer
- 重試需要考慮到底什麼請求能重試,以及什麼異常能重試:
- 假設我們有查詢介面,和沒有做冪等性的扣款介面,那麼很直觀的就能感覺出查詢介面是可以重試的,沒有做冪等性的扣款介面是不能重試的。
- 業務上不能重試的介面,對於特殊的異常(其實是表示請求並沒有發出去的異常),我們是可以重試的。雖然是沒有做冪等性的扣款介面,但是如果丟擲的是原因是 Connect Timeout 的 IOException,這樣的異常代表請求還沒有發出去,是可以重試的。
- 重試策略:重試幾次,重試間隔。類比多處理器程式設計模式中的 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 並指定預設配置:
@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: