Diagrama de arquitetura de um app de chat: WebSockets, fan-out e armazenamento

O diagrama de arquitetura de um app de chat: gateways WebSocket, ordem das mensagens, envios idempotentes, fan-out, presença e armazenamento.

· Fundador e desenvolvedor

Publicado em

Diagrama de arquitetura de um app de chat: WebSockets, fan-out e armazenamento

Um app de chat é a pergunta de system design que mais esconde estado. Cada usuário mantém uma conexão ativa com um servidor específico, cada mensagem precisa chegar exatamente uma vez e na ordem, e a mesma mensagem pode precisar alcançar uma pessoa ou dez mil. Um diagrama com “cliente, servidor, banco de dados” não responde nada disso.

Este artigo percorre um backend de chat uma decisão por vez: como os clientes se conectam, como uma mensagem ganha seu lugar em uma conversa, como ela chega a todos que devem vê-la e como é armazenada. Ele termina com um diagrama de arquitetura que você pode usar como ponto de partida.

Requisitos

  • Conversas individuais e em grupo, com texto e mídia.
  • As mensagens chegam na mesma ordem para todos os membros de uma conversa.
  • Nenhuma duplicata quando um cliente reenvia uma mensagem.
  • Usuários online recebem as mensagens em menos de um segundo; usuários offline recebem uma notificação push e se atualizam ao reconectar.
  • Confirmações de entrega e de leitura, e um indicador de online.

Conexões: por que WebSockets e por que gateways

Fazer polling atrás de mensagens novas desperdiça requisições e aumenta o atraso. Uma conexão WebSocket fica aberta, então o servidor pode enviar uma mensagem no instante em que ela existe. Cada dispositivo mantém uma conexão com um gateway de chat.

Os gateways são a parte com estado do sistema. Um gateway sabe quais usuários estão conectados a ele e mais nada. Mantenha-os enxutos: autenticar a conexão uma vez, receber mensagens e entregá-las a um serviço sem estado, e empurrar as mensagens de saída pelo socket certo. Quando você faz deploy de uma nova versão do gateway, as conexões caem e os clientes reconectam; por isso os clientes precisam reconectar com backoff e jitter e retomar a partir da última mensagem que viram.

Coloque na frente um balanceador de carga de camada 4, ou um que entenda upgrades para WebSocket, e espere que as conexões longas deixem a carga desigual. Conexões novas devem preferir o gateway menos carregado.

Enviando uma mensagem: idempotência e ordem

O cliente gera um client_msg_id (um UUID) antes de enviar. Se a conexão cair antes de chegar a confirmação, o cliente reenvia com o mesmo ID, e o serviço de mensagens descarta a duplicata. Sem ele, redes móveis instáveis produzem mensagens em dobro.

A ordem é por conversa, não global. O serviço de mensagens atribui a cada mensagem o próximo número de sequência da conversa e a grava. Os clientes ordenam por esse número, nunca pelo próprio relógio. Dois jeitos práticos de atribuí-lo:

  • Um contador por conversa, incrementado na mesma escrita que grava a mensagem.
  • Um ID ordenável no tempo, como um ID no estilo Snowflake gerado pelo servidor, quando você aceita que “ordem” signifique “a ordem que o servidor viu”.

O número de sequência também barateia a atualização. Um cliente que reconecta diz “tenho a conversa 42 até a 1871” e recebe tudo o que vem depois.

Fan-out: online, offline e grupos grandes

Depois que a mensagem é gravada, o serviço publica um evento. Os workers de fan-out assumem a partir daí:

  1. Buscam os membros da conversa.
  2. Buscam quais membros estão online e qual gateway mantém a conexão deles, em um registro de presença e sessões como o Redis.
  3. Enviam a mensagem a esses gateways, que a empurram pelos sockets.
  4. Enviam notificações push via APNs e FCM aos membros offline.

Gravar a mensagem uma vez e espalhar as leituras mantém o armazenamento linear no número de mensagens, não de membros. Grupos grandes pedem cuidado: um canal com 100.000 membros não deve disparar 100.000 notificações push por mensagem. Notifique só quem optou por isso, ou agrupe e resuma.

Presença

Presença é um heartbeat com TTL. O gateway renova “o usuário 7 está conectado ao gateway 3” a cada 30 segundos, mais ou menos; quando a chave expira, o usuário está offline. Não transmita cada mudança de presença para cada contato em tempo real. É um dos recursos mais caros de um chat, considerando o quão pouco os usuários percebem atrasos nele. Busque a presença quando uma conversa é aberta e atualize-a periodicamente. Chaves que expiram funcionam como entradas de cache; o artigo sobre estratégias de cache cobre os TTLs e o que acontece quando todos expiram ao mesmo tempo.

Confirmações e criptografia de ponta a ponta

Confirmações de entrega e de leitura também são mensagens. Quando um dispositivo recebe a mensagem 1871, ele envia um pequeno evento “entregue até 1871”; quando o usuário abre a conversa, envia “lida até 1871”. Guarde o maior número por membro e conversa em vez de uma confirmação por mensagem, e o custo fica estável em grupos movimentados.

A criptografia de ponta a ponta muda o que o servidor pode ver. Com um protocolo como o do Signal, o servidor armazena e repassa texto cifrado, então busca, moderação e prévias de links vão para os clientes. Decida cedo, porque isso muda tanto o armazenamento quanto o fan-out.

Armazenamento

As mensagens são gravadas uma vez, lidas principalmente quando são recentes e quase sempre lidas por conversa. Isso aponta para um armazenamento particionado por conversa e ordenado pelo número de sequência. O Discord, por exemplo, particiona as mensagens por canal mais uma faixa de tempo fixa, para que um canal movimentado não vire uma partição enorme; o artigo sobre como ele armazena trilhões de mensagens descreve a migração desse modelo do Cassandra para o ScyllaDB.

Mantenha a mídia fora do armazenamento de mensagens. Os clientes enviam imagens e vídeos direto para um armazenamento de objetos com uma URL pré-assinada, e a mensagem carrega uma referência. O serviço de mensagens nunca lida com os bytes.

O diagrama de arquitetura

Os usuários se conectam por WebSocket, via balanceador de carga, a gateways de chat. Os gateways passam as mensagens a um serviço que as grava e publica eventos. Workers de fan-out checam a presença no Redis, entregam pelos gateways e mandam push para quem está offline.

11 componentes · 11 conexões

Abrir no Archboard
Conteúdo do diagrama
  • Users: mobile and web
  • Load balancer
  • Chat gateway
  • Message service
  • Message store
  • Message events
  • Fan-out workers
  • Presence and sessions
  • APNs and FCM
  • Media bucket
  • wss
  • send
  • append, next seq
  • event
  • who is online, which gateway
  • deliver
  • offline
  • heartbeat
  • pre-signed upload
Um backend de chat com gateways WebSocket com estado e um serviço de mensagens sem estado. Cada mensagem recebe um número de sequência por conversa, é gravada uma vez e chega aos membros online pelo gateway deles e aos offline por push.

Como ler:

  • Esquerda: os usuários se conectam por WebSocket (wss), via balanceador de carga, a um gateway de chat. Os uploads de mídia vão direto para o bucket.
  • Meio: o gateway entrega cada envio ao serviço de mensagens, que descarta duplicatas pelo client_msg_id, atribui o próximo número de sequência e anexa a mensagem ao armazenamento.
  • Direita: o serviço publica um evento. Os workers de fan-out checam presença e sessões, entregam pelo gateway certo e mandam uma notificação push aos membros offline.
  • O heartbeat do gateway para a presença é pontilhado porque roda por timer, não por mensagem.
  • O bloco Markdown mostra o envelope da mensagem, para que todos concordem com os campos.

O que rotular

  • Quais componentes guardam estado. Os gateways guardam conexões; o serviço de mensagens não guarda nada. Isso decide como você faz deploy e escala cada um.
  • A regra de ordem. “Sequência por conversa, atribuída pelo servidor” deve estar no diagrama.
  • A chave de deduplicação. Escreva client_msg_id no caminho de envio.
  • Os caminhos online e offline. Desenhe os dois; a maioria dos bugs mora no segundo.

O artigo sobre arquitetura orientada a eventos aprofunda o lado de eventos e consumidores, e o exemplo de diagrama de sequência é uma boa forma de documentar passo a passo a troca entre envio e confirmação.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione uma Pessoa do pack Modelo C4 para os usuários e um Balanceador de carga na nuvem do pack Nuvem.
  3. Adicione três nós Contêiner do pack Modelo C4: o gateway de chat, o serviço de mensagens e os workers de fan-out. Anote em cada um se ele guarda estado.
  4. Adicione um Banco de dados do pack Modelo C4 para o armazenamento de mensagens, Redis e Kafka do pack Dados para presença e eventos, um nó Armazenamento de objetos do pack Nuvem para a mídia e um Sistema externo para APNs e FCM.
  5. Conecte-os, desenhe o heartbeat e o upload de mídia como arestas tracejadas ou pontilhadas e adicione um bloco Markdown do pack Básico com o envelope da mensagem.

O exemplo usa 11 nós de diagrama e fica dentro do plano Free.

Perguntas frequentes

Preciso de WebSockets para um app de chat? Para entrega em tempo real, sim, ou um canal push parecido, como server-sent events mais HTTP para os envios. Long polling funciona em apps pequenos, mas gasta mais requisições e aumenta o atraso.

Como mantenho as mensagens em ordem? Deixe o servidor atribuir um número de sequência por conversa ao gravar a mensagem e faça os clientes ordenarem por ele. Os relógios dos clientes discordam e não servem para ordenar.

Como evito mensagens duplicadas? O cliente gera um ID de mensagem antes de enviar e o reutiliza nas novas tentativas. O servidor guarda o ID e ignora uma segunda mensagem com o mesmo.

Que banco de dados deve guardar as mensagens do chat? Um que leia rápido por conversa e ordene por sequência, como um armazenamento wide-column particionado por conversa e faixa de tempo. Para apps menores, o Postgres funciona bem com um índice em conversa e sequência.

Como as mensagens chegam a usuários offline? Elas são gravadas como qualquer outra mensagem. A etapa de fan-out envia uma notificação push e, quando o app reconecta, ele pede tudo o que veio depois do último número de sequência que tem.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.