Hoe Uber zich beschermt tegen retry-stormen
Introductie
Retry-stormen hebben historisch gezien een grote impact op de bedrijfsvoering en het vertrouwen in het merk. Hoewel het afstellen van retry-configuraties en het instellen van 'retry-budgetten' zinvolle mitigatie bieden op serviceniveau, worden deze handmatig geconfigureerd. Bovendien bieden ze geen inzicht in de cross-service amplificatie die wordt veroorzaakt door diepe afhankelijkheidsketens en fan-out patronen. Hierdoor is het moeilijk om de infrastructuur te beschermen tegen het domino-effect dat wordt veroorzaakt door een enkele service-outage dieper in de stack.
Een belangrijke reden hiervoor is dat retry-gedrag vandaag de dag niet contextbewust is. Hoewel we kunnen controleren hoeveel retries er plaatsvinden, kunnen we niet precies controleren wanneer ze plaatsvinden. Dit komt door de uitdaging om betrouwbaar onderscheid te maken tussen fouten die door een service worden gegenereerd en fouten die simpelweg door die service worden doorgegeven.
Als gevolg hiervan worden retries uniform toegepast in plaats van conditioneel.
Deze aanpak werkt bij kortstondige storingen of lage foutpercentages. Echter, tijdens matige of ernstige degradatie wordt dit contraproductief. Agressief opnieuw proberen (retrying) tegen een service die al worstelt, verhoogt de belasting, versnelt het falen en versterkt het retry-verkeer over upstream-afhankelijkheden. Wat begint als een lokale outage, kan snel escaleren tot een incident in de hele stack, waardoor de gebruikerservaring verslechtert of in het ergste geval volledig wordt doorbroken.
Men zou kunnen aanvoeren dat foutcodes van downstream-services upstream vertaald zouden kunnen worden om context te bieden voor retries. Hoewel dit theoretisch mogelijk is, schaalt deze aanpak niet bij Uber vanwege de grote fan-in en fan-out, evoluerende call-flows en de noodzaak voor frequente adaptieve wijzigingen. Daarom hebben we een contextbewust mechanisme ontwikkeld in de gedeelde infrastructuur om fouten efficiënter af te handelen. Deze blog legt dat mechanisme uit.
Achtergrond
Beschouw een eenvoudige aanroepketen (call chain) zoals getoond in Figuur 1, waarbij het totale aantal verzoeken dat bij Node A arriveert $\eta$ is. Bij deductie bedienen alle nodes (B, C, D, E, F en G) $\eta$ verzoeken in de stabiele toestand (wanneer geen enkele node een fout geeft).
Figuur 1: Aanroepketen met 1:1 fan-out, waarbij een node zijn downstream precies één keer aanroept voor elk inkomend verzoek.
Als service D fouten begint te geven en elke service is geconfigureerd om één keer opnieuw te proberen (1 reguliere poging en een tweede poging als de downstream faalt), dan ziet het totale aantal verzoeken dat door elke node wordt afgehandeld er als volgt uit:
Figuur 2: Aanroepketen waarbij een service fouten geeft.
| Node | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| Diepte | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| Verzoeken afgehandeld | $\eta$ | $2 \times \eta$ | $4 \times \eta$ | $8 \times \eta$ | $8 \times \eta$ | $8 \times \eta$ | $8 \times \eta$ |
Dit kan worden teruggebracht tot een eenvoudige formule, uitgaande van het feit dat het aantal retries $R$ bij elke stap gelijk is. Hierbij staat $d$ voor de diepte van de node in de aanroepketen en is het aantal verzoeken dat door de node wordt afgehandeld als de node een fout creëert of doorgeeft:
$R^d \times \eta$
Retry-budgetten
We kunnen dit optimaliseren door retry-budgetten te introduceren. Stel dat we bij elke stap hetzelfde retry-budget $B$ hanteren. De nieuwe formule wordt:
$(1+B)^d \times \eta$
Laten we kijken naar het aantal verzoeken dat wordt afgehandeld met een retry-budget van 10%:
| Node | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| Diepte | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| Verzoeken afgehandeld | $\eta$ | $1.1 \times \eta$ | $1.21 \times \eta$ | $1.33 \times \eta$ | $1.33 \times \eta$ | $1.33 \times \eta$ | $1.33 \times \eta$ |
In het bovenstaande voorbeeld ontstaat de fout bij Node D. Als we de retry beperken tot alleen tussen Node D en Node C, en Node A en Node B volledig verbieden om op deze fout te reageren, kunnen we een vergelijkbare beschikbaarheid van het aanroeppad garanderen zonder Node D, E, F en G te overbelasten.
Error Ownership (Fout-eigenaarschap)
Beschouw hetzelfde voorbeeld van retry-budgetten, maar waarbij we retries beperken tot de verbinding tussen Node C en Node D, waar de fout ontstaat.
| Node | A | B | C | D | E | F | G |
|---|---|---|---|---|---|---|---|
| Diepte | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| Verzoeken afgehandeld | $\eta$ | $\eta$ | $\eta$ | $1.1 \times \eta$ | $1.1 \times \eta$ | $1.1 \times \eta$ | $1.1 \times \eta$ |
Hier beperken we het totale aantal verzoeken dat door alle nodes van D tot de leaf-node G wordt afgehandeld tot slechts 10% boven de baseline, terwijl we ten minste één retry toestaan voor maximaal 10% van de fouten wanneer ze voor het eerst worden geretourneerd. Maar hoe zit het met de beschikbaarheid van Node D zoals gezien door Node C? Laten we some getallen bekijken voor verschillende beschikbaarheidscenario's.
| Basis beschikbaarheid % | Basis foutpercentage % | Retry Budget | Foutpercentage na retries % | Beschikbaarheid na retries % |
|---|---|---|---|---|
| 99.9 | 0.1 | 10% | 0.0001 | 99.9999 |
| 99 | 1 | 10% | 0.01 | 99.99 |
| 95 | 5 | 10% | 0.25 | 99.75 |
| 90 | 10 | 10% | 1 | 99 |
| 80 | 20 | 10% | 12 | 88 |
| 70 | 30 | 10% | 23 | 77 |
Zoals in de tabel te zien is, helpt zelfs een enkele retry bij beschikbaarheidsdalingen tot 10% in de callee-node om de waargenomen beschikbaarheid voor de caller-node op 99% te houden. Daarboven daalt de waargenomen beschikbaarheid aanzienlijk, omdat een groot deel van de verzoeken nooit wordt herhaald vanwege het ingestelde retry-budget.
Deze berekening gaat ervan uit dat de fouten van de callee onafhankelijk zijn en dat retries leiden tot herstel. In veel real-world scenario's, zoals service-overbelasting, defecte database-hosts of sharding-problemen, blijft de kans op fouten echter hoog, zelfs bij retries.
Dit spreekt het idee tegen dat retries naar een callee altijd de waargenomen beschikbaarheid voor de caller verhogen. Dit inzicht vormt de basis voor error ownership. Tijdens perioden met hoge foutpercentages bij een service zijn de fouten minder waarschijnlijk willekeurig en is er geen baat bij een hoger aantal retries; sterker nog, ze kunnen verantwoordelijk zijn voor verdere degradatie.
Architectuur
De oplossing draait om het vaststellen van fout-eigenaarschap (error ownership), wat kan worden uitgelegd met de analogie van symptoom versus oorzaak.
Als een service N outbound-aanroepen doet om een verzoek te voltooien, en een outbound-fout ervoor zorgt dat de service een fout teruggeeft, dan is de fout die door die service wordt geretourneerd slechts een symptoom. Tegelijkertijd is, vanuit het perspectief van de service, de oorzaak de inkomende fout van zijn downstream.
Echter, als geen enkele outbound-aanroep van de service een fout geeft tijdens het voltooien van het verzoek, maar de service toch een fout teruggeeft, dan is de service de oorzaak van de geretourneerde fout en is hij de eigenaar.
Eenvoudige correlatie
Het claimen van fout-eigenaarschap We gebruiken de Service Dependency Analysis Solution om een inbound-fout te correleren met een outbound-fout en gebruiken een set regels (Figuur 3) om foutclaims te maken of te weigeren.
Figuur 3: Beslissingslogica voor het claimen van fout-eigenaarschap.
Retrying met fout-eigenaarschap De caller gebruikt de logica in Figuur 4 om te bepalen of het verzoek opnieuw moet worden geprobeerd.
Figuur 4: Beslissingslogica voor het toestaan van retries.
Een scenario waarin fout-claim-headers ontbreken, kan voorkomen in een niet-coöperatieve omgeving, bijvoorbeeld omdat de downstream-service de service dependency analysis solution niet heeft en outbound- en inbound-fouten niet kan correleren, of omdat er contextpropagatie ontbreekt.
In dit geval zal de eerste node die een ontbrekende fout-claim van een downstream ziet, de fout 'unclaimen'. Dit beperkt de impactstraal van de retry-verstoring (het is geen storm meer), terwijl er nog steeds voldoende retries worden toegestaan naar de service die de fout teruggeeft.
Beslissingsmatrix
| Callee Fout | Caller Fout | Callee Error Claim | Caller Should Retry (Retry Middleware) | Caller Propagated Error Claim |
|---|---|---|---|---|
| Nee | Ja | N.V.T. | N.V.T. | Claim |
| Ja | Ja | Missing | Ja | Unclaim |
| Ja | Ja | Claimed | Ja | Unclaim |
| Ja | Ja | Unclaimed | Nee | Unclaim |
Figuur 5: 3 nodes gebruikt om caller-fouten te demonstreren. Wanneer de verbindingen A → B en B → C 'fail-close' zijn, en Node C een interne fout teruggeeft die geclaimd is, moet Node B bij het zien van de geclaimde fout van C een retry doen naar C. Als de retry faalt, propageert Node B de fout naar Node A, maar moet hij de fout 'unclaimen' bij het teruggeven. Node A, bij het zien van de fout van B en de 'unclaimed' error header, mag het verzoek naar Node B niet opnieuw proberen.
Figuur 6: Beslissingslogica voor Error claim propagatie.
Toevallige fouten en de noodzaak van Service Dependency Analysis
De bovenstaande beslissingsmatrix dekt gevallen waarbij de downstream-aanroep faalt of er een interne serverfout is. Er kunnen echter scenario's zijn waarin beide gelijktijdig gebeuren (Figuur 7).
Figuur 7: Voorbeeld waarbij Node A twee 'fail-open' afhankelijkheden heeft, Node B en Node C.
In dit voorbeeld ervaart Node A ook een foutpercentage van 10% vanwege een overbelaste cache/database die niet wordt gevolgd via de service dependency analysis solution. Dit maakt het een interne serverfout van Node A, terwijl Nodes B en C om ongerelateerde redenen ook een foutpercentage van 10% hebben.
De service dependency analysis solution probeert fouten eerst toe te schrijven aan downstreams en pas als laatste aan zichzelf. Zelfs wanneer Nodes B en C 'fail-open' zijn, wijst het systeem de schuld aan hen wanneer de fouten gelijktijdig optreden. Dit 'unclaimt' de fout en voorkomt dat de upstream van Node A opnieuw naar Node A probeert, wat herstel zou kunnen belemmeren.
De impact van gemiste legitieme retry-kansen in zo'n scenario is significant. Als Node A 100 verzoeken afhandelt, waarvan er 10 interne serverfouten ervaren die bij Node A ontstaan, zouden 1,9 van die 10 verzoeken onterecht door Node A worden 'unclaimed', wat resulteert in een gemiste legitieme retry-kans.
Op basis van een analyse van zes maanden aan data is gebleken dat toevallige fouten (zoals een legitieme serverfout tijdens een fail-open dependency fout) uiterst zeldzaam zijn. In het ergste scenario, voor 80% van de verbindingen met meer dan 100 callee-fouten per minuut, faalde de caller in ongeveer 2% van de gevallen ook.
Zelfs in het slechtste geval kan de service dependency analysis solution, omdat deze een geheugen van foutpatronen opbouwt, dit geheugen gebruiken om fouten alleen te 'unclaimen' wanneer inbound-fouten correleren met outbound-fouten in fail-close afhankelijkheden. Dit elimineert het risico van retry-onderdrukking bij toevallige fouten.
Figuur 8: Beslissingslogica voor het bepalen wanneer er opnieuw geprobeerd moet worden.
Edge Case Scenario's
Garanderen van 'At-Least-Once' Retries
Veel services hebben geen retries geconfigureerd voor hun fail-close outbounds. Het elimineren van retries van de callers van deze services zou leiden tot een daling in de beschikbaarheid in de inkomende caller-keten.
Dit wordt opgelost door een vlag te introduceren die aangeeft of aan de retry-criteria is voldaan voor een fout die door een downstream wordt geretourneerd. Wanneer de retry-middleware een fout van de downstream ziet, kan deze berekenen of aan de retry-criteria is voldaan en dit doorgeven aan de service dependency analysis solution.
Figuur 9: Retry-logica om at-least-once retries te garanderen.
De Retry Middleware markeert de retry-criteria als voldaan als aan een van de condities in Figuur 10 is voldaan.
Figuur 10: Beslissingslogica om te bepalen of aan de retry-criteria is voldaan.
De service dependency analysis solution maakt gebruik van dit signaal om bij de inbound-aanroep te beslissen of hij de fout die door de fail-close downstream wordt geretourneerd, moet claimen.
Figuur 11: Beslissingslogica voor service-fout-eigenaarschap. Figuur 12: End-to-end flow.
We introduceren geen nieuwe retries in de aanroepketen. Het at-least-once-retry gedrag werkt alleen als ten minste één service in de aanroepketen retries heeft geconfigureerd. Door deze garantie te introduceren, hebben we beschikbaarheidsdalingen in onze aanroepketens geëlimineerd, terwijl we ze tegelijkertijd hebben beschermd tegen retry-stormen via fout-eigenaarschap.
Afhandeling van Context-verlies (Context Drop)
Figuur 13: 4 nodes gebruikt om context-drop afhandeling te demonstreren.
Voor de service dependency analysis solution en fout-eigenaarschap verschuift het verlies van intra-service context de retrybare fout naar links. Beschouw het voorbeeld in Figuur 14, waarbij Node D een interne fout teruggeeft aan Node C. De intra-service context in Node C is gebroken, waardoor de service dependency analysis solution de outbound-aanvraag van Node C naar D niet kan correleren met de inkomende aanvraag van Node B naar C.
Als gevolg hiervan claimt Node C de fout, terwijl ontkoppelde retries in de retry-middleware blijven plaatsvinden tussen Node C en D. Er vinden ook retries plaats tussen Node B en Node C, omdat Node C de fout die hij teruggeeft moet claimen. Echter, wanneer de retries falen en Node B een fout teruggeeft aan Node A, geeft hij ook de 'unclaimed' error header terug, wat Node A zou moeten tegenhouden om het verzoek naar Node B opnieuw te proberen. In dit geval verminderen we het totale aantal verzoeken naar Nodes B tot D met de helft, uitgaande van een configuratie van één retry bij alle nodes. Als er vijf nodes links van Node A zouden zijn, zou het ergste scenario zonder fout-eigenaarschap-propagatie 32 keer meer verzoeken hebben gegenereerd.
Figuur 14: Beschrijving van context-verlies.
Fout-eigenaarschap in Productie
Alles wat tot nu toe is beschreven is niet theoretisch; fout-eigenaarschap is volledig geïmplementeerd en operationeel in de service mesh van Uber. Het schema draait in de retry-middleware en de Service Dependency Analysis Solution in het pad van onze gebruikers-API's. Omdat het is ingebed in gedeelde infrastructuur, erven services bescherming tegen retry-stormen zonder dat er specifieke foutafhandelingslogica per service nodig is.
Use Cases bij Uber
Op 18 november 2025 had Uber een grote outage door een probleem met een Core Entity service. Deze service bevindt zich vijf niveaus diep in onze aanroepketen en is cruciaal voor de bedrijfsvoering. Door een onderliggend infrastructuurprobleem begon de service een zeer hoog foutpercentage terug te geven, wat snel werd gepropageerd naar boven in de keten.
Tijdens deze periode hadden veel upstream-caller-services de gelegenheid om de mislukte verzoeken opnieuw te proberen. Met eenvoudige retry-budgetten zou dit hebben geresulteerd in een toename van het verkeer op de gedegradeerde service van 46% tot 135%, waardoor de outage zou worden verlengd door de kans op herstel te verkleinen. Omdat fout-eigenaarschap al was ingeschakeld, werd de blast radius automatisch beperkt.
Directe retry-pogingen naar de directe callers van de gedegradeerde service werden gestopt, waarbij sommige callers werden verhinderd tot wel 200.000 extra verzoeken te doen. We berekenden ook de retries die werden gestopt bij de voorouders van deze directe callers en aggregeerden deze bij de root-node. We ontdekten dat we maar liefst 9,5 miljoen overbodige verzoeken in onze service mesh konden stoppen. Deze hadden de outage gemakkelijk kunnen verlengen door de gedegradeerde service constant te bestoken.
Conclusie
Onze aanpak heeft het totale volume aan verzoeken dat door de aanroepgrafiek stroomt tijdens degradatie-events drastisch verminderd.
Om dit te kwantificeren, definiëren we de maximale "retry-storm straal" na fout-eigenaarschap als de maximale diepte van het aanroeppad waar een retry-storm zou kunnen plaatsvinden. Voor al onze gebruikers-API's hebben we deze waarde verlaagd naar een maximum van 3, terwijl de eerdere waarde tot 25 liep. Het gemiddelde is gedaald van 20 naar 2.
Figuur 15: Maximale retry-storm straal na fout-eigenaarschap.
Door retries uitsluitend te beperken tot services die het eigenaarschap van de fout hebben — en waar ze het probleem daadwerkelijk kunnen oplossen — kunnen we de exponentiële fan-out van verzoeken die kenmerkend is voor retry-stormen voorkomen. Dit heeft geholpen de infrastructuur van Uber te beschermen tegen cascaderende uitval veroorzaakt door degradaties op één enkel punt.
Groetjes,