指数関数的バックオフとジッターによるより良い再試行

1.  概要

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

2. リトライ

分散システムでは、多数のコンポーネント間のネットワーク通信がいつでも失敗する可能性があります。 *クライアントアプリケーションは、*再試行*を実装することにより、これらの障害に対処します。
リモートサービスであるclient_PingPongService_を呼び出すクライアントアプリケーションがあるとします。
interface PingPongService {
    String call(String ping) throws PingPongServiceException;
}
_PingPongService_が__PingPongServiceException_を返す場合、クライアントアプリケーションは再試行する必要があります。 次のセクションでは、クライアントの再試行を実装する方法を見ていきます。

3. Resilience4j再試行

この例では、https://www.baeldung.com/resilience4j [Resilience4j]ライブラリ、特にそのhttps://resilience4j.readme.io/docs/retry[retry] moduleを使用します。 「resilience4j-retry」モジュールを_pom.xml_に追加する必要があります。
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
</dependency>
再試行の使用に関する復習については、https://www.baeldung.com/resilience4j [Resilience4jのガイド]をご覧ください。

4. 指数バックオフ

クライアントアプリケーションは、責任を持って再試行を実装する必要があります。 *クライアントが待機せずに失敗したコールを再試行すると、システムを圧倒する可能性があり*、すでに苦しんでいるサービスのさらなる低下に寄与する可能性があります。
指数バックオフは、失敗したネットワークコールの再試行を処理するための一般的な戦略です。 簡単に言えば、*クライアントは連続する再試行の間隔を徐々に長くします*:
wait_interval = base * multiplier^n
どこで、
  • _base_は初期間隔です。つまり、最初の再試行を待機します

  • _n_は発生した障害の数です

  • _multiplier_は任意の乗数で、任意の乗数に置き換えることができます
    適切な値

    このアプローチでは、断続的な障害やさらに深刻な問題から回復するための呼吸空間をシステムに提供します。
    Resilience4jの再試行で指数バックオフアルゴリズムを使用するには、_initialInterval_と_multiplier_を受け入れる__IntervalFunction __を構成します。
    _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
ここでは明確なパターンを見ることができます-「クライアントは指数関数的に増加する間隔を待機しますが、すべてのクライアントがリトライ(衝突)のたびに正確にリモートサービスを呼び出します。 + link:/uploads/chart.png []
問題の一部のみに対処しました。リモートサービスを再試行でハンマーすることはもうありません。しかし、時間をかけてワークロードを分散する代わりに、アイドル時間を増やして作業期間を散在させます。*この動作はhttpsと類似しています://en.wikipedia.org/wiki/Thundering_herd_problem [Thundering Herd Problem]。

5. ジッタの紹介

以前のアプローチでは、クライアントの待機は徐々に長くなりますが、それでも同期されます。 *ジッターを追加すると、クライアント間で同期が解除され、衝突が回避されます*。 このアプローチでは、待機間隔にランダム性を追加します。
wait_interval = (base * 2^n) +/- (random_interval)
ここで、_random_interval_が追加(または減算)され、クライアント間で同期が中断されます。
「ランダムな間隔」を計算する仕組みには触れませんが、ランダム化によりスパイクを間隔を空けて、クライアントコールをよりスムーズに分散させる必要があります。
Resilience4jのリトライでは、_randomizationFactor_も受け入れる指数ランダムバックオフconfiguring__IntervalFunction __を設定することで、ジッターを伴う指数バックオフを使用できます。
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
今、私たちははるかに良いスプレッドを持っています。 衝突とアイドル時間の両方を排除し、最初の急増を除いて、クライアント呼び出しの割合はほぼ一定になりました。
link:/uploads/chart-1.png []
注:説明のために間隔を誇張しており、実際のシナリオでは、ギャップは小さくなります。

6. 結論

このチュートリアルでは、ジッターで指数関数的なバックオフを強化することにより、クライアントアプリケーションが失敗した呼び出しを再試行する方法を改善する方法を検討しました。 チュートリアルで使用されるサンプルのソースコードは、https://github.com/eugenp/tutorials/tree/master/patterns/backoff-jitter [GitHub]で入手できます。