Pular para o conteúdo

Arquitetura real · Pleno

Discord: Elixir com 5 milhões de usuários simultâneos

Arquitetura real em produção. Pontos centrais: WebSocket e presença e consistent hashing.

15 min de leitura · Fonte: Discord · Conteúdo revisado em 23 de outubro de 2026

Resposta curta

Arquitetura real em produção. Pontos centrais: WebSocket e presença e consistent hashing. Segundo o próprio Discord, quase tudo o que o produto faz se resume a publicação e assinatura (pub/sub). O usuário abre uma conexão WebSocket e ganha um processo de sessão (um GenServer).

01 · CONTEXTO

Um produto de tempo real feito de pub/sub

Segundo o próprio Discord, quase tudo o que o produto faz se resume a publicação e assinatura (pub/sub). O usuário abre uma conexão WebSocket e ganha um processo de sessão (um GenServer). Esse processo conversa com nós Erlang remotos que hospedam os processos de guild, o nome interno de um servidor do Discord.

Tudo o que é publicado numa guild é distribuído a toda sessão conectada a ela. Quando um usuário entra, a guild também publica a presença dele para as demais sessões conectadas.

  1. O cliente abre uma conexão WebSocket com o gateway, e a conexão ganha um processo de sessão.

    Diagrama da arquitetura: Discord: sessões e guilds
    CLCliente Discord
    CPGateway de sessões (Elixir)uma sessão por conexão
    CPNós de guild (Elixir)
    CPAPI em Python
  2. A sessão conversa com os nós remotos que hospedam os processos de guild.

    Diagrama da arquitetura: Discord: sessões e guilds
    CLCliente Discord
    CPGateway de sessões (Elixir)
    CPNós de guild (Elixir)processos de guild
    CPAPI em Python
  3. Quando algo é publicado numa guild, o evento é distribuído a cada sessão conectada a ela.

    Diagrama da arquitetura: Discord: sessões e guilds
    CLCliente Discord
    CPGateway de sessões (Elixir)
    CPNós de guild (Elixir)publica
    CPAPI em Python
  4. Quando um usuário entra, a guild publica a presença dele para as outras sessões conectadas.

    Diagrama da arquitetura: Discord: sessões e guilds
    CLCliente Discord
    CPGateway de sessões (Elixir)
    CPNós de guild (Elixir)publica presença
    CPAPI em Python
  5. A API do produto era servida por Python, separada do gateway em tempo real.

    Diagrama da arquitetura: Discord: sessões e guilds
    CLCliente Discord
    CPGateway de sessões (Elixir)
    CPNós de guild (Elixir)
    CPAPI em PythonAPI em Python

Em 2015 o Discord escolheu Elixir e Python como linguagens principais. O Elixir ficou com o gateway WebSocket, responsável por retransmitir mensagens e replicar em tempo real, e o Python com a API. Segundo o estudo de caso, a API Python era um monólito e a pilha Elixir reunia cerca de vinte serviços.

02 · FANOUT

Quando uma guild grande não consegue publicar

O desenho funcionou enquanto o Discord atendia grupos de até 25 pessoas. Depois surgiram servidores com até 30 mil usuários simultâneos. No pico, os processos de guild deixaram de acompanhar suas filas de mensagens, e foi preciso desligar manualmente funcionalidades que geravam mensagens para aliviar a carga.

O time fez benchmark dos caminhos quentes dos processos de guild e achou um culpado óbvio: enviar mensagens entre processos Erlang custava mais do que o esperado. Uma única chamada a send/2 levava de 30 a 70 microssegundos de tempo real, porque o Erlang descalonava o processo chamador, e publicar um evento de uma guild grande no pico podia levar de 900 ms a 2,1 s. Como processos Erlang são efetivamente single-thread, a forma de paralelizar é particioná-los.

A primeira ideia foi criar um processo por publicação, mas ela foi descartada: cada publicação poderia ser agendada em momento diferente, e os clientes dependem da linearizabilidade dos eventos.

  1. Enviar mensagens entre processos Erlang custava mais do que o esperado: uma única chamada a send/2 levava de 30 a 70 microssegundos de tempo real.

    Diagrama da arquitetura: Discord: fanout com Manifold
    CPProcesso da guildcusto de envio
    CPManifold (lado emissor)
    CPManifold.Partitioner (nó remoto)
    CPWorkers do partitioner
    CPProcessos de sessão
  2. Numa guild grande, publicar um evento no pico podia levar de 900 ms a 2,1 s.

    Diagrama da arquitetura: Discord: fanout com Manifold
    CPProcesso da guildlento no pico
    CPManifold (lado emissor)
    CPManifold.Partitioner (nó remoto)
    CPWorkers do partitioner
    CPProcessos de sessão
  3. O Manifold recebe os destinatários e os agrupa pelo nó remoto onde cada um vive.

    Diagrama da arquitetura: Discord: fanout com Manifold
    CPProcesso da guild
    CPManifold (lado emissor)agrupa por nó
    CPManifold.Partitioner (nó remoto)
    CPWorkers do partitioner
    CPProcessos de sessão
  4. Em vez de um envio por destinatário, o emissor chama send/2 no máximo uma vez por nó remoto, rumo ao partitioner daquele nó.

    Diagrama da arquitetura: Discord: fanout com Manifold
    CPProcesso da guild
    CPManifold (lado emissor)
    CPManifold.Partitioner (nó remoto)um envio por nó
    CPWorkers do partitioner
    CPProcessos de sessão
  5. No nó remoto, o partitioner aplica hash consistente aos PIDs, agrupa por número de núcleos e envia a workers filhos.

    Diagrama da arquitetura: Discord: fanout com Manifold
    CPProcesso da guild
    CPManifold (lado emissor)
    CPManifold.Partitioner (nó remoto)
    CPWorkers do partitionerentregam
    CPProcessos de sessão
  6. Os workers entregam as mensagens aos processos reais, e a linearizabilidade garantida por send/2 se mantém.

    Diagrama da arquitetura: Discord: fanout com Manifold
    CPProcesso da guild
    CPManifold (lado emissor)
    CPManifold.Partitioner (nó remoto)
    CPWorkers do partitioner
    CPProcessos de sessãorecebem o evento

03 · HASH CONSISTENTE

O anel que diz onde cada guild vive

O Discord é um sistema distribuído obtido por hash consistente, o que exige uma estrutura em anel para descobrir o nó responsável por uma entidade. O problema apareceu em rajadas de reconexão: o processo dono do anel não dava conta dos pedidos e o sistema inteiro sobrecarregava.

Os números do caminho quente, segundo o post: um usuário médio está em 5 guilds, uma VM Erlang de sessões pode ter até 500 mil sessões ativas, e ao conectar cada sessão consulta o nó remoto de cada guild de interesse. Um request/reply entre processos custa cerca de 12 microssegundos e, se o servidor de sessões caísse e reiniciasse, só as consultas ao anel levariam cerca de 30 segundos.

  1. Ao conectar, a sessão consulta o anel para descobrir o nó remoto de cada guild de seu interesse.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistenteconsulta por guild
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidos
  2. Em rajadas de reconexão, o processo dono do anel não acompanhava os pedidos e o sistema todo sobrecarregava.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistentedono do anel saturado
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidos
  3. Mover o anel para ETS melhorou o desempenho, mas copiar a estrutura grande para dentro e fora do ETS era a maior parte do custo.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistentecusto de cópia
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidos
  4. Com a técnica do mochiglobal, adotada na versão chamada FastGlobal, os dados viram uma constante numa heap compartilhada somente leitura, lida sem cópia.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistenteFastGlobal
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidos
AbordagemCusto por consultaTempo total das consultas
Processo dono do anelcerca de 12 microssegundos por request/replycerca de 30 segundos num restart
Anel copiado em ETScerca de 7 microssegundos por leitura17,5 segundos
Módulo constante (mochiglobal/FastGlobal)0,3 microssegundo750 ms

04 · CONCORRÊNCIA LIMITADA

Acelerar o caminho moveu o gargalo

Depois de acelerar a busca do nó, os processos de registro de guilds nos nós de guild começaram a acumular fila. A lentidão da busca funcionava antes como contrapressão que os protegia; o problema passou a ser que quase 5 milhões de processos de sessão disputavam dez processos de registro, um em cada nó de guild.

O pior era o ciclo: a chamada da sessão estourava o timeout e deixava a requisição na fila do registro. A sessão repetia o pedido após um backoff, e os pedidos se acumulavam até um estado irrecuperável. As filas de mensagens das sessões inflavam até a VM Erlang inteira ficar sem memória, com quedas em cascata.

  1. Quase 5 milhões de sessões disputavam dez processos de registro de guilds, um em cada nó de guild.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistente
    CPRegistro de guilds (um por nó)registro disputado
    CHContador atômico de pedidos
  2. A chamada estourava o timeout e a requisição ficava na fila do registro; o retry com backoff acumulava pedidos até um estado irrecuperável.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistente
    CPRegistro de guilds (um por nó)fila acumulada
    CHContador atômico de pedidos
  3. As filas de mensagens das sessões inflavam até estourar a memória da VM Erlang, com quedas em cascata.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessãosem memória
    CHAnel de hash consistente
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidos
  4. A solução foi um semáforo: um contador atômico em ETS de pedidos pendentes, para a sessão desistir cedo quando o número está alto.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessão
    CHAnel de hash consistente
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidoscontador atômico
  5. Quando o serviço de presença caiu por um problema não relacionado, as sessões não oscilaram.

    Diagrama da arquitetura: Discord: anel e registro de guilds
    CPProcessos de sessãonão oscilam
    CHAnel de hash consistente
    CPRegistro de guilds (um por nó)
    CHContador atômico de pedidos

O Discord descartou um circuit breaker porque não queria que uma rajada de timeouts levasse a um estado temporário em que nenhuma tentativa é feita. Em outras linguagens, um contador atômico de pedidos pendentes permitiria desistir cedo, o que na prática é um semáforo. No Erlang, a peça fundamental foi o :ets.update_counter/4, que faz incremento condicional atômico sobre um número guardado numa chave ETS.

05 · LIÇÕES E LIMITES

O que levar para o seu desenho

O estudo de caso do Elixir acrescenta peças do ecossistema: o Discord usa Cowboy para conexões WebSocket e GenStage para back-pressure e load-shedding, e monta uma malha parcial entre serviços Elixir, com etcd para descoberta de serviços e configuração compartilhada. Segundo o estudo de caso, a equipe de infraestrutura de chat é formada por cinco engenheiros.

Perguntas frequentes

O que é Discord: Elixir com 5 milhões de usuários simultâneos?

Arquitetura real em produção. Pontos centrais: WebSocket e presença e consistent hashing.

O que o design de Discord: Elixir com 5 milhões de usuários simultâneos cobre?

Ele está organizado nestas seções: Um produto de tempo real feito de pub/sub, Quando uma guild grande não consegue publicar, O anel que diz onde cada guild vive, Acelerar o caminho moveu o gargalo e O que levar para o seu desenho.

Quanto tempo leva Discord: Elixir com 5 milhões de usuários simultâneos e qual é o nível?

Leva cerca de 15 minutos para ler e tem nível Pleno.

Agora projete você mesmo

Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.