Steven
Steven5 min leestijd

De dag dat onze app zichzelf DDoSte

Een achterstand aan wachtende uploads, in één keer losgelaten bij het opstarten, veranderde elke GeekBye-client in een kleine denial-of-service-aanval op onze eigen servers. De fix — en de connectie-liveness-ladder die hij ons dwong te bouwen — is een van de nuttigste dingen die v2 ons heeft geleerd.

Betrouwbaarheid
Netwerken
Engineering
GeekBye Releases
De dag dat onze app zichzelf DDoSte

Elke distributed-systems-engineer ontmoet vroeg of laat de thundering herd: een massa clients die allemaal op precies hetzelfde moment hetzelfde doen, en de gedeelde resource erachter die bezwijkt. Meestal zijn het andermans clients. In GeekBye v2.0.1 waren het de onze, en de aanvaller was onze eigen app.

Hoe een notulist zichzelf aanvalt

GeekBye kan opnames uploaden naar je Google Drive. Als een opname niet meteen kan uploaden — je was offline, de app sloot af, Drive haperde — komt hij in een achterstand om later opnieuw te proberen. Verstandig.

De fout zat in wanneer "later" gebeurde. Bij de volgende launch probeerde de app de hele achterstand in één keer te legen. Eén gebruiker met een week aan wachtende opnames werd een uitbarsting van gelijktijdige uploadverzoeken op het moment dat ze de app openden. Vermenigvuldig dat met elke gebruiker die 's ochtends opstart, en onze backend werd gevraagd om in dezelfde paar seconden een muur van verkeer te authenticeren, rate te checken en te verwerken — van clients die zich, technisch gezien, allemaal netjes gedroegen.

Onze backend deed het juiste en rate-limitte de vloed. En daar begon de tweede-orde-schade. Een rate-limit-antwoord is een HTTP 429, en 429 lijkt sterk op andere fouten als je niet oppast:

  • De profielfetch bij het opstarten kreeg een 429 en de app behandelde het als auth mislukt — waardoor gebruikers kortstondig werden uitgelogd uit een volkomen geldige sessie.
  • De transcriptieverbinding kreeg een generieke 429 en toonde een audiolimiet-bereikt-upgradeprompt — betalende gebruikers vertellen dat ze een quota hadden bereikt die ze niet hadden bereikt.

Zo produceerde één root cause — een ongedoseerde achterstand — drie zichtbare symptomen: serverbelasting, valse logouts en valse upsells. De klassieke vorm van een self-DoS: de belasting is slecht, maar het is de misinterpretatie van de symptomen van de belasting die gebruikers daadwerkelijk voelen.

De fix, in twee lagen

Stop de stampede. De uploadachterstand wordt nu gedoseerd — verzoeken worden gespreid met backoff, en een cooldown na elke 429 zodat de client zich terugtrekt van een belaste server in plaats van er harder tegenaan te leunen. Een thundering herd wordt een ordelijke wachtrij.

Stop de misinterpretatie. Een tijdelijke 429 of 5xx tijdens het opstarten logt je niet langer uit — de client onderscheidt "de server is even druk" van "je sessie is ongeldig." En een generieke rate-limit-429 op het transcriptiepad doet zich niet langer voor als een audio-quotafout; alleen een echt quota-antwoord toont de upgradeprompt. De les die bleef hangen: een foutcode is geen foutbetekenis. 429 betekent "vertraag," niet "je bent niet geautoriseerd" en niet "je bent door je quota heen" — en de client moet het verschil kennen.

De liveness-ladder die volgde

Het doseren van de herd legde een stiller probleem bloot: als een verbinding onder belasting wél slecht ging, hoe snel merkten we het dan? Trager dan we wilden. Dus bouwde v2.0.4 een connectie-liveness-ladder uit voor real-time transcriptie:

  • Heartbeats — de transcriptiesocket wordt op een vast interval gepingd, zodat een stilzwijgend dode verbinding in seconden wordt gedetecteerd in plaats van te wachten tot de volgende chunk audio faalt.
  • Een netwerk-terug-herverbindingsschop — wanneer het OS meldt dat het netwerk terug is, herstelt de app proactief de verbinding in plaats van te wachten om er per ongeluk achter te komen.
  • Control ping/pong-liveness — een round-trip op applicatieniveau die niet alleen bevestigt "de socket is open" maar "de andere kant antwoordt daadwerkelijk."
  • Een service-eigen herverbindingspoort — één plek beslist of er herverbonden wordt, in plaats van meerdere delen van de app die met elkaar racen om het te doen en elkaar voor de voeten lopen.

Niets hiervan is glamoureus. Alles ervan is waarom een v2-sessie aanvoelt alsof ze gewoon... verbonden blijft.

Het gebeurde deze week weer — een niveau lager

Hier komt het deel dat dit meer maakt dan een oorlogsverhaal. Dezelfde faalklasse dook dagen geleden weer op, een niveau onder ons. Eén enkele client vuurde 21 transcriptie-sessiestarts af in minder dan 400 milliseconden — en trof de account-brede limiet van onze spraakprovider van 15 concurrent verzoeken. Een stampede tegen gedeelde infrastructuur, precies zoals de uploadachterstand, alleen gericht op een dependency in plaats van onze eigen backend.

De vorm is identiek, en zo ook de fix: de stampede-veroorzakende client heeft een single-flight guard nodig zodat hij niet twintig starts kan afvuren voor één intentie, en de gedeelde resource heeft een per-gebruiker-cap nodig zodat één client niet de hele pool kan opslokken. We hebben de client-side versie hiervan eerder gebouwd — de upload-pacer is dit patroon. Nu passen we het toe op sessiestarts. De thundering herd is geen bug die je één keer fixt; het is een vorm die je leert herkennen overal waar clients een gedeelde limiet tegenkomen.

Drie dingen om mee te nemen

  1. Je eigen clients zijn een load test die je niet had gepland. Alles wat batcht-bij-opstart, retryt-bij-launch of herverbindt-bij-wake is een thundering herd die op genoeg gebruikers wacht. Doseer het voordat je ze hebt.
  2. Onderscheid de code van de betekenis. 429 is de meest verkeerd gelezen status die er bestaat. "Vertraag" is niet "log uit" en niet "betaal ons." Route elke fout naar wat hij daadwerkelijk betekent.
  3. Liveness is een ladder, geen vlag. "Is de verbinding levend?" heeft meerdere eerlijke antwoorden op verschillende lagen — socket open, bytes stromen, andere kant antwoordt, netwerk aanwezig. Een robuuste app controleert er meer dan één.

Dit is de onglamoureuze machinerie onder de rust van GeekBye v2. Voor de betrouwbaarheidsfeatures die het mogelijk maakte, zie waarom je AI-notulist stopt bij slechte wifi en live transcriptie wanneer de firewall WebSockets blokkeert (v2.0.8). Voor aangrenzende releases in deze serie, waarom je AI-notulist midden in een meeting stopt met opnemen (v2.0.9).