Arquitetura para resiliência: quando 150 RPS se tornam 2.000: Encontrando o gargalo

Sumário executivo
No kleinanzeigen.demudamos nossa página inicial para uma pilha Node.js moderna. Isso nos ajudou a entregar recursos com mais rapidez, mas também expôs novos riscos de estabilidade. Ataques DDoS recorrentes ameaçavam nosso tempo de atividade. Para manter o site online, tivemos que provisionar nossa infraestrutura em 1000%.
Este estudo de caso explica como liderei a iniciativa de passar de uma estratégia de expansão de “força bruta” para uma arquitetura resiliente. Reduzimos os custos de infraestrutura 60% e impediu que o aplicativo travasse durante os ataques, enquanto a equipe continuava lançando novos recursos.
O Contexto Empresarial: Sobrevivendo ao Dilúvio
Kleinanzeigen é o maior mercado online da Alemanha. Nossa página inicial é a porta de entrada do nosso ecossistema. Ele lida com uma linha de base de cerca 150 solicitações por segundo (RPS) e atende milhões de usuários todos os dias.
Recentemente, migramos de um monólito Java legado para um Astro + Node.js arquitetura. O lançamento correu bem e o desempenho foi forte. Mas a realidade de operar um alvo de alta visibilidade apanhou-nos rapidamente.
Somos alvo frequente de ataques DDoS. É importante compreender a escala aqui – estes não são pequenos aborrecimentos. Estamos falando de ataques que geram milhões de solicitações por segundo na borda.
O problema do “vazamento”
Nosso CDN e Firewall fazem um trabalho incrível, filtrando a grande maioria desse tráfego. Mas quando você lida com milhões de RPS, mesmo um pequeno “vazamento” é devastador.
Se um ataque enviar 10 milhões de RPS e o firewall parar 99,9% dele, ainda ficaremos com 10.000 RPS atingindo nossos servidores de origem.
- Carga normal: 150 RPS.
- Vazamento de ataque: 2.000 a 10.000 RPS.
Estávamos enfrentando picos de tráfego que estavam 10x a 60x nossa capacidade normal, atingindo-nos instantaneamente. Para piorar a situação, os ataques foram aumentando em volume ao longo do tempo. O método de força bruta para adicionar mais servidores estava se tornando insustentável. Não poderíamos simplesmente continuar comprando hardware para acompanhar o crescimento exponencial dos ataques.
O Impacto Operacional
Durante essas explosões, nosso novo serviço SSR (Server-Side Rendering) entraria em um “crashloop”. As sondagens de atividade do Kubernetes falharam, os pods foram reiniciados e travaram novamente imediatamente.
Para manter o negócio funcionando, nossa equipe de SRE teve que dimensionar a implantação de forma agressiva. Nós corremos Mais de 100 pods (1 CPU / 2 GB de RAM) o tempo todo. Estávamos pagando por 100 servidores ociosos apenas para absorver um pico de 2 minutos uma vez por semana. Precisávamos de uma solução estrutural.
A lacuna de observabilidade: encontrando a causa raiz
Não poderíamos interromper o roteiro do produto para corrigir isso. Meu papel como engenheiro de equipe era proteger a velocidade da equipe, assumindo a responsabilidade por esse problema de estabilidade. Tive que analisar o problema e desenhar uma solução sem bloquear o trabalho diário dos recursos.
O primeiro desafio foi a visibilidade. No caos inicial, presumimos que a CPU estava lutando para renderizar os componentes do React. Contudo, as ferramentas de monitorização padrão não nos forneceram detalhes suficientes. No Node.js, um Event Loop bloqueado muitas vezes pode se mascarar como baixo uso da CPU enquanto o servidor não responde.
Para consertar a arquitetura, primeiro precisei de dados melhores.
Instrumentação Estratégica
Arquitetei uma camada de observabilidade personalizada para ver exatamente o que estava acontecendo dentro do tempo de execução:
- Métricas de SSR: Eu criei uma integração personalizada do Prometheus (
astro-prometheus-node-integration) para medir os tempos brutos do servidor HTTP. - Rastreamento de Dependência: Eu implementei
observableFetchum wrapper em torno de nossas chamadas internas de API para rastrear a latência de serviços downstream.
A descoberta do “fan-out”
Com a instrumentação instalada, trabalhei com meus colegas de trabalho para executar testes de carga. Os dados nos mostraram uma falha crítica. Não foi a renderização da CPU que nos matou. Foi o Fan-Out.
Para cada solicitação de página inicial, nosso servidor Node.js fez 20 chamadas de API simultâneas para diferentes microsserviços de back-end (atendimento ao usuário, pesquisa, recomendações, etc.).
- Linha de base: A 150 RPS, geramos 3.000 RPS internos. O backend lidou com isso facilmente.
- Sob ataque: Quando o tráfego saltou para 2.000 RPS (um pequeno vazamento), nosso aplicativo amplificou essa carga. Estávamos martelando nossas APIs internas com 40.000 RPS.
Construímos um amplificador DDoS. Nosso próprio aplicativo estava obstruindo nossos serviços de back-end e causando lentidão. Isso aumentou o “tempo de espera” para nossos processos Node.js, levando à saturação de memória e travamentos em cascata.
Olhando para o futuro: a solução
Agora eu tinha as evidências de que precisava. O problema não era apenas “tráfego”. Foi uma estratégia de busca de dados ineficiente que não teve boa escalabilidade.
Em Parte 2detalharei como desmontamos essa arquitetura fan-out. Explicarei a estratégia específica de cache que implementamos para sobreviver à inundação e como economizamos 60% para a empresa em custos de infraestrutura, sem sacrificar a experiência do usuário.
