Steven
Steven6 min citire

Ziua în care aplicația noastră și-a făcut singură DDoS

Un backlog de încărcări în așteptare, eliberat dintr-odată la pornire, a transformat fiecare client GeekBye într-un mic atac de denial-of-service asupra propriilor noastre servere. Fixul — și scara de connection-liveness pe care ne-a obligat să o construim — e unul dintre cele mai utile lucruri pe care ni le-a predat v2.

Fiabilitate
Rețelistică
Inginerie
Lansări GeekBye
Ziua în care aplicația noastră și-a făcut singură DDoS

Orice inginer de sisteme distribuite întâlnește la un moment dat thundering herd-ul (turma dezlănțuită): o masă de clienți care fac toți același lucru în același instant, iar resursa comună din spatele lor se prăbușește. De obicei sunt clienții altcuiva. În GeekBye v2.0.1 au fost ai noștri, iar atacatorul era propria noastră aplicație.

Cum se atacă singur un notetaker

GeekBye poate încărca înregistrări în Google Drive-ul tău. Dacă o înregistrare nu poate fi încărcată imediat — erai offline, aplicația s-a închis, Drive a avut o sughițeală — intră într-un backlog ca să fie reîncercată mai târziu. Rezonabil.

Eșecul era în momentul în care se întâmpla acel „mai târziu". La următoarea lansare, aplicația încerca să golească întregul backlog dintr-odată. Un utilizator cu o săptămână de înregistrări în așteptare devenea o rafală de cereri de încărcare simultane în clipa în care deschidea aplicația. Înmulțește cu fiecare utilizator care lansează aplicația dimineața, iar backendului nostru i se cerea să autentifice, să verifice rate-ul și să proceseze un zid de trafic în aceleași câteva secunde — de la clienți care, tehnic, se comportau toți corect.

Backendul nostru a făcut ce trebuia și a rate-limitat potopul. Și aici a început dauna de ordinul doi. Un răspuns de rate-limit e un HTTP 429, iar 429 seamănă foarte mult cu alte eșecuri dacă nu ești atent:

  • Profile fetch-ul de la pornire a primit un 429, iar aplicația l-a tratat drept auth eșuat — dând logout pentru scurt timp utilizatorilor dintr-o sesiune perfect validă.
  • Conexiunea de transcriere a primit un 429 generic și a arătat un prompt de upgrade audio-limit-reached — spunând utilizatorilor plătitori că au atins o cotă pe care nu o atinseseră.

Deci o singură cauză-rădăcină — un backlog neritmat — a produs trei simptome vizibile: efort pe server, logout-uri false și upsell-uri false. Forma clasică a unui self-DoS: încărcarea e rea, dar interpretarea greșită a simptomelor încărcării e ceea ce utilizatorii chiar simt.

Fixul, în două straturi

Oprește busculada. Backlogul de încărcări e acum ritmat — cererile sunt distribuite în timp cu backoff, plus un cooldown după orice 429, astfel încât clientul se retrage dintr-un server încordat în loc să se sprijine mai tare pe el. Un thundering herd devine o coadă ordonată.

Oprește interpretarea greșită. Un 429 sau 5xx tranzitoriu în timpul pornirii nu-ți mai dă logout — clientul distinge „serverul e ocupat pentru scurt timp" de „sesiunea ta e invalidă". Iar un 429 generic de rate-limit pe calea de transcriere nu se mai maschează drept eroare de cotă audio; doar un răspuns genuin de cotă arată promptul de upgrade. Lecția care a rămas: un cod de eroare nu e o semnificație a erorii. 429 înseamnă „încetinește", nu „ești neautorizat" și nu „ai rămas fără cotă" — iar clientul trebuie să știe diferența.

Scara de liveness care a urmat

Ritmarea turmei a scos la iveală o problemă mai discretă: când o conexiune chiar se strica sub sarcină, cât de repede observam? Mai încet decât ne-am fi dorit. Așa că v2.0.4 a construit o scară de connection-liveness pentru transcrierea în timp real:

  • Heartbeat-uri — socketul de transcriere e ping-uit la un interval fix, ca o conexiune moartă în tăcere să fie detectată în secunde, în loc să aștepte ca următorul fragment de audio să eșueze.
  • Un kick de reconectare la revenirea rețelei — când sistemul de operare raportează că rețeaua e înapoi, aplicația restabilește proactiv conexiunea, în loc să aștepte să se împiedice de descoperire.
  • Liveness prin ping/pong de control — un round-trip la nivel de aplicație care confirmă nu doar „socketul e deschis", ci „celălalt capăt chiar răspunde".
  • O poartă de reconectare deținută de serviciu — un singur loc decide dacă să se reconecteze, în loc ca mai multe părți ale aplicației să se întreacă în a o face și să se calce reciproc.

Nimic din toate astea nu e spectaculos. Toate sunt motivul pentru care o sesiune v2 pare că pur și simplu... rămâne conectată.

S-a întâmplat din nou săptămâna aceasta — cu un nivel mai jos

Iată partea care face din asta mai mult decât o poveste de război. Aceeași clasă de eșec a reapărut acum câteva zile, cu un nivel sub noi. Un singur client a lansat 21 de porniri de sesiune de transcriere în mai puțin de 400 de milisecunde — și a declanșat limita la nivel de cont a furnizorului nostru de speech, de 15 cereri concurente. O busculadă împotriva infrastructurii comune, exact ca backlogul de încărcări, doar că îndreptată spre o dependență în loc de propriul nostru backend.

Forma e identică, la fel și fixul: clientul care busculează are nevoie de un single-flight guard ca să nu poată lansa douăzeci de porniri pentru o singură intenție, iar resursa comună are nevoie de un plafon per utilizator, ca un singur client să nu poată consuma întregul bazin. Am construit versiunea de partea clientului a acestui lucru și înainte — pacer-ul de încărcări este acest tipar. Acum îl aplicăm la pornirile de sesiune. Thundering herd-ul nu e un bug pe care îl repari o dată; e o formă pe care înveți să o recunoști peste tot unde clienții întâlnesc o limită comună.

Trei lucruri de reținut

  1. Propriii tăi clienți sunt un test de sarcină pe care nu l-ai programat. Orice se grupează-la-pornire, reîncearcă-la-lansare sau se reconectează-la-trezire e un thundering herd care așteaptă suficienți utilizatori. Ritmează-l înainte să-i ai.
  2. Distinge codul de semnificație. 429 e cel mai greșit interpretat status din existență. „Încetinește" nu e „logout" și nu e „plătește-ne". Direcționează fiecare eșec către ceea ce înseamnă cu adevărat.
  3. Liveness-ul e o scară, nu un steag. „E conexiunea vie?" are mai multe răspunsuri oneste la straturi diferite — socket deschis, byți care curg, celălalt capăt răspunde, rețea prezentă. O aplicație robustă verifică mai mult de unul.

Aceasta e mașinăria neseducătoare de sub calmul lui GeekBye v2. Pentru funcțiile de fiabilitate pe care le-a activat, vezi de ce notetaker-ul tău AI se oprește pe Wi-Fi prost și transcriere live când firewall-ul blochează WebSocket-urile (v2.0.8). Pentru lansările vecine din această serie, de ce notetaker-ul tău AI oprește înregistrarea în mijlocul ședinței (v2.0.9).