Tentativi migliori con backoff esponenziale e jitter

1. Panoramica

In questo tutorial, esploreremo come migliorare i tentativi del client con due diverse strategie: backoff esponenziale e jitter.

2. Riprova

In un sistema distribuito, la comunicazione di rete tra i numerosi componenti può fallire in qualsiasi momento. Le applicazioni client gestiscono questi errori implementando nuovi tentativi .

Supponiamo di avere un'applicazione client che richiama un servizio remoto: PingPongService .

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

L'applicazione client deve riprovare se il PingPongService restituisce un PingPongServiceException . Nelle sezioni seguenti, esamineremo i modi per implementare i tentativi del client.

3. Resilience4j Retry

Per il nostro esempio, utilizzeremo la libreria Resilience4j, in particolare il suo modulo di ripetizione. Dovremo aggiungere il modulo resilience4j-retry al nostro pom.xml :

 io.github.resilience4j resilience4j-retry 

Per un aggiornamento sull'utilizzo dei tentativi, non dimenticare di consultare la nostra Guida a Resilience4j.

4. Backoff esponenziale

Le applicazioni client devono implementare i tentativi in ​​modo responsabile. Quando i clienti ritentano le chiamate non riuscite senza attendere, possono sovraccaricare il sistema e contribuire a un ulteriore degrado del servizio che è già in difficoltà.

Il backoff esponenziale è una strategia comune per la gestione dei tentativi di chiamate di rete non riuscite. In termini semplici, i client attendono intervalli progressivamente più lunghi tra i tentativi consecutivi :

wait_interval = base * multiplier^n 

dove,

  • base è l'intervallo iniziale, ovvero attendere il primo tentativo
  • n è il numero di errori che si sono verificati
  • moltiplicatore è un moltiplicatore arbitrario che può essere sostituito con qualsiasi valore adatto

Con questo approccio, forniamo al sistema uno spazio di respiro per riprendersi da guasti intermittenti o problemi anche più gravi.

Possiamo utilizzare l'algoritmo di backoff esponenziale in Resilience4j retry configurando la sua IntervalFunction che accetta un initialInterval e un moltiplicatore .

L'IntervalFunction viene utilizzato dal meccanismo di tentativo come una funzione sleep:

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"); 

Simuliamo uno scenario del mondo reale e supponiamo di avere diversi client che invocano simultaneamente PingPongService :

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

Diamo un'occhiata ai log delle chiamate remote per NUM_CONCURRENT_CLIENTS uguale a 4:

[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 

Possiamo vedere uno schema chiaro qui: i client aspettano intervalli crescenti in modo esponenziale, ma tutti chiamano il servizio remoto esattamente nello stesso momento ad ogni tentativo (collisioni).

Abbiamo affrontato solo una parte del problema: non martelliamo più il servizio remoto con nuovi tentativi, ma invece di distribuire il carico di lavoro nel tempo, abbiamo intervallato periodi di lavoro con più tempi di inattività. Questo comportamento è simile al problema della mandria tonante.

5. Introduzione a Jitter

Nel nostro approccio precedente, le attese del client sono progressivamente più lunghe ma ancora sincronizzate. L'aggiunta di jitter fornisce un modo per interrompere la sincronizzazione tra i client evitando così collisioni . In questo approccio, aggiungiamo casualità agli intervalli di attesa.

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

dove viene aggiunto (o sottratto) random_interval per interrompere la sincronizzazione tra i client.

Non entreremo nei meccanismi di calcolo dell'intervallo casuale, ma la randomizzazione deve distanziare i picchi per una distribuzione molto più uniforme delle chiamate dei client.

Possiamo usare il backoff esponenziale con jitter in Resilience4j retry configurando un IntervalFunction di backoff casuale esponenziale che accetta anche un fattore di randomizzazione :

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

Torniamo al nostro scenario del mondo reale e guardiamo i registri delle chiamate remote con 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

Ora abbiamo una diffusione molto migliore. Abbiamo eliminato sia le collisioni che i tempi di inattività e ci ritroviamo con un tasso quasi costante di chiamate dei clienti , salvo il picco iniziale.

Nota: abbiamo sovrastimato l'intervallo per l'illustrazione e, in scenari del mondo reale, avremmo lacune minori.

6. Conclusione

In questo tutorial, abbiamo esplorato come migliorare il modo in cui le applicazioni client ritentano le chiamate non riuscite aumentando il backoff esponenziale con jitter.

Il codice sorgente per gli esempi utilizzati nel tutorial è disponibile su GitHub.