
Den, kdy si naše aplikace udělala DDoS sama na sebe
Backlog čekajících uploadů, uvolněný najednou při startu, proměnil každého GeekBye klienta v malý denial-of-service útok na naše vlastní servery. Oprava — a žebřík connection-liveness, který nás donutila postavit — je jedna z nejužitečnějších věcí, které nás v2 naučila.
Každý inženýr distribuovaných systémů dřív nebo později potká thundering herd (splašené stádo): masu klientů, kteří všichni dělají totéž ve stejný okamžik, a sdílený zdroj za nimi se hroutí. Obvykle jsou to klienti někoho jiného. V GeekBye v2.0.1 byli naši a útočníkem byla naše vlastní aplikace.
Jak zapisovatel útočí sám na sebe
GeekBye umí nahrávat záznamy do tvého Google Drive. Pokud se záznam nemůže nahrát okamžitě — byl jsi offline, aplikace se zavřela, Drive zaškytal — jde do backlogu, aby to zkusil znovu později. Rozumné.
Selhání bylo v tom, kdy to „později" nastalo. Při dalším spuštění se aplikace pokusila vyprázdnit celý backlog najednou. Jeden uživatel s týdnem čekajících záznamů se stal salvou souběžných upload požadavků ve chvíli, kdy otevřel aplikaci. Vynásob každým uživatelem, který ji ráno spustí, a od našeho backendu se chtělo, aby autentizoval, rate-zkontroloval a zpracoval zeď provozu ve stejných pár sekundách — od klientů, kteří se všichni, technicky, chovali správně.
Náš backend udělal správnou věc a rate-limitoval záplavu. A tady začala škoda druhého řádu. Rate-limit odpověď je HTTP 429, a 429 se hodně podobá jiným selháním, když nedáváš pozor:
- Profile fetch při startu dostal
429a aplikace ho vzala jako selhání auth — na chvíli odhlašovala uživatele z naprosto platné session. - Připojení transkripce dostalo obecnou
429a zobrazilo upgrade výzvu typu dosažen audio limit — říkalo platícím uživatelům, že narazili na kvótu, na kterou nenarazili.
Takže jedna kořenová příčina — nerytmovaný backlog — vyprodukovala tři viditelné symptomy: zátěž serveru, falešná odhlášení a falešné upselly. Klasický tvar self-DoS: zátěž je špatná, ale to, co uživatelé skutečně cítí, je špatná interpretace symptomů té zátěže.
Oprava, ve dvou vrstvách
Zastav tlačenici. Upload backlog je teď rytmovaný — požadavky jsou rozprostřené v čase s backoffem, plus cooldown po každé 429, takže klient couvne od zatíženého serveru, místo aby se na něj opíral tvrději. Z thundering herd se stane spořádaná fronta.
Zastav špatnou interpretaci. Přechodná 429 nebo 5xx během startu tě už neodhlásí — klient rozlišuje „server je krátce zaneprázdněný" od „tvá session je neplatná". A obecná rate-limit 429 na cestě transkripce se už nemaskuje jako chyba audio kvóty; upgrade výzvu zobrazí jen skutečná odpověď o kvótě. Ponaučení, které zůstalo: kód chyby není význam chyby. 429 znamená „zpomal", ne „nejsi autorizovaný" a ne „došla ti kvóta" — a klient musí ten rozdíl znát.
Žebřík liveness, který následoval
Rytmování stáda odhalilo tišší problém: když se připojení opravdu pod zátěží pokazilo, jak rychle jsme si toho všimli? Pomaleji, než jsme chtěli. Tak v2.0.4 vybudovala žebřík connection-liveness pro přepis v reálném čase:
- Heartbeaty — transkripční socket se pinguje v pevném intervalu, takže potichu mrtvé připojení se odhalí za sekundy, místo čekání, až selže další kus audia.
- Kick pro opětovné připojení při návratu sítě — když operační systém ohlásí, že síť je zpět, aplikace proaktivně obnoví připojení, místo aby čekala, až o to zakopne.
- Liveness přes control ping/pong — round-trip na úrovni aplikace, který potvrzuje nejen „socket je otevřený", ale „druhý konec skutečně odpovídá".
- Brána opětovného připojení vlastněná službou — jedno místo rozhoduje, zda se znovu připojit, místo aby o to závodilo několik částí aplikace a šlapaly si navzájem po nohou.
Nic z toho není okázalé. Všechno z toho je důvod, proč v2 session působí, jako by prostě... zůstala připojená.
Stalo se to znovu tento týden — o úroveň níž
Tady je ta část, díky které je to víc než válečná historka. Stejná třída selhání se před pár dny vynořila znovu, o úroveň pod námi. Jeden jediný klient odpálil 21 startů transkripční session za méně než 400 milisekund — a překročil limit našeho speech poskytovatele na úrovni účtu, 15 souběžných požadavků. Tlačenice proti sdílené infrastruktuře, přesně jako upload backlog, jen mířená na závislost místo na náš vlastní backend.
Tvar je totožný a oprava také: dupající klient potřebuje single-flight guard, aby nemohl odpálit dvacet startů na jeden záměr, a sdílený zdroj potřebuje strop na uživatele, aby jeden klient nemohl spotřebovat celý pool. Klientskou verzi tohohle jsme už postavili dřív — upload pacer je tento vzor. Teď ho aplikujeme na starty session. Thundering herd není bug, který opravíš jednou; je to tvar, který se naučíš rozpoznávat všude, kde klienti narazí na sdílený limit.
Tři věci k zapamatování
- Tvoji vlastní klienti jsou zátěžový test, který jsi nenaplánoval. Cokoli, co dávkuje-při-startu, zkouší-znovu-při-spuštění nebo se připojuje-znovu-při-probuzení, je thundering herd čekající na dost uživatelů. Zrytmuj to, než je budeš mít.
- Rozlišuj kód od významu.
429je nejhůř čtený status v existenci. „Zpomal" není „odhlásit se" a není „zaplať nám". Nasměruj každé selhání k tomu, co skutečně znamená. - Liveness je žebřík, ne flag. „Je připojení naživu?" má několik poctivých odpovědí na různých vrstvách — socket otevřený, bajty tečou, druhý konec odpovídá, síť přítomna. Robustní aplikace kontroluje víc než jednu.
Tohle je neokázalá mašinerie pod klidem GeekBye v2. K funkcím spolehlivosti, které umožnila, viz proč se váš AI zapisovatel zastaví při špatném Wi-Fi a přepis naživo, když firewall blokuje WebSockety (v2.0.8). K sousedním vydáním z této série, proč váš AI zapisovatel přestane nahrávat uprostřed meetingu (v2.0.9).