Pular para o conteúdo

Arquitetura real · Pleno

Discord: de Go para Rust (picos de GC)

Arquitetura real em produção. Pontos centrais: pausas de GC.

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

Resposta curta

Arquitetura real em produção. Pontos centrais: pausas de GC. Este caso vem de um relato de engenharia da Discord sobre o serviço Read States. A função dele é acompanhar quais canais e mensagens cada pessoa já leu. Ele é acessado quando alguém se conecta, quando uma mensagem é enviada e quando uma mensagem é lida, ou seja, fica no caminho…

01 · CONTEXTO

O serviço que sabe o que você já leu

Este caso vem de um relato de engenharia da Discord sobre o serviço Read States. A função dele é acompanhar quais canais e mensagens cada pessoa já leu. Ele é acessado quando alguém se conecta, quando uma mensagem é enviada e quando uma mensagem é lida, ou seja, fica no caminho crítico do produto.

O volume de dados é grande: há bilhões de Read States, um por usuário e por canal. Cada um carrega contadores que precisam ser atualizados de forma atômica e que costumam voltar a zero, como o número de menções que você tem em um canal.

Na versão em Go, o serviço era rápido na maior parte do tempo, mas a cada poucos minutos aparecia um grande pico de latência que prejudicava a experiência. A investigação atribuiu os picos ao modelo de memória e ao coletor de lixo (GC) do Go. A portagem inicial para Rust foi concluída em maio de 2019.

02 · ARQUITETURA

Cache LRU na memória, Cassandra atrás

Cada servidor de Read States mantém um cache LRU (Least Recently Used) de Read States, para conseguir atualizações atômicas de contadores com rapidez. Segundo o relato, cada cache tem milhões de usuários, dezenas de milhões de Read States e centenas de milhares de atualizações por segundo.

Para persistência, o cache é sustentado por um cluster Cassandra. Os Read States de uma chave despejada são gravados no banco, e toda atualização também agenda uma gravação para 30 segundos depois; segundo o relato, há dezenas de milhares de gravações no banco por segundo.

  1. Os clientes acessam o Read States ao conectar, ao enviar uma mensagem e ao ler uma mensagem.

    Diagrama da arquitetura: Read States em Go: cache LRU e Cassandra
    CLClientes do Discord
    CPServidor Read States (Go)caminho crítico
    CHCache LRU de Read States
    DBCluster Cassandra
  2. O servidor mantém os Read States e seus contadores num cache LRU em memória, para atualizar contadores de forma atômica e rápida.

    Diagrama da arquitetura: Read States em Go: cache LRU e Cassandra
    CLClientes do Discord
    CPServidor Read States (Go)
    CHCache LRU de Read Statescontadores atômicos
    DBCluster Cassandra
  3. Quando uma chave é despejada do cache, os Read States dela são gravados no cluster Cassandra.

    Diagrama da arquitetura: Read States em Go: cache LRU e Cassandra
    CLClientes do Discord
    CPServidor Read States (Go)
    CHCache LRU de Read States
    DBCluster Cassandrapersistência
  4. Além do despejo, cada atualização agenda uma gravação no banco para 30 segundos depois.

    Diagrama da arquitetura: Read States em Go: cache LRU e Cassandra
    CLClientes do Discord
    CPServidor Read States (Go)
    CHCache LRU de Read Statesagenda gravação
    DBCluster Cassandrarecebe gravações

03 · O PROBLEMA

Picos periódicos e o dilema do tamanho do cache

Nos gráficos do horário de pico, a latência e o uso de CPU do serviço em Go tinham picos aproximadamente a cada 2 minutos. Os gráficos são da versão 1.9.2 do Go; as versões 1.8, 1.9 e 1.10 também foram testadas, sem melhoria.

Em Go, a memória de uma chave despejada do cache não é liberada na hora: o coletor de lixo roda de tempos em tempos para achar a memória sem referências e liberá-la, e esse trabalho pode deixar o programa mais lento. O código já era eficiente e fazia poucas alocações, então a equipe não estava gerando muito lixo.

Ao estudar o código-fonte do Go, a equipe viu que ele força uma coleta de lixo quando nenhuma rodou em 2 minutos, independentemente do crescimento do heap. Ajustar o GC percent, porém, não mudou nada, porque o serviço não alocava memória rápido o bastante para forçar coletas mais frequentes.

  1. No horário de pico, o serviço em Go mostra picos de latência e de CPU aproximadamente a cada 2 minutos.

    Diagrama da arquitetura: Read States em Go: cache LRU e Cassandra
    CLClientes do Discord
    CPServidor Read States (Go)pico periódico
    CHCache LRU de Read States
    DBCluster Cassandra
  2. O coletor de lixo precisa varrer o cache LRU inteiro para saber se a memória está livre de referências, e é isso que torna os picos tão grandes.

    Diagrama da arquitetura: Read States em Go: cache LRU e Cassandra
    CLClientes do Discord
    CPServidor Read States (Go)
    CHCache LRU de Read Statesvarrido pelo coletor
    DBCluster Cassandra

Os picos eram grandes porque o coletor precisava varrer o cache LRU inteiro para saber se a memória estava mesmo livre de referências. Com o cache menor, a coleta gerava picos menores, e a equipe passou a ter vários caches LRU particionados por servidor. A contrapartida foi uma latência maior no 99º percentil, pois é menos provável que o Read State do usuário esteja no cache e, quando não está, é preciso uma leitura no banco.

  1. A equipe passou a ter vários caches LRU particionados por servidor, e com caches menores o coletor de lixo gerava picos menores.

    Diagrama da arquitetura: Read States em Go: caches particionados
    CLClientes do Discord
    CPServidor Read States (Go)
    CHCache LRU particionado Apartição menor
    CHCache LRU particionado Bpartição menor
    DBCluster Cassandra
  2. Em contrapartida, com caches menores é menos provável que o Read State esteja na memória, então mais pedidos vão ao banco e a latência do 99º percentil sobe.

    Diagrama da arquitetura: Read States em Go: caches particionados
    CLClientes do Discord
    CPServidor Read States (Go)
    CHCache LRU particionado A
    CHCache LRU particionado B
    DBCluster Cassandramais leituras

04 · A MUDANÇA

Sem coletor de lixo: liberação imediata

O Rust não tem coleta de lixo. Ele acompanha quem pode ler e escrever na memória e aplica as regras de memória em tempo de compilação. Na versão em Rust do Read States, quando o Read State de um usuário é despejado do cache LRU, a memória é liberada na hora, sem esperar por um coletor.

A escolha desse serviço teve motivos práticos: ele era pequeno e autocontido, e a equipe queria criar os frameworks e as bibliotecas para construir novos serviços inteiramente em Rust. Havia um obstáculo, porque na época o Rust estável não tinha uma boa solução para Rust assíncrono. A equipe se comprometeu a usar o Rust nightly até o async ser totalmente suportado no estável.

05 · RESULTADOS

Teste de carga, canário e cache maior

A reescrita começou como uma tradução direta e depois foi enxugada. No teste de carga, a latência da versão em Rust ficou tão boa quanto a do Go e sem picos de latência. Mesmo com otimização básica, o Rust superou a versão Go ajustada à mão, e depois de perfilar e otimizar, a versão em Rust ficou melhor que a de Go em latência, CPU e memória.

As otimizações incluíram trocar o HashMap por um BTreeMap no cache LRU para usar menos memória, trocar a biblioteca de métricas por uma que usa a concorrência moderna do Rust e reduzir o número de cópias de memória. O lançamento foi tranquilo por causa do teste de carga.

  1. No Rust, a memória de um Read State despejado do cache é liberada imediatamente, sem esperar um coletor.

    Diagrama da arquitetura: Read States em Rust: nó canário e frota
    CLClientes do Discord
    CPNó canário Read States (Rust)
    CHCache LRU do nó canáriolibera na hora
    CPDemais servidores Read States (Rust)
    CHCache LRU dos servidores da frota
    DBCluster Cassandra
  2. O serviço foi colocado primeiro em um único nó canário.

    Diagrama da arquitetura: Read States em Rust: nó canário e frota
    CLClientes do Discord
    CPNó canário Read States (Rust)canário
    CHCache LRU do nó canário
    CPDemais servidores Read States (Rust)
    CHCache LRU dos servidores da frota
    DBCluster Cassandra
  3. No canário apareceram alguns casos de borda que faltavam, e a equipe os corrigiu.

    Diagrama da arquitetura: Read States em Rust: nó canário e frota
    CLClientes do Discord
    CPNó canário Read States (Rust)casos de borda corrigidos
    CHCache LRU do nó canário
    CPDemais servidores Read States (Rust)
    CHCache LRU dos servidores da frota
    DBCluster Cassandra
  4. Logo depois, a versão em Rust foi liberada para toda a frota.

    Diagrama da arquitetura: Read States em Rust: nó canário e frota
    CLClientes do Discord
    CPNó canário Read States (Rust)
    CHCache LRU do nó canário
    CPDemais servidores Read States (Rust)frota inteira
    CHCache LRU dos servidores da frotacache da frota
    DBCluster Cassandra
  5. Em Go, um cache maior alongava as coletas de lixo. Depois de alguns dias rodando com sucesso em Rust, e sem esse problema, a equipe aumentou a capacidade do cache LRU para 8 milhões de Read States.

    Diagrama da arquitetura: Read States em Rust: nó canário e frota
    CLClientes do Discord
    CPNó canário Read States (Rust)
    CHCache LRU do nó canário
    CPDemais servidores Read States (Rust)
    CHCache LRU dos servidores da frotacache maior
    DBCluster Cassandra

Com o cache maior, o tempo médio passou a ser medido em microssegundos e o máximo de menções em milissegundos. Atualizar o tokio, o runtime assíncrono usado pela equipe, também deu ganho de CPU sem esforço.

Coleta de lixoCache e latência
Go com cache LRU menorPicos menoresLatência maior no 99º percentil, com mais leituras no banco
Rust, antes do cache maiorSem picos de latência no teste de cargaLatência, CPU e memória melhores que no Go
Rust com cache LRU maiorSem coletor de lixo para esperar pela memória despejadaCapacidade aumentada para 8 milhões de Read States

Perguntas frequentes

O que é Discord: de Go para Rust (picos de GC)?

Arquitetura real em produção. Pontos centrais: pausas de GC.

O que o design de Discord: de Go para Rust (picos de GC) cobre?

Ele está organizado nestas seções: O serviço que sabe o que você já leu, Cache LRU na memória, Cassandra atrás, Picos periódicos e o dilema do tamanho do cache, Sem coletor de lixo: liberação imediata e Teste de carga, canário e cache maior.

Quanto tempo leva Discord: de Go para Rust (picos de GC) 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.