Steven
Steven6 min de leitura

O dia em que a nossa app se fez DDoS a si própria

Um backlog de uploads pendentes, libertado todo de uma vez no arranque, transformou cada cliente do GeekBye num pequeno ataque de negação de serviço contra os nossos próprios servidores. A correção — e a escada de verificação de vitalidade da ligação que nos obrigou a construir — é uma das coisas mais úteis que a v2 nos ensinou.

Fiabilidade
Redes
Engenharia
Lançamentos GeekBye
O dia em que a nossa app se fez DDoS a si própria

Todo o engenheiro de sistemas distribuídos acaba por encontrar a debandada (a thundering herd): uma massa de clientes a fazer todos a mesma coisa no mesmo instante, e o recurso partilhado por trás deles a ceder. Normalmente são os clientes de outra pessoa. No GeekBye v2.0.1 eram os nossos, e o atacante era a nossa própria app.

Como um notetaker se ataca a si próprio

O GeekBye pode enviar gravações para o teu Google Drive. Se uma gravação não conseguir ser enviada de imediato — estavas offline, a app fechou, o Drive teve um soluço — vai para um backlog para reenviar mais tarde. Sensato.

A falha estava no quando desse "mais tarde". No arranque seguinte, a app tentava esvaziar todo o backlog de uma vez. Um utilizador com uma semana de gravações pendentes tornava-se numa rajada de pedidos de upload simultâneos no instante em que abria a app. Multiplica por cada utilizador a arrancar de manhã, e ao nosso backend pedia-se que autenticasse, verificasse limites e processasse um muro de tráfego nos mesmos poucos segundos — de clientes que, tecnicamente, estavam todos a comportar-se bem.

O nosso backend fez a coisa certa e limitou a enxurrada. E foi aí que começou o dano de segunda ordem. Uma resposta de limite de taxa é um HTTP 429, e um 429 parece-se muito com outras falhas se não tiveres cuidado:

  • O carregamento do perfil no arranque recebia um 429 e a app tratava-o como falha de autenticação — desligando brevemente os utilizadores de uma sessão perfeitamente válida.
  • A ligação de transcrição recebia um 429 genérico e mostrava um aviso de upgrade por limite de áudio atingido — dizendo a utilizadores pagantes que tinham atingido uma quota que não tinham tocado.

Assim, uma única causa raiz — um backlog sem doseamento — produzia três sintomas visíveis: pressão no servidor, deslogues falsos e vendas cruzadas falsas. A forma clássica de um auto-DoS: a carga é má, mas o que os utilizadores sentem de verdade é a má interpretação dos sintomas dessa carga.

A correção, em duas camadas

Parar a debandada. O backlog de uploads é agora doseado — os pedidos espalham-se com backoff, e há um arrefecimento após qualquer 429 para que o cliente se afaste de um servidor sobrecarregado em vez de carregar com mais força. Uma debandada torna-se uma fila ordenada.

Parar a má interpretação. Um 429 ou 5xx transitório durante o arranque já não te desliga — o cliente distingue "o servidor está ocupado por um momento" de "a tua sessão é inválida". E um 429 genérico de limite de taxa no caminho da transcrição já não se faz passar por um erro de quota de áudio; só uma resposta genuína de quota mostra o aviso de upgrade. A lição que ficou: um código de erro não é um significado de erro. 429 significa "abranda", não "sai da sessão" nem "ficaste sem quota" — e o cliente tem de saber a diferença.

A escada de vitalidade que se seguiu

Dosear a debandada expôs um problema mais silencioso: quando uma ligação ficava mesmo má sob carga, com que rapidez nos apercebíamos? Mais devagar do que queríamos. Por isso a v2.0.4 construiu uma escada de verificação de vitalidade da ligação para a transcrição em tempo real:

  • Heartbeats — é feito ping ao socket de transcrição em intervalos fixos, para que uma ligação morta em silêncio seja detetada em segundos em vez de esperar que o próximo fragmento de áudio falhe.
  • Um reengate de reconexão quando a rede volta — quando o sistema operativo reporta que a rede regressou, a app restabelece a ligação de forma proativa em vez de esperar tropeçar na descoberta.
  • Vitalidade por ping/pong de controlo — uma ida e volta ao nível da aplicação que confirma não só que "o socket está aberto" mas que "a outra ponta está mesmo a responder".
  • Uma comporta de reconexão gerida pelo serviço — um único lugar decide se reconecta, em vez de várias partes da app a competir por fazê-lo e a atrapalharem-se umas às outras.

Nada disto é glamoroso. Tudo isto é a razão pela qual uma sessão da v2 parece simplesmente... continuar ligada.

Voltou a acontecer esta semana — um nível abaixo

Aqui está a parte que faz disto mais do que uma história de guerra. Esta mesma classe de falha ressurgiu há dias, um nível abaixo de nós. Um único cliente disparou 21 inícios de sessão de transcrição em menos de 400 milissegundos — e fez saltar o limite de toda a conta do nosso fornecedor de voz de 15 pedidos concorrentes. Uma debandada contra infraestrutura partilhada, exatamente como o backlog de uploads, só que apontada a uma dependência em vez do nosso próprio backend.

A forma é idêntica, e a correção também: o cliente que debanda precisa de uma proteção single-flight para não poder disparar vinte inícios por uma só intenção, e o recurso partilhado precisa de um teto por utilizador para que um cliente não possa consumir todo o pool. Já construímos a versão do lado do cliente disto antes — o doseador de uploads é este padrão. Agora aplicamo-lo aos inícios de sessão. A debandada não é um bug que se corrige uma vez; é uma forma que se aprende a reconhecer em todo o lado onde clientes se encontram com um limite partilhado.

Três coisas a levar

  1. Os teus próprios clientes são um teste de carga que não agendaste. Qualquer coisa que agrupe-no-arranque, tente-de-novo-no-lançamento ou reconecte-ao-acordar é uma debandada à espera de ter utilizadores suficientes. Doseia-a antes de os teres.
  2. Distingue o código do significado. 429 é o estado mais mal lido que existe. "Abranda" não é "sai da sessão" nem "paga-nos". Encaminha cada falha para o que ela realmente significa.
  3. A vitalidade é uma escada, não um flag. "A ligação está viva?" tem várias respostas honestas em camadas diferentes — socket aberto, bytes a fluir, a outra ponta a responder, rede presente. Uma app robusta verifica mais do que uma.

Esta é a maquinaria sem glamour por baixo da calma da v2 do GeekBye. Para as funcionalidades de fiabilidade que ela permitiu, vê porque é que o teu notetaker com IA para com mau Wi-Fi e transcrição ao vivo quando a firewall bloqueia os WebSockets (v2.0.8). Para os lançamentos vizinhos desta série, porque é que o teu notetaker com IA para de gravar a meio da reunião (v2.0.9).