Steven
Steven5 min læsning

Dagen vores app DDoS'ede sig selv

En kø af ventende uploads, sluppet løs på én gang ved opstart, forvandlede hver GeekBye-klient til et lille denial-of-service-angreb (DoS) mod vores egne servere. Fixet — og den forbindelseslivlighedsstige, det tvang os til at bygge — er en af de mest nyttige ting, v2 lærte os.

Pålidelighed
Netværk
Udvikling
GeekBye-releases
Dagen vores app DDoS'ede sig selv

Enhver ingeniør inden for distribuerede systemer møder før eller siden thundering herd — den stormende hjord: en masse klienter, der alle gør det samme i samme øjeblik, og den delte ressource bag dem, der giver efter. Normalt er det en andens klienter. I GeekBye v2.0.1 var det vores, og angriberen var vores egen app.

Hvordan en notetager angriber sig selv

GeekBye kan uploade optagelser til din Google Drive. Hvis en optagelse ikke kan uploades med det samme — du var offline, appen lukkede, Drive hakkede — ryger den i en kø for at prøve igen senere. Fornuftigt.

Fejlen lå i hvornår "senere" indtraf. Ved næste opstart forsøgte appen at tømme hele køen på én gang. Én bruger med en uges ventende optagelser blev til et sværm af samtidige uploadanmodninger i det øjeblik, de åbnede appen. Gang det med hver bruger, der starter op om morgenen, og vores backend blev bedt om at autentificere, hastighedstjekke og behandle en mur af trafik inden for de samme få sekunder — fra klienter, der alle, teknisk set, opførte sig.

Vores backend gjorde det rigtige og hastighedsbegrænsede floden. Og der begyndte anden-ordens-skaden. Et hastighedsbegrænsningssvar er en HTTP 429, og 429 ligner andre fejl til forveksling, hvis man ikke er forsigtig:

  • Opstartens profilhentning fik en 429, og appen behandlede det som autentificering mislykkedes — og loggede kortvarigt brugere ud af en helt gyldig session.
  • Transskriptionsforbindelsen fik en generisk 429 og viste en opgraderingsbesked om nået lydgrænse — og fortalte betalende brugere, at de havde ramt en kvote, de ikke havde.

Så én grundårsag — en utaktet kø — producerede tre synlige symptomer: serverbelastning, falske udlogninger og falske mersalg. Den klassiske form på en selv-DoS: belastningen er skidt, men det er fejltolkningen af belastningens symptomer, brugerne rent faktisk mærker.

Fixet, i to lag

Stop stormløbet. Uploadkøen er nu taktet — anmodninger spredes ud med backoff, og en nedkølingsperiode efter enhver 429, så klienten trækker sig væk fra en belastet server i stedet for at læne sig hårdere op ad den. En thundering herd bliver til en ordentlig kø.

Stop fejltolkningen. En forbigående 429 eller 5xx under opstart logger dig ikke længere ud — klienten skelner "serveren er kortvarigt optaget" fra "din session er ugyldig". Og en generisk hastighedsbegrænsnings-429 på transskriptionsvejen maskerer sig ikke længere som en lydkvotefejl; kun et ægte kvotesvar viser opgraderingsbeskeden. Læren, der satte sig fast: en fejlkode er ikke en fejlbetydning. 429 betyder "sæt farten ned", ikke "du er logget ud" og ikke "din kvote er opbrugt" — og klienten skal kende forskellen.

Livlighedsstigen, der fulgte

At takte hjorden blottede et mere stille problem: når en forbindelse rent faktisk gik galt under belastning, hvor hurtigt opdagede vi det? Langsommere, end vi ønskede. Så v2.0.4 byggede en forbindelseslivlighedsstige til realtidstransskription:

  • Hjerteslag — transskriptionssocket'en pinges med et fast interval, så en lydløst død forbindelse opdages på sekunder i stedet for at vente på, at den næste lydbid fejler.
  • Et netværk-oppe-genoprettelsesspark — når OS'et rapporterer, at netværket er tilbage, genopretter appen forbindelsen proaktivt i stedet for at vente på at snuble ind i opdagelsen.
  • Kontrol-ping/pong-livlighed — en tur-retur på applikationsniveau, der bekræfter ikke blot "socket'en er åben", men "den anden ende svarer rent faktisk".
  • En tjenesteejet genoprettelsesport — ét sted afgør, om der skal genoprettes forbindelse, i stedet for at flere dele af appen kapløber om at gøre det og træder hinanden over tæerne.

Intet af dette er glamourøst. Alt af det er grunden til, at en v2-session føles, som om den bare... forbliver forbundet.

Det skete igen denne uge — ét niveau nede

Her er den del, der gør dette til mere end en krigshistorie. Samme fejlklasse dukkede op igen for få dage siden, ét niveau under os. En enkelt klient affyrede 21 starter af transskriptionssessioner på under 400 millisekunder — og udløste vores taleudbyders kontodækkende grænse på 15 samtidige forespørgsler. Et stormløb mod delt infrastruktur, præcis som uploadkøen, bare rettet mod en afhængighed i stedet for vores egen backend.

Formen er identisk, og det er fixet også: den stormende klient har brug for en single-flight-vagt, så den ikke kan affyre tyve starter for én hensigt, og den delte ressource har brug for et loft per bruger, så én klient ikke kan opbruge hele puljen. Vi har bygget klientsideversionen af dette før — uploadtakteren er dette mønster. Nu anvender vi det på sessionsstarter. Thundering herd er ikke en fejl, du fikser én gang; det er en form, du lærer at genkende overalt, hvor klienter møder en delt grænse.

Tre ting at tage med

  1. Dine egne klienter er en belastningstest, du ikke planlagde. Alt, der bunker-ved-opstart, prøver-igen-ved-start eller genopretter-ved-opvågnen, er en thundering herd, der venter på nok brugere. Takt det, før du har dem.
  2. Skeln koden fra betydningen. 429 er den mest fejllæste status, der findes. "Sæt farten ned" er ikke "log ud" og ikke "betal os". Dirigér hver fejl hen til, hvad den rent faktisk betyder.
  3. Livlighed er en stige, ikke et flag. "Er forbindelsen i live?" har flere ærlige svar på forskellige lag — socket åben, bytes flyder, den anden ende svarer, netværk til stede. En robust app tjekker mere end ét.

Dette er det uglamourøse maskineri under GeekBye v2's ro. For de pålidelighedsfunktioner, det muliggjorde, se hvorfor din AI-notetager stopper på dårlig Wi-Fi og livetransskription når firewallen blokerer WebSockets (v2.0.8). For de nærliggende releases i denne serie, hvorfor din AI-notetager stopper med at optage midt i mødet (v2.0.9).