Back

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:

  1. Aumente o intervalo de portas locais:

    # /etc/sysctl.conf net.ipv4.ip_local_port_range = 1024 65535
  2. Habilite a Reutilização de TCP:

    net.ipv4.tcp_tw_reuse = 1

    Isso permite ao SO reclamar portas no estado TIME_WAIT mais 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

  1. Não transmita para todos de uma vez. Divida suas transmissões em pedaços (chunks).
  2. Use múltiplos processos. Use o módulo cluster do 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

  1. Balanceador de Carga (Nginx/HAProxy): Termina SSL, distribui conexões para nós backend.
  2. 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.
  3. 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):

  1. Usuário A envia mensagem ao Servidor 1.
  2. Servidor 1 publica a mensagem em um canal do Redis: publish('user:B', payload).
  3. Servidor 2 (e todos os outros servidores) estão inscritos no Redis.
  4. Servidor 2 recebe a mensagem, verifica se o Usuário B está conectado localmente.
  5. 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:

  1. Aumentar ulimit (Open Files).
  2. Ajustar sysctl (Intervalo de portas, reutilização TCP).
  3. Otimizar Memória de Aplicação (Guardar IDs, não Objetos).
  4. 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á.

System DesignWebSocketsPerformanceLinuxNode.js

Explore ferramentas relacionadas

Experimente estas ferramentas gratuitas do Pockit