Escalando WebSockets: Como lidar com 1 milhão de conexões simultâneas
O "problema C10K" é história antiga. Hoje, falamos sobre o problema C1M: lidar com 1 milhão de conexões simultâneas em um único servidor.
Se você está construindo uma aplicação em tempo real — seja um chat, um feed de esportes ao vivo ou uma ferramenta de edição colaborativa — você eventualmente vai bater em um muro. Seu servidor vai travar, novas conexões vão dar timeout e sua latência vai disparar. E o culpado raramente é o código da sua aplicação. Geralmente é a configuração do Sistema Operacional.
Neste artigo, vamos ver os passos exatos para ajustar um servidor Linux e lidar com 1 milhão de conexões WebSocket simultâneas. Vamos cobrir descritores de arquivos, portas efêmeras e a arquitetura necessária para escalar além de um único nó.
O gargalo NÃO é o Node.js (Geralmente)
Muitos desenvolvedores assumem que o Node.js (ou Python/Ruby) é muito lento para lidar com milhões de conexões. Embora seja verdade que uma única thread tem limites, o primeiro gargalo que você encontrará é quase sempre o Sistema Operacional.
Por padrão, o Linux é configurado para computação de propósito geral, não para manter milhões de conexões TCP persistentes.
1. O erro "Too Many Open Files"
No Linux, tudo é um arquivo. Um socket é um arquivo. Um arquivo no disco é um arquivo. Quando um processo abre uma conexão, ele consome um descritor de arquivo (FD).
Verifique seu limite atual:
ulimit -n # Saída: 1024 (geralmente)
Se você tentar abrir 1.025 conexões, sua aplicação falhará com EMFILE: too many open files.
A Solução:
Você precisa aumentar tanto o limite do sistema quanto o limite por processo.
Edite /etc/sysctl.conf:
fs.file-max = 2097152
Edite /etc/security/limits.conf:
* soft nofile 1048576 * hard nofile 1048576 root soft nofile 1048576 root hard nofile 1048576
Aplique as mudanças com sysctl -p. Agora seu SO permite descritores de arquivo suficientes. Mas estamos apenas começando.
2. A armadilha da Exaustão de Portas Efêmeras
Este é o "assassino silencioso" mais comum em arquiteturas WebSocket de alta escala.
Quando um cliente se conecta a um servidor, ele usa um IP de origem e uma porta de origem. Quando seu servidor se conecta a um banco de dados backend ou outro serviço, ele se torna o cliente e usa uma porta local.
Existem apenas 65.535 portas disponíveis. Se você tem um balanceador de carga ou proxy na frente do seu servidor WebSocket, você pode ficar sem portas para as conexões de saída.
A Tupla TCP
Uma conexão TCP é identificada por uma tupla de 4 elementos:
{IP Origem, Porta Origem, IP Destino, Porta Destino}
Se seu balanceador de carga se conecta ao seu servidor WebSocket, e ambos estão em IPs específicos, você está limitado pelo número de portas de origem disponíveis (aprox 60k).
A Solução:
-
Aumente o intervalo de portas locais:
# /etc/sysctl.conf net.ipv4.ip_local_port_range = 1024 65535 -
Habilite a Reutilização de TCP:
net.ipv4.tcp_tw_reuse = 1Isso permite ao SO reclamar portas no estado
TIME_WAITmais rapidamente.
3. Uso de Memória: O custo real de um Socket
Quanta RAM consomem 1 milhão de conexões?
No passado, uma conexão podia custar alguns kilobytes de memória do kernel. Em kernels modernos, é muito mais eficiente, mas a memória da aplicação é o fator maior.
Se você usa Node.js, cada objeto Socket ocupa espaço no heap.
- Socket Vazio: ~2-4 KB
- Com Metadados (User ID, info do canal): ~10 KB
Matemática:
1.000.000 conexões * 10 KB = 10 GB de RAM.
Isso é viável em um servidor moderno, mas você deve ter cuidado.
Dica de Otimização:
Não guarde o objeto de Usuário completo no socket. Guarde apenas o userId e busque os detalhes no Redis quando necessário.
// RUIM socket.user = { id: 1, name: "Alice", email: "...", bio: "..." }; // BOM socket.userId = 1;
4. O Lag do Event Loop
O Node.js é single-threaded. Se você tem 1 milhão de usuários conectados e tenta transmitir uma mensagem para todos eles, você bloqueará o event loop.
// NÃO FAÇA ISSO users.forEach(user => { user.socket.send("Olá!"); });
Enviar 1 milhão de pacotes leva tempo. Se levar 0,01ms por envio, são 10 segundos de tempo de bloqueio. Seu servidor não responderá durante 10 segundos.
A Solução: Batching e Workers
- Não transmita para todos de uma vez. Divida suas transmissões em pedaços (chunks).
- Use múltiplos processos. Use o módulo
clusterdo Node.js ou rode múltiplas instâncias de contêineres.
5. Arquitetura para Escala Infinita
Um único servidor pode lidar com 1M de conexões com ajustes. Mas e quanto a 10M? 100M?
Você precisa de uma Arquitetura WebSocket Distribuída.
A Abordagem em Camadas
- Balanceador de Carga (Nginx/HAProxy): Termina SSL, distribui conexões para nós backend.
- Nós WebSocket (Node.js/Go): Mantêm as conexões ativas. São "stateful" no sentido de que têm o socket, mas "stateless" em relação à lógica de negócio.
- Camada Pub/Sub (Redis/NATS): A cola que mantém tudo unido.
Como funciona o Pub/Sub
Quando o Usuário A (conectado ao Servidor 1) quer enviar uma mensagem ao Usuário B (conectado ao Servidor 2):
- Usuário A envia mensagem ao Servidor 1.
- Servidor 1 publica a mensagem em um canal do Redis:
publish('user:B', payload). - Servidor 2 (e todos os outros servidores) estão inscritos no Redis.
- Servidor 2 recebe a mensagem, verifica se o Usuário B está conectado localmente.
- Servidor 2 envia a mensagem ao Usuário B.
Esta arquitetura permite adicionar servidores horizontalmente sem limites.
6. Testando seus Limites
Não espere a produção falhar. Você precisa testar isso.
Ferramentas como Artillery ou k6 são ótimas, mas para concorrência massiva, você pode precisar de uma frota de máquinas cliente.
Tsung é uma ferramenta de teste de carga distribuída baseada em Erlang que é excelente para simular milhões de usuários simultâneos.
Conclusão
Lidar com 1 milhão de conexões é uma medalha de honra para engenheiros de sistemas. Requer sair da zona de conforto do "npm install" e mergulhar nas entranhas do Linux.
Checklist Resumo:
- Aumentar
ulimit(Open Files). - Ajustar
sysctl(Intervalo de portas, reutilização TCP). - Otimizar Memória de Aplicação (Guardar IDs, não Objetos).
- Usar um backend Pub/Sub (Redis) para escala horizontal.
Na próxima vez que seu servidor travar sob carga, não apenas aumente o tamanho da instância. Olhe para o kernel. A resposta geralmente está lá.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit