Better Retries with Exponential Backoff and Jitter

1. Oversikt

I denne opplæringen vil vi utforske hvordan vi kan forbedre klientforsøk med to forskjellige strategier: eksponentiell backoff og jitter.

2. Prøv på nytt

I et distribuert system kan nettverkskommunikasjon mellom de mange komponentene mislykkes når som helst. Kundesøknader takler disse feilene ved å implementere demprøver på nytt.

La oss anta at vi har et klientprogram som påkaller en ekstern tjeneste - PingPongService.

grensesnitt PingPongService {Stringanrop (String ping) kaster PingPongServiceException; }

Kundesøknaden må prøve på nytt hvis PingPongService returnerer a PingPongServiceException. I de følgende avsnittene vil vi se på måter å implementere klientforsøk på nytt.

3. Resilience4j Prøv på nytt

For vårt eksempel bruker vi Resilience4j-biblioteket, spesielt dets prøvemodul. Vi må legge til modulen resilience4j-retry i vår pom.xml:

 io.github.resilience4j resilience4j-prøv på nytt 

Ikke glem å sjekke ut vår guide til motstandsdyktighet4j for å få en oppdatering på hvordan du prøver på nytt.

4. Eksponentiell tilbakeslag

Kundesøknader må implementere forsøk på en ansvarlig måte. Når klienter prøver på nytt mislykkede samtaler uten å vente, kan de overvelde systemet, og bidra til ytterligere forringelse av tjenesten som allerede er under nød.

Eksponentiell tilbakeslag er en vanlig strategi for å håndtere forsøk på mislykkede nettverkssamtaler. For å si det enkelt, kundene venter gradvis lengre intervaller mellom påfølgende forsøk:

wait_interval = base * multiplikator ^ n 

hvor,

  • utgangspunkt er det opprinnelige intervallet, dvs. vent på det første forsøket på nytt
  • n er antall feil som har oppstått
  • multiplikator er en vilkårlig multiplikator som kan erstattes med en hvilken som helst passende verdi

Med denne tilnærmingen gir vi et pusterom til systemet for å komme seg etter periodiske feil, eller enda mer alvorlige problemer.

Vi kan bruke den eksponensielle backoff-algoritmen i Resilience4j på nytt ved å konfigurere dens IntervalFunction som godtar en initialInterval og en multiplikator.

De IntervalFunction brukes av forsøksmekanismen som en søvnfunksjon:

IntervalFunction intervalFn = IntervalFunction.ofExponentialBackoff (INITIAL_INTERVAL, MULTIPLIER); RetryConfig retryConfig = RetryConfig.custom () .maxAttempts (MAX_RETRIES) .intervalFunction (intervalFn) .build (); Prøv på nytt = Prøv på nytt.of ("pingpong", prøv på nyttConfig); Funksjon pingPongFn = Prøv på nytt. DekorerFunksjon (prøv på nytt, ping -> service.call (ping)); pingPongFn.apply ("Hei"); 

La oss simulere et virkelig scenario, og anta at vi har flere kunder som påkaller PingPongService samtidig:

ExecutorService executors = newFixedThreadPool (NUM_CONCURRENT_CLIENTS); Listeoppgaver = nKopier (NUM_CONCURRENT_CLIENTS, () -> pingPongFn.apply ("Hei")); executors.invokeAll (oppgaver); 

La oss se på de eksterne påkallingsloggene for NUM_CONCURRENT_CLIENTS lik 4:

[tråd-1] Kl. 00: 37: 42.756 [tråd-2] Kl. 00: 37: 42.756 [tråd-3] Kl. 00: 37: 42.756 [tråd-4] Kl. 00: 37: 42.756 [tråd-2] Kl. 00: 37: 43.802 [tråd-4] Kl. 00: 37: 43.802 [tråd-1] Kl. 00: 37: 43.802 [tråd-3] Ved 00: 37: 43.802 [tråd-2] Kl. 00: 37: 45.803 [ tråd-1] Kl. 00: 37: 45.803 [tråd-4] Kl. 00: 37: 45.803 [tråd-3] Kl. 00: 37: 45.803 [tråd-2] Kl. 00: 37: 49.808 [tråd-3] Kl. 00 : 37: 49.808 [tråd-4] Kl. 00: 37: 49.808 [tråd-1] Kl. 00: 37: 49.808 

Vi kan se et tydelig mønster her - klientene venter på eksponentielt voksende intervaller, men alle ringer fjerntjenesten nøyaktig samtidig ved hvert nytt forsøk (kollisjoner).

Vi har tatt opp bare en del av problemet - vi hamrer ikke fjerntjenesten med forsøk lenger, men i stedet for å spre arbeidsmengden over tid, har vi ispedd perioder med arbeid med mer inaktiv tid. Denne oppførselen ligner på Thundering Herd Problem.

5. Vi presenterer Jitter

I vår forrige tilnærming er klientens ventetid gradvis lenger, men fortsatt synkronisert. Å legge til jitter gir en måte å bryte synkroniseringen på tvers av klientene og dermed unngå kollisjoner. I denne tilnærmingen legger vi tilfeldighet til venteintervallene.

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

hvor, tilfeldig_intervall blir lagt til (eller trukket) for å bryte synkroniseringen på tvers av klienter.

Vi går ikke inn i mekanikken for å beregne det tilfeldige intervallet, men randomisering må plassere toppene til en mye jevnere fordeling av klientsamtaler.

Vi kan bruke eksponentiell backoff med jitter i Resilience4j på nytt ved å konfigurere en eksponentiell random backoff IntervalFunction som også aksepterer a randomizationFactor:

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

La oss gå tilbake til vårt virkelige scenario, og se på eksterne påkallingslogger med 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] Ved 39: 21.868 [tråd-4] Ved 39: 22.011 [tråd-1] Ved 39: 22.184 [tråd-1] Ved 39: 23.086 [tråd-5] Ved 39: 23.939 [tråd-3] Ved 39: 24.152 [ tråd-4] Ved 39: 24.977 [tråd-3] Ved 39: 26.861 [tråd-1] Ved 39: 28.617 [tråd-4] Ved 39: 28.942 [tråd-2] Ved 39: 31.039

Nå har vi fått en mye bedre spredning. Vi har eliminert både kollisjoner og inaktiv tid, og ender opp med en nesten konstant frekvens av klientsamtaler, sperring av den opprinnelige bølgen.

Merk: Vi har overvurdert intervallet for illustrasjon, og i virkelige scenarier vil vi ha mindre hull.

6. Konklusjon

I denne veiledningen har vi utforsket hvordan vi kan forbedre hvordan klientapplikasjoner prøver mislykkede anrop på nytt ved å øke eksponentiell tilbakeslag med jitter.

Kildekoden for prøvene som brukes i opplæringen, er tilgjengelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found