1. 概要

分散クラウド環境でアプリケーションを構築する場合、障害に備えて設計する必要があります。 多くの場合、これには再試行が含まれます。

Spring WebFluxは、失敗した操作を再試行するためのいくつかのツールを提供します。

このチュートリアルでは、SpringWebFluxアプリケーションに再試行を追加および構成する方法を見ていきます。

2. 使用事例

この例では、 MockWebServer を使用して、一時的に使用できなくなってから使用可能になる外部システムをシミュレートします。

このRESTサービスに接続するコンポーネントの簡単なテストを作成しましょう。

@Test
void givenExternalServiceReturnsError_whenGettingData_thenRetryAndReturnResponse() {

    mockExternalService.enqueue(new MockResponse()
      .setResponseCode(SERVICE_UNAVAILABLE.code()));
    mockExternalService.enqueue(new MockResponse()
      .setResponseCode(SERVICE_UNAVAILABLE.code()));
    mockExternalService.enqueue(new MockResponse()
      .setResponseCode(SERVICE_UNAVAILABLE.code()));
    mockExternalService.enqueue(new MockResponse()
      .setBody("stock data"));

    StepVerifier.create(externalConnector.getData("ABC"))
      .expectNextMatches(response -> response.equals("stock data"))
      .verifyComplete();

    verifyNumberOfGetRequests(4);
}

3. 再試行の追加

MonoおよびFluxAPIに組み込まれている2つの主要な再試行演算子があります。

3.1. 再試行を使用

まず、 try メソッドを使用してみましょう。これにより、アプリケーションがすぐにエラーを返さず、指定された回数だけ再サブスクライブします。

public Mono<String> getData(String stockId) {
    return webClient.get()
        .uri(PATH_BY_ID, stockId)
        .retrieve()
        .bodyToMono(String.class)
        .retry(3);
}

これにより、Webクライアントからどのようなエラーが返されても、最大3回再試行されます。

3.2. retryWhenを使用する

次に、restartWhenメソッドを使用して構成可能な戦略を試してみましょう。

public Mono<String> getData(String stockId) {
    return webClient.get()
        .uri(PATH_BY_ID, stockId)
        .retrieve()
        .bodyToMono(String.class)
        .retryWhen(Retry.max(3));
}

これにより、 Retry オブジェクトを構成して、目的のロジックを記述することができます。

ここでは、 max 戦略を使用して、最大試行回数まで再試行しました。 これは最初の例と同等ですが、より多くの構成オプションを使用できます。 特に、この場合、各再試行は可能な限り迅速に行われることに注意してください

4. 遅延の追加

遅延なしで再試行することの主な欠点は、これが失敗したサービスに回復する時間を与えないことです。 それはそれを圧倒し、問題を悪化させ、回復の可能性を減らすかもしれません。

4.1. fixedDelayで再試行します

fixedDelay 戦略を使用して、各試行の間に遅延を追加できます。

public Mono<String> getData(String stockId) {
    return webClient.get()
      .uri(PATH_BY_ID, stockId)
      .retrieve()
      .bodyToMono(String.class)
      .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(2)));
}

この構成では、試行の間に2秒の遅延が許可され、成功の可能性が高くなる可能性があります。 ただし、サーバーの停止時間が長くなる場合は、さらに長く待つ必要があります。 ただし、すべての遅延を長時間に設定すると、短いブリップによってサービスの速度がさらに低下します。

4.2. backoffで再試行します

一定の間隔で再試行する代わりに、backoff戦略を使用できます。

public Mono<String> getData(String stockId) {
    return webClient.get()
      .uri(PATH_BY_ID, stockId)
      .retrieve()
      .bodyToMono(String.class)
      .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)));
}

事実上、このは、試行間の遅延を徐々に増加させます —この例ではおよそ2、4、次に8秒間隔です。 このは、外部システムに、ありふれた接続の問題からを回復したり、作業のバックログを処理したりするためのより良い機会を提供します。

4.3. ジッターで再試行

backoff 戦略の追加の利点は、計算された遅延間隔にランダム性またはjitterを追加することです。 したがって、ジッターは、複数のクライアントがlockstepで再試行する再試行ストームを減らすのに役立ちます。

デフォルトでは、この値は0.5に設定されています。これは、計算された遅延の最大50% ofのジッターに対応します。

jitter メソッドを使用して、計算された遅延の最大75% ofのジッターを表す0.75の異なる値を構成してみましょう。

public Mono<String> getData(String stockId) {
    return webClient.get()
      .uri(PATH_BY_ID, stockId)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(String.class)
      .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)).jitter(0.75));
}

可能な値の範囲は、0(ジッターなし)から1(計算された遅延の最大100 % o fのジッター)の間であることに注意してください。

5. フィルタリングエラー

この時点で、サービスからのエラーは、 400:Bad Request401:Unauthorizedなどの4xxエラーを含む再試行につながります。

明らかに、サーバーの応答に違いはないため、このようなクライアントエラーを再試行しないでください。 したがって、特定のエラーの場合にのみ再試行戦略を適用する方法を見てみましょう。

まず、サーバーエラーを表す例外を作成しましょう。

public class ServiceException extends RuntimeException {
    
    public ServiceException(String message, int statusCode) {
        super(message);
        this.statusCode = statusCode;
    }
}

次に、5xxエラーを除いてエラー Mono を作成し、filterメソッドを使用して戦略を構成します。

public Mono<String> getData(String stockId) {
    return webClient.get()
      .uri(PATH_BY_ID, stockId)
      .retrieve()
      .onStatus(HttpStatus::is5xxServerError, 
          response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
      .bodyToMono(String.class)
      .retryWhen(Retry.backoff(3, Duration.ofSeconds(5))
          .filter(throwable -> throwable instanceof ServiceException));
}

これで、ServiceExceptionWebClientパイプラインでスローされた場合にのみ再試行します。

6. 使い果たされた再試行の処理

最後に、すべての再試行が失敗した可能性を説明できます。 この場合、ストラテジーによるデフォルトの動作は、 RetryExhaustedException を伝播し、最後のエラーをラップすることです。

代わりに、 onRetryExhaustedThrow メソッドを使用してこの動作をオーバーライドし、ServiceExceptionのジェネレーターを提供しましょう。

public Mono<String> getData(String stockId) {
    return webClient.get()
      .uri(PATH_BY_ID, stockId)
      .retrieve()
      .onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
      .bodyToMono(String.class)
      .retryWhen(Retry.backoff(3, Duration.ofSeconds(5))
          .filter(throwable -> throwable instanceof ServiceException)
          .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
              throw new ServiceException("External Service failed to process after max retries", HttpStatus.SERVICE_UNAVAILABLE.value());
          }));
}

これで、失敗した一連の再試行の最後に、ServiceExceptionでリクエストが失敗します。

7. 結論

この記事では、retryおよびretryWhenメソッドを使用してSpringWebFluxアプリケーションに再試行を追加する方法について説明しました。

最初に、失敗した操作の最大再試行回数を追加しました。 次に、さまざまな戦略を使用および構成することにより、試行間の遅延を導入しました。

最後に、特定のエラーを再試行し、すべての試行が終了したときの動作をカスタマイズすることを検討しました。

いつものように、完全なソースコードはGitHubから入手できます。