
Dagen vår app DDoS:ade sig själv
En kö av väntande uppladdningar, släppt loss allt på en gång vid start, förvandlade varje GeekBye-klient till en liten överbelastningsattack (DoS) mot våra egna servrar. Fixen — och den anslutningslivlighetsstege den tvingade oss att bygga — är en av de mest användbara sakerna v2 lärde oss.
Varje ingenjör inom distribuerade system möter förr eller senare thundering herd — den stormande hjorden: en massa klienter som alla gör samma sak i samma ögonblick, och den delade resursen bakom dem som viker sig. Vanligtvis är det någon annans klienter. I GeekBye v2.0.1 var det våra, och angriparen var vår egen app.
Hur ett anteckningsverktyg attackerar sig självt
GeekBye kan ladda upp inspelningar till din Google Drive. Om en inspelning inte kan laddas upp direkt — du var offline, appen stängdes, Drive hackade — hamnar den i en kö för att försöka igen senare. Vettigt.
Felet låg i när "senare" inträffade. Vid nästa start försökte appen tömma hela kön på en gång. En användare med en veckas väntande inspelningar blev en skur av samtidiga uppladdningsförfrågningar i samma ögonblick som de öppnade appen. Multiplicera med varje användare som startar på morgonen, och vår backend ombads autentisera, hastighetskontrollera och behandla en vägg av trafik inom samma få sekunder — från klienter som alla, tekniskt sett, betedde sig.
Vår backend gjorde rätt sak och hastighetsbegränsade floden. Och där började andra ordningens skada. Ett hastighetsbegränsningssvar är en HTTP 429, och 429 ser ut precis som andra fel om man inte är försiktig:
- Startens profilhämtning fick en
429och appen behandlade det som autentisering misslyckades — och loggade kortvarigt ut användare från en fullt giltig session. - Transkriberingsanslutningen fick en generisk
429och visade en uppgraderingsuppmaning om nådd ljudgräns — och berättade för betalande användare att de nått en kvot de inte nått.
Så en grundorsak — en otaktad kö — gav tre synliga symptom: serverbelastning, falska utloggningar och falska merförsäljningar. Den klassiska formen på en själv-DoS: belastningen är dålig, men det är feltolkningen av belastningens symptom som användarna faktiskt känner av.
Fixen, i två lager
Stoppa stormen. Uppladdningskön är nu taktad — förfrågningar sprids ut med backoff, och en nedkylningsperiod efter varje 429 så att klienten drar sig undan från en belastad server istället för att luta sig hårdare mot den. En thundering herd blir en ordnad kö.
Stoppa feltolkningen. En övergående 429 eller 5xx under start loggar inte längre ut dig — klienten skiljer "servern är kort upptagen" från "din session är ogiltig". Och en generisk hastighetsbegränsnings-429 på transkriberingsvägen maskerar sig inte längre som ett ljudkvotsfel; bara ett genuint kvotsvar visar uppgraderingsuppmaningen. Läxan som fastnade: en felkod är inte en felbetydelse. 429 betyder "sakta ner", inte "du är utloggad" och inte "din kvot är slut" — och klienten måste veta skillnaden.
Livlighetsstegen som följde
Att takta hjorden blottade ett tystare problem: när en anslutning verkligen gick dålig under belastning, hur snabbt märkte vi det? Långsammare än vi ville. Så v2.0.4 byggde ut en anslutningslivlighetsstege för realtidstranskribering:
- Hjärtslag — transkriberingssocketen pingas med ett fast intervall, så att en tyst död anslutning upptäcks på sekunder istället för att vänta på att nästa ljudbit ska misslyckas.
- En nätverk-upp-återanslutningsspark — när OS rapporterar att nätverket är tillbaka återupprättar appen anslutningen proaktivt istället för att vänta på att snubbla in i upptäckten.
- Kontroll-ping/pong-livlighet — en rundtur på applikationsnivå som bekräftar inte bara "socketen är öppen" utan "andra änden svarar faktiskt".
- En tjänstägd återanslutningsgrind — en plats bestämmer om det ska återanslutas, istället för att flera delar av appen kapplöper om att göra det och trampar på varandra.
Inget av detta är glamoröst. Allt av det är varför en v2-session känns som att den bara... förblir ansluten.
Det hände igen den här veckan — ett steg ner
Här är delen som gör detta till mer än en krigshistoria. Samma felklass dök upp igen för några dagar sedan, en nivå under oss. En enda klient avfyrade 21 starter av transkriberingssessioner på under 400 millisekunder — och slog i vår talleverantörs kontoövergripande gräns på 15 samtidiga förfrågningar. En storm mot delad infrastruktur, precis som uppladdningskön, bara riktad mot ett beroende istället för vår egen backend.
Formen är identisk, och det är fixen också: den stormande klienten behöver ett single-flight-skydd så att den inte kan avfyra tjugo starter för en avsikt, och den delade resursen behöver ett tak per användare så att en klient inte kan förbruka hela poolen. Vi har byggt klientsidesversionen av detta förut — uppladdningstaktaren är det här mönstret. Nu tillämpar vi det på sessionsstarter. Thundering herd är inte en bugg du fixar en gång; det är en form du lär dig känna igen överallt där klienter möter en delad gräns.
Tre saker att ta med sig
- Dina egna klienter är ett belastningstest du inte schemalagt. Allt som buntar-vid-start, försöker-igen-vid-start eller återansluter-vid-uppvaknande är en thundering herd som väntar på tillräckligt många användare. Takta det innan du har dem.
- Skilj koden från betydelsen.
429är den mest felläsa statusen som finns. "Sakta ner" är inte "logga ut" och inte "betala oss". Dirigera varje fel till vad det faktiskt betyder. - Livlighet är en stege, inte en flagga. "Är anslutningen vid liv?" har flera ärliga svar på olika lager — socket öppen, bytes flödar, andra änden svarar, nätverk närvarande. En robust app kontrollerar mer än ett.
Detta är det oglamorösa maskineriet under GeekBye v2:s lugn. För tillförlitlighetsfunktionerna det möjliggjorde, se varför ditt AI-anteckningsverktyg stannar på dålig Wi-Fi och livetranskribering när brandväggen blockerar WebSockets (v2.0.8). För de närliggande releaserna i den här serien, varför din AI-anteckningsapp slutar spela in mitt i mötet (v2.0.9).