Steven
Steven6 min di lettura

Il giorno in cui la nostra app si è fatta DDoS da sola

Un backlog di upload in sospeso, rilasciato tutto in una volta all'avvio, ha trasformato ogni client GeekBye in un piccolo attacco di negazione del servizio contro i nostri stessi server. La correzione — e la scala di verifica di vitalità della connessione che ci ha costretti a costruire — è una delle cose più utili che la v2 ci ha insegnato.

Affidabilità
Networking
Engineering
Release GeekBye
Il giorno in cui la nostra app si è fatta DDoS da sola

Ogni ingegnere di sistemi distribuiti prima o poi incontra l'assalto (la thundering herd): una massa di client che fanno tutti la stessa cosa nello stesso istante, e la risorsa condivisa dietro di loro che cede. Di solito sono i client di qualcun altro. In GeekBye v2.0.1 erano i nostri, e l'attaccante era la nostra stessa app.

Come un notetaker attacca sé stesso

GeekBye può caricare le registrazioni sul tuo Google Drive. Se una registrazione non può essere caricata subito — eri offline, l'app si è chiusa, Drive ha avuto un singhiozzo — finisce in un backlog per riprovare più tardi. Ragionevole.

Il guasto stava nel quando accadeva quel "più tardi". Al lancio successivo, l'app tentava di svuotare tutto il backlog in una volta. Un utente con una settimana di registrazioni in sospeso diventava una raffica di richieste di upload simultanee nell'istante in cui apriva l'app. Moltiplica per ogni utente che avvia al mattino, e al nostro backend si chiedeva di autenticare, verificare i limiti e processare un muro di traffico negli stessi pochi secondi — da parte di client che, tecnicamente, si comportavano tutti bene.

Il nostro backend ha fatto la cosa giusta e ha limitato l'ondata. Ed è lì che è iniziato il danno di secondo ordine. Una risposta di limite di frequenza è un HTTP 429, e un 429 assomiglia molto ad altri fallimenti se non stai attento:

  • Il caricamento del profilo all'avvio riceveva un 429 e l'app lo trattava come autenticazione fallita — sloggando brevemente gli utenti da una sessione perfettamente valida.
  • La connessione di trascrizione riceveva un 429 generico e mostrava un invito all'upgrade per limite audio raggiunto — dicendo a utenti paganti che avevano raggiunto una quota che non avevano toccato.

Così un'unica causa radice — un backlog non cadenzato — produceva tre sintomi visibili: pressione sul server, sloggamenti falsi e vendite aggiuntive false. La forma classica di un auto-DoS: il carico è cattivo, ma ciò che gli utenti sentono davvero è la cattiva interpretazione dei sintomi di quel carico.

La correzione, in due strati

Fermare l'assalto. Il backlog di upload ora è cadenzato — le richieste si distribuiscono con backoff, e c'è un raffreddamento dopo ogni 429 così che il client si allontani da un server sotto stress invece di spingerci più forte. Un assalto diventa una coda ordinata.

Fermare la cattiva interpretazione. Un 429 o 5xx transitorio durante l'avvio non ti slogga più — il client distingue "il server è occupato per un momento" da "la tua sessione non è valida". E un 429 generico di limite di frequenza sul percorso di trascrizione non si spaccia più per un errore di quota audio; solo una genuina risposta di quota mostra l'invito all'upgrade. La lezione che è rimasta: un codice di errore non è un significato di errore. 429 significa "rallenta", non "sloggati" e non "hai esaurito la quota" — e il client deve conoscere la differenza.

La scala di vitalità che ne è seguita

Cadenzare l'assalto ha esposto un problema più silenzioso: quando una connessione diventava davvero cattiva sotto carico, con quanta rapidità ce ne accorgevamo? Più lentamente di quanto volessimo. Così la v2.0.4 ha costruito una scala di verifica di vitalità della connessione per la trascrizione in tempo reale:

  • Heartbeat — al socket di trascrizione viene fatto ping a intervalli fissi, così che una connessione morta in silenzio venga rilevata in secondi invece di aspettare che il prossimo frammento di audio fallisca.
  • Un rilancio di riconnessione al ritorno della rete — quando il sistema operativo segnala che la rete è tornata, l'app ristabilisce la connessione in modo proattivo invece di aspettare di inciampare nella scoperta.
  • Vitalità tramite ping/pong di controllo — un andata e ritorno a livello applicativo che conferma non solo che "il socket è aperto" ma che "l'altro capo sta davvero rispondendo".
  • Un cancello di riconnessione gestito dal servizio — un unico posto decide se riconnettersi, invece di più parti dell'app che gareggiano per farlo pestandosi i piedi a vicenda.

Nulla di tutto ciò è affascinante. Tutto ciò è il motivo per cui una sessione della v2 dà la sensazione di semplicemente... restare connessa.

È successo di nuovo questa settimana — un livello più in basso

Ecco la parte che rende questo più di un aneddoto di guerra. Questa stessa classe di guasto è riemersa giorni fa, un livello sotto di noi. Un singolo client ha scatenato 21 avvii di sessione di trascrizione in meno di 400 millisecondi — e ha fatto scattare il limite a livello di account del nostro fornitore vocale di 15 richieste concorrenti. Un assalto contro un'infrastruttura condivisa, esattamente come il backlog di upload, solo puntato a una dipendenza invece che al nostro stesso backend.

La forma è identica, e così è la correzione: il client che assale ha bisogno di una protezione single-flight così che non possa scatenare venti avvii per una sola intenzione, e la risorsa condivisa ha bisogno di un tetto per utente così che un client non possa consumare l'intero pool. La versione lato client di questo l'abbiamo già costruita — il cadenzatore degli upload è questo pattern. Ora lo applichiamo agli avvii di sessione. L'assalto non è un bug che si corregge una volta; è una forma che si impara a riconoscere ovunque i client incontrino un limite condiviso.

Tre cose da portare a casa

  1. I tuoi stessi client sono un test di carico che non hai programmato. Qualsiasi cosa che raggruppa-all'avvio, riprova-al-lancio o si-riconnette-al-risveglio è un assalto in attesa di avere abbastanza utenti. Cadenzalo prima di averli.
  2. Distingui il codice dal significato. 429 è lo stato più frainteso che esista. "Rallenta" non è "sloggati" e non è "pagaci". Indirizza ogni fallimento verso ciò che significa davvero.
  3. La vitalità è una scala, non un flag. "La connessione è viva?" ha diverse risposte oneste a livelli diversi — socket aperto, byte che scorrono, l'altro capo che risponde, rete presente. Un'app robusta ne verifica più di una.

Questo è il macchinario senza gloria sotto la calma della v2 di GeekBye. Per le funzionalità di affidabilità che ha abilitato, vedi perché il tuo notetaker AI si ferma con un Wi-Fi scadente e trascrizione dal vivo quando il firewall blocca i WebSocket (v2.0.8). Per le release vicine di questa serie, perché il tuo notetaker AI smette di registrare a metà riunione (v2.0.9).