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).

Articole Similare

Tăcerea era portantă
Steven
Steven8 min citire

Tăcerea era portantă

Ultimele două lansări ale GeekBye v1 sunt despre același adevăr incomod: transcrierea în timp real peste o rețea reală nu e fără pierderi, iar mișcarea onestă e să încetezi să pretinzi că e. v1.8.20 a păstrat pe disc o copie a fiecărui fragment audio înainte să-l arunce în timpul unei reconectări și a început să marcheze cu voce tare golurile din transcriere. v1.9.0 a încetat să trimită tăcere ca să economisească lățime de bandă — și a descoperit că tăcerea era exact semnalul pe care transcriitorul îl folosea ca să știe că o propoziție s-a încheiat. Două lansări despre costul aruncării lucrurilor.

Inginerie
Audio
Fiabilitate
O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea
Steven
Steven7 min citire

O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea

Când backend-ul nostru a picat în plină ședință, nu doar că a oprit transcrierea — a prăbușit întreaga aplicație. Cauza era un singur eveniment netratat, iar reparația a fost de zece linii. Acesta e grupul de lansări care a făcut GeekBye să rămână autentificat și conectat prin lucrurile care înainte îl omorau.

Inginerie
Fiabilitate
Electron
Verificarea de siguranță care a făcut aplicația noastră imposibil de închis
Steven
Steven6 min citire

Verificarea de siguranță care a făcut aplicația noastră imposibil de închis

Auto-update a fost cea mai grea funcție pe care am livrat-o vreodată — șase lansări în patru zile ca s-o oprim din a strica aplicația. Cel mai urât bug a fost unul pe care l-am introdus chiar noi încercând să fim precauți: o verificare de „siguranță" de 500 de milisecunde care transforma un update eșuat într-un proces pe care pur și simplu nu-l puteai închide.

Inginerie
Electron
Fiabilitate