1. 概要

このチュートリアルでは、指数バックオフとジッターという2つの異なる戦略を使用して、クライアントの再試行を改善する方法について説明します。

2. リトライ

分散システムでは、多数のコンポーネント間のネットワーク通信がいつでも失敗する可能性があります。クライアントアプリケーションは、再試行を実装することでこれらの失敗に対処します。

リモートサービスPingPongServiceを呼び出すクライアントアプリケーションがあると仮定します。

interface PingPongService {
    String call(String ping) throws PingPongServiceException;
}

PingPongServicePingPongServiceExceptionを返した場合、クライアントアプリケーションは再試行する必要があります。 次のセクションでは、クライアントの再試行を実装する方法を見ていきます。

3. Resilience4j再試行

この例では、 Resilience4j ライブラリ、特にそのtryモジュールを使用します。 resilience4j-retryモジュールをpom.xmlに追加する必要があります。

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

再試行の使用に関する復習については、Resilience4jガイドを確認することを忘れないでください。

4. 指数バックオフ

クライアントアプリケーションは、責任を持って再試行を実装する必要があります。 クライアントが待機せずに失敗した呼び出しを再試行すると、システムを圧倒し、すでに悩んでいるサービスのさらなる低下に寄与する可能性があります。

指数バックオフは、失敗したネットワーク呼び出しの再試行を処理するための一般的な戦略です。 簡単に言うと、クライアントは連続する再試行の間隔を徐々に長くします

wait_interval = base * multiplier^n

どこ、

  • base は初期間隔です。つまり、最初の再試行を待ちます
  • n は、発生した障害の数です。
  • multiplier は、任意の適切な値に置き換えることができる任意の乗数です

このアプローチにより、断続的な障害やさらに深刻な問題から回復するための呼吸スペースをシステムに提供します。

initialIntervalmultiplierを受け入れるIntervalFunctionを構成することにより、Resilience4j再試行で指数バックオフアルゴリズムを使用できます。

IntervalFunction は、スリープ機能として再試行メカニズムによって使用されます。

IntervalFunction intervalFn =
  IntervalFunction.ofExponentialBackoff(INITIAL_INTERVAL, MULTIPLIER);

RetryConfig retryConfig = RetryConfig.custom()
  .maxAttempts(MAX_RETRIES)
  .intervalFunction(intervalFn)
  .build();
Retry retry = Retry.of("pingpong", retryConfig);

Function<String, String> pingPongFn = Retry
    .decorateFunction(retry, ping -> service.call(ping));
pingPongFn.apply("Hello");

実際のシナリオをシミュレートして、PingPongServiceを同時に呼び出すクライアントが複数あると仮定します。

ExecutorService executors = newFixedThreadPool(NUM_CONCURRENT_CLIENTS);
List<Callable> tasks = nCopies(NUM_CONCURRENT_CLIENTS, () -> pingPongFn.apply("Hello"));
executors.invokeAll(tasks);

4に等しいNUM_CONCURRENT_CLIENTSのリモート呼び出しログを見てみましょう。

[thread-1] At 00:37:42.756
[thread-2] At 00:37:42.756
[thread-3] At 00:37:42.756
[thread-4] At 00:37:42.756

[thread-2] At 00:37:43.802
[thread-4] At 00:37:43.802
[thread-1] At 00:37:43.802
[thread-3] At 00:37:43.802

[thread-2] At 00:37:45.803
[thread-1] At 00:37:45.803
[thread-4] At 00:37:45.803
[thread-3] At 00:37:45.803

[thread-2] At 00:37:49.808
[thread-3] At 00:37:49.808
[thread-4] At 00:37:49.808
[thread-1] At 00:37:49.808

ここで明確なパターンを見ることができます。クライアントは指数関数的に増加する間隔を待ちますが、すべてのクライアントは、再試行(衝突)ごとに正確に同時にリモートサービスを呼び出します。

問題の一部のみに対処しました。リモートサービスを再試行で槌で打つことはもうありませんが、時間の経過とともにワークロードを分散する代わりに、より多くのアイドル時間で作業期間を分散させました。この動作は雷の群れの問題に似ています。

5. ジッターの紹介

以前のアプローチでは、クライアントの待機時間は徐々に長くなりますが、それでも同期されます。 ジッターを追加すると、クライアント間の同期を解除して、衝突を回避する方法が提供されます。 このアプローチでは、待機間隔にランダム性を追加します。

wait_interval = (base * 2^n) +/- (random_interval)

ここで、 random_interval は、クライアント間の同期を解除するために加算(または減算)されます。

ランダム間隔の計算の仕組みについては説明しませんが、ランダム化では、スパイクの間隔を空けて、クライアント呼び出しをよりスムーズに分散させる必要があります。

randomizationFactorも受け入れる指数ランダムバックオフIntervalFunctionを構成することにより、Resilience4j再試行でジッター付きの指数バックオフを使用できます。

IntervalFunction intervalFn = 
  IntervalFunction.ofExponentialRandomBackoff(INITIAL_INTERVAL, MULTIPLIER, RANDOMIZATION_FACTOR);

実際のシナリオに戻り、ジッターのあるリモート呼び出しログを見てみましょう。

[thread-2] At 39:21.297
[thread-4] At 39:21.297
[thread-3] At 39:21.297
[thread-1] At 39:21.297

[thread-2] At 39:21.918
[thread-3] At 39:21.868
[thread-4] At 39:22.011
[thread-1] At 39:22.184

[thread-1] At 39:23.086
[thread-5] At 39:23.939
[thread-3] At 39:24.152
[thread-4] At 39:24.977

[thread-3] At 39:26.861
[thread-1] At 39:28.617
[thread-4] At 39:28.942
[thread-2] At 39:31.039

今、私たちははるかに良い広がりを持っています。 は衝突とアイドル時間の両方を排除し、最初の急増を除いて、ほぼ一定のクライアント呼び出しになります。

注:説明のために間隔を誇張しており、実際のシナリオでは、ギャップが小さくなります。

6. 結論

このチュートリアルでは、ジッターで指数バックオフを強化することにより、クライアントアプリケーションが失敗した呼び出しを再試行する方法を改善する方法を検討しました。

チュートリアルで使用されているサンプルのソースコードは、GitHubから入手できます。