Bessere Wiederholungen mit exponentiellem Backoff und Jitter

1. Übersicht

In diesem Tutorial werden wir untersuchen, wie wir Client-Wiederholungsversuche mit zwei verschiedenen Strategien verbessern können: exponentielles Backoff und Jitter.

2. Wiederholen Sie den Vorgang

In einem verteilten System kann die Netzwerkkommunikation zwischen den zahlreichen Komponenten jederzeit fehlschlagen. Clientanwendungen beheben diese Fehler, indem sie Wiederholungsversuche implementieren .

Nehmen wir an, wir haben eine Clientanwendung, die einen Remotedienst aufruft - den PingPongService .

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

Die Clientanwendung muss es erneut versuchen, wenn der PingPongService eine PingPongServiceException zurückgibt . In den folgenden Abschnitten werden Möglichkeiten zum Implementieren von Client-Wiederholungsversuchen beschrieben.

3. Resilience4j Wiederholen

In unserem Beispiel verwenden wir die Resilience4j-Bibliothek, insbesondere das Wiederholungsmodul. Wir müssen das Resilience4j-Retry-Modul zu unserer pom.xml hinzufügen :

 io.github.resilience4j resilience4j-retry 

Vergessen Sie nicht, unseren Leitfaden zu Resilience4j zu lesen, um sich über die Verwendung von Wiederholungsversuchen zu informieren.

4. Exponentielles Backoff

Clientanwendungen müssen Wiederholungsversuche verantwortungsbewusst implementieren. Wenn Clients fehlgeschlagene Anrufe wiederholen, ohne zu warten, können sie das System überfordern und zu einer weiteren Verschlechterung des bereits in Not geratenen Dienstes beitragen.

Exponentielles Backoff ist eine gängige Strategie für die Behandlung von Wiederholungsversuchen fehlgeschlagener Netzwerkanrufe. In einfachen Worten, die Clients warten zunehmend längere Intervalle zwischen aufeinanderfolgenden Wiederholungsversuchen :

wait_interval = base * multiplier^n 

wo,

  • Basis ist das Anfangsintervall, dh warten Sie auf den ersten erneuten Versuch
  • n ist die Anzahl der aufgetretenen Fehler
  • Der Multiplikator ist ein beliebiger Multiplikator, der durch einen beliebigen geeigneten Wert ersetzt werden kann

Mit diesem Ansatz bieten wir dem System eine Atempause, um sich von zeitweiligen Ausfällen oder noch schwerwiegenderen Problemen zu erholen.

Wir können den exponentiellen Backoff-Algorithmus in Resilience4j-Wiederholungsversuchen verwenden, indem wir seine IntervalFunction konfigurieren , die ein initialInterval und einen Multiplikator akzeptiert .

Die IntervalFunction wird vom Wiederholungsmechanismus als Schlaffunktion verwendet:

IntervalFunction intervalFn = IntervalFunction.ofExponentialBackoff(INITIAL_INTERVAL, MULTIPLIER); RetryConfig retryConfig = RetryConfig.custom() .maxAttempts(MAX_RETRIES) .intervalFunction(intervalFn) .build(); Retry retry = Retry.of("pingpong", retryConfig); Function pingPongFn = Retry .decorateFunction(retry, ping -> service.call(ping)); pingPongFn.apply("Hello"); 

Lassen Sie uns ein reales Szenario simulieren und davon ausgehen, dass mehrere Clients gleichzeitig den PingPongService aufrufen :

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

Schauen wir uns die Remote- Aufrufprotokolle für NUM_CONCURRENT_CLIENTS gleich 4 an:

[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 

Wir können hier ein klares Muster erkennen - die Clients warten auf exponentiell wachsende Intervalle, aber alle rufen den Remote-Service bei jedem erneuten Versuch (Kollisionen) genau zur gleichen Zeit an.

Wir haben nur einen Teil des Problems behoben - wir hämmern den Remote-Service nicht mehr mit Wiederholungsversuchen, sondern anstatt die Arbeitslast über die Zeit zu verteilen, haben wir Arbeitsperioden mit mehr Leerlaufzeit unterbrochen. Dieses Verhalten ähnelt dem Problem der donnernden Herde.

5. Jitter vorstellen

In unserem vorherigen Ansatz sind die Wartezeiten des Clients zunehmend länger, aber immer noch synchronisiert. Das Hinzufügen von Jitter bietet eine Möglichkeit, die Synchronisation zwischen den Clients zu unterbrechen und so Kollisionen zu vermeiden . Bei diesem Ansatz fügen wir den Warteintervallen Zufälligkeit hinzu.

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

Dabei wird random_interval hinzugefügt (oder subtrahiert), um die Synchronisation zwischen den Clients zu unterbrechen .

Wir werden nicht auf die Mechanik der Berechnung des Zufallsintervalls eingehen, aber die Randomisierung muss die Spitzen ausgleichen, um eine viel gleichmäßigere Verteilung der Clientanrufe zu erreichen.

Wir können das exponentielle Backoff mit Jitter in Resilience4j-Wiederholungsversuchen verwenden, indem wir eine exponentielle zufällige Backoff- IntervalFunktion konfigurieren , die auch einen Randomisierungsfaktor akzeptiert :

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

Kehren wir zu unserem realen Szenario zurück und betrachten die Remote-Aufrufprotokolle mit Jitter:

[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

Jetzt haben wir eine viel bessere Verbreitung. Wir haben sowohl Kollisionen als auch Leerlaufzeiten eliminiert und haben eine nahezu konstante Rate an Kundenanrufen , abgesehen von dem anfänglichen Anstieg.

Hinweis: Wir haben das Intervall zur Veranschaulichung überbewertet und in realen Szenarien hätten wir kleinere Lücken.

6. Fazit

In diesem Tutorial haben wir untersucht, wie wir verbessern können, wie Clientanwendungen fehlgeschlagene Anrufe wiederholen, indem wir das exponentielle Backoff mit Jitter erweitern.

Der Quellcode für die im Tutorial verwendeten Beispiele ist auf GitHub verfügbar.