Discord: Como Armazenar Trilhões de Mensagens — Teardown da Migração Cassandra → ScyllaDB
Ouvir estudo
gerado ao ouvirGerado apenas no primeiro play
Com tecnologia Amazon Polly + OmniVoice
O Discord migrou seu armazenamento de mensagens de Apache Cassandra para ScyllaDB, eliminando latências de cauda imprevisíveis e pausas de GC que afetavam milhões de usuários. Este teardown reconstrói a arquitetura, examina as decisões de engenharia e os trade-offs envolvidos, e apresenta minha leitura crítica do que foi feito bem — e o que eu faria diferente.
Quando você tem trilhões de mensagens e latência de cauda que quebra a experiência do usuário, trocar o banco de dados não é uma decisão de fim de semana. O Discord fez exatamente isso — e a história de como eles saíram do Cassandra para o ScyllaDB, reescreveram os data services em Rust e eliminaram pausas de GC que duravam segundos é um dos casos de engenharia de dados mais instrutivos da última década.
Fatos do Caso
- Empresa
- Discord Inc.
- Domínio
- Plataforma de comunicação em tempo real
- Escala de dados
- Trilhões de mensagens armazenadas; bilhões de inserções por dia (estimativa)
- Stack anterior
- Apache Cassandra (JVM), Python/Go data services
- Stack atual
- ScyllaDB (C++), data services em Rust
- Publicação do post original
- 2023 (blog oficial do Discord)
- Principal problema resolvido
- Latência de cauda (p99/p999) e pausas de GC no Cassandra causando degradação perceptível ao usuário
- Resultado reportado
- Redução drástica de latência de cauda; clusters menores; operação mais previsível
O Problema: Cassandra Funcionava, Mas Não o Suficiente
O Discord adotou Apache Cassandra cedo, e a escolha fazia sentido: modelo de dados wide-column, escalabilidade horizontal sem ponto único de falha, e um ecossistema maduro. Para uma plataforma de mensagens onde cada servidor do Discord (guild) tem canais, e cada canal tem um histórico de mensagens que precisa ser lido de forma eficiente por timestamp, o modelo de partição do Cassandra é quase perfeito na teoria. Você particiona por (channel_id, bucket) e clusteriza por message_id (Snowflake, monotonicamente crescente), o que dá leituras de intervalo de tempo em uma única partição. Elegante.
O problema não era o modelo de dados. Era a JVM.
O Cassandra roda na JVM e, com heaps grandes — necessários para manter índices e caches em memória para trilhões de mensagens — o garbage collector se torna o inimigo. Pausas de GC de segundos não são hipotéticas nessa escala; são rotineiras. O Discord reportou latências de p99 e p999 completamente imprevisíveis, com picos que chegavam a segundos em operações que deveriam levar milissegundos. Em uma plataforma de comunicação em tempo real, isso é perceptível. Usuários veem o histórico de mensagens demorar para carregar. A experiência quebra.
Há também o problema de compactação. O Cassandra usa LSM trees (Log-Structured Merge-tree) internamente, e o processo de compactação — necessário para manter a performance de leitura ao longo do tempo — compete com operações de produção por I/O e CPU. Em clusters grandes e sob carga contínua, janelas de compactação se tornam eventos de risco operacional. A equipe do Discord descreveu nós quentes (hot nodes) que causavam pressão de cauda no cluster inteiro, porque o Cassandra não tem isolamento de recursos por operação — um nó lento contamina o percentil de latência do cluster.
A alternativa óbvia seria tunar o GC, usar ZGC ou Shenandoah, reduzir o heap, fragmentar os clusters. A equipe tentou essas abordagens. O problema é que elas compram tempo, não resolvem a causa raiz: você está rodando um banco de dados de missão crítica em cima de um runtime com coleta de lixo não-determinística. Em algum ponto, a engenharia de tuning vira dívida técnica acumulada, e a relação custo-benefício inverte.
Arquitetura Reconstruída: Caminho de Leitura/Escrita de Mensagens
Diagrama reconstruído com base no post oficial do Discord. Mostra o fluxo de dados desde o cliente até o armazenamento, incluindo a camada de data services em Rust e o ScyllaDB como storage primário.
- Discord Client · (Web / Mobile / Desktop)
- Gateway Service · (WebSocket)
- API Service · (REST / Internal RPC)
- Message Data Service · (Rust)
- Hot Message Cache · (in-process / Redis)
- ScyllaDB Cluster · (C++, Shard-per-core)
- Cassandra Cluster · (Legacy / Migration Source)
- Data Migrator · (Dual-write / Backfill)
Como o Sistema Funciona: Modelo de Dados, ScyllaDB e a Camada Rust
O modelo de dados central permaneceu essencialmente o mesmo após a migração — e isso é um ponto importante. O Discord não precisou redesenhar o schema para mudar de Cassandra para ScyllaDB, porque o ScyllaDB é compatível com o protocolo CQL (Cassandra Query Language). A tabela de mensagens usa (channel_id, bucket) como chave de partição, onde bucket é derivado do timestamp da mensagem para evitar partições ilimitadas (um canal ativo por anos teria uma partição gigantesca sem bucketing). A chave de clusterização é o message_id, um Snowflake ID que codifica o timestamp, garantindo ordenação cronológica nativa e leituras de intervalo eficientes.
O que mudou foi a engine por baixo. O ScyllaDB é uma reimplementação do Cassandra em C++ com o framework Seastar, que usa um modelo shard-per-core: cada core da CPU tem seu próprio conjunto de dados e sua própria fila de I/O, sem compartilhamento de memória entre cores. Isso elimina a contenção de locks que é endêmica em sistemas multi-threaded com heap compartilhado. Não há GC. O gerenciamento de memória é determinístico. O resultado prático é que as latências de cauda — p99, p999 — colapsam para valores muito menores e, mais importante, muito mais previsíveis.
A reescrita dos data services em Rust foi complementar e igualmente deliberada. Os serviços anteriores em Python/Go introduziam sua própria variabilidade de latência: o GC do Go, embora muito melhor que o da JVM, ainda introduz pausas; o Python tem o GIL. Rust oferece performance determinística sem runtime de GC, com segurança de memória em tempo de compilação. Para um serviço que está no caminho crítico de cada leitura e escrita de mensagem, a escolha faz sentido técnico claro. O custo é a curva de aprendizado e a velocidade de desenvolvimento menor — trade-off que o Discord avaliou como aceitável dado o perfil de carga.
A migração em si foi conduzida com dual-write e backfill. Durante a fase de transição, novas mensagens eram escritas tanto no Cassandra quanto no ScyllaDB. Um processo de migração em background lia dados históricos do Cassandra e os escrevia no ScyllaDB. Quando a paridade de dados era confirmada, o tráfego de leitura era gradualmente desviado para o ScyllaDB. Essa abordagem é conservadora e correta para dados de missão crítica — você nunca faz big-bang migration em armazenamento de mensagens de produção. O risco de perda de dados ou inconsistência é alto demais. O custo do dual-write (dobrar a carga de escrita temporariamente) é um preço razoável pela segurança operacional.
Matriz de Decisão: Opções de Armazenamento Avaliadas
Continuar com Cassandra (tuning agressivo)
- Zero custo de migração
- Equipe já conhece o sistema
- Ecossistema maduro, tooling amplo
- GC não-determinístico é causa raiz, não sintoma
- Tuning vira dívida técnica acumulada
- Hot nodes e compactação continuam sendo riscos operacionais
Rejeitado: trata sintoma, não causa
ScyllaDB (C++, shard-per-core)
- Compatível com CQL — sem mudança de schema
- Sem GC: latência de cauda determinística
- Shard-per-core elimina contenção entre threads
- Menor footprint de cluster para mesma carga (estimativa)
- Migração de trilhões de registros é operação de alto risco
- Ecossistema menor que Cassandra
- Vendor menor — risco de suporte de longo prazo
Aceito: resolve a causa raiz com compatibilidade de protocolo
PostgreSQL / CockroachDB (SQL distribuído)
- ACID completo, queries flexíveis
- Ecossistema e tooling excelentes
- Modelo de dados relacional não é natural para wide-column de mensagens
- Escalabilidade horizontal mais complexa nessa escala
- Reescrita completa do schema e access patterns
Rejeitado: custo de migração alto sem ganho claro no caso de uso
DynamoDB / Bigtable (cloud-native)
- Operação gerenciada, sem overhead de cluster
- Escalabilidade virtualmente ilimitada
- Lock-in de cloud significativo
- Custo em escala de trilhões de registros pode ser proibitivo
- Menor controle sobre performance de cauda
Não avaliado publicamente — lock-in e custo são barreiras reais nessa escala
A Decisão pelo Rust: Determinismo Como Requisito de Sistema
A reescrita dos data services em Rust merece análise separada porque ela revela uma filosofia de engenharia importante: quando você tem um requisito de latência de cauda agressivo, cada camada do stack precisa ser avaliada como potencial fonte de jitter. O Discord não trocou apenas o banco de dados — eles auditaram o caminho crítico de ponta a ponta e identificaram que a camada de serviço também era uma fonte de variabilidade.
O argumento técnico para Rust nesse contexto é sólido. O modelo de ownership do Rust garante que não há coleta de lixo em runtime — alocações e desalocações são determinísticas e previsíveis. Para um serviço que processa milhões de operações de leitura e escrita por segundo, a ausência de pausas de GC — mesmo as sub-milissegundo do Go — se traduz em percentis de latência mais baixos e mais estáveis. Além disso, Rust tem performance próxima a C/C++ sem os riscos de segurança de memória que tornariam C++ impraticável para uma equipe de produto.
O custo real do Rust é a velocidade de desenvolvimento e o tamanho do pool de engenheiros. Rust tem uma curva de aprendizado significativa — o borrow checker é um obstáculo real para desenvolvedores vindos de linguagens com GC. Para uma empresa do tamanho do Discord, isso significa que a base de engenheiros que pode contribuir para esses serviços é menor, e o tempo de onboarding de novos engenheiros é maior. Esse é um trade-off que faz sentido para serviços de infraestrutura de baixo nível com requisitos de performance extremos, mas seria questionável para serviços de negócio de alto nível onde a velocidade de iteração é mais importante que a latência de cauda.
O que o Discord fez corretamente foi limitar o uso de Rust ao caminho crítico — os data services que ficam diretamente na frente do banco de dados. Eles não reescreveram tudo em Rust. Essa contenção é madura: você aplica a ferramenta mais cara (em termos de custo de desenvolvimento) onde o retorno é mais alto.
Leitura pelo AWS Well-Architected Framework
Segurança
O post não detalha controles de segurança, mas a arquitetura implica isolamento de rede entre camadas (data services não expostos diretamente), e o modelo shard-per-core do ScyllaDB reduz a superfície de ataque de processos compartilhados. Um ponto cego: não há menção de criptografia em trânsito entre data services e ScyllaDB, nem de controles de acesso granulares ao nível de tabela. Em sistemas financeiros ou regulados, isso seria um gap crítico.
Confiabilidade
Alta. O modelo de replicação do ScyllaDB (herdado do Cassandra) oferece tolerância a falhas de nós sem downtime. A migração via dual-write e backfill gradual é o padrão correto para zero downtime em dados críticos. O risco residual é a janela de dual-write: se houver divergência de dados entre Cassandra e ScyllaDB durante a migração, a reconciliação é complexa. Não há menção de estratégia de rollback explícita.
Eficiência de performance
Este é o pilar central do caso. A combinação ScyllaDB (sem GC, shard-per-core) + Rust (sem GC, zero-cost abstractions) elimina as duas principais fontes de jitter de latência identificadas. O modelo de dados wide-column com bucketing de timestamp é correto para o padrão de acesso (leituras de intervalo por canal). O uso de cache hot para mensagens recentes reduz a pressão sobre o ScyllaDB para o caso de acesso mais comum.
Otimização de custos
O Discord reporta clusters menores para a mesma carga após a migração — o ScyllaDB extrai mais performance por nó que o Cassandra, o que se traduz em menos hardware. O custo de migração (engenharia, dual-write, operação paralela de dois clusters) foi significativo, mas é um custo único. O custo recorrente de operação deve ser menor. A reescrita em Rust tem custo de engenharia alto, mas reduz custo de infraestrutura de forma permanente.
Sustentabilidade
Clusters menores para a mesma carga significa menor consumo de energia. A eficiência do ScyllaDB por nó e a ausência de GC (que desperdiça CPU em coleta) reduzem o footprint computacional. Não é o foco do post, mas é um benefício real.
A decisão de migrar para ScyllaDB foi correta e bem executada. Mas há três áreas onde eu teria feito escolhas diferentes ou adicionado camadas que o post não menciona. 1. Estratégia de rollback explícita durante a migração. O post descreve dual-write e backfill, mas não menciona um plano de rollback formal. Em sistemas com trilhões de registros, a janela de dual-write pode durar semanas ou meses. Durante esse período, você precisa de um processo de reconciliação automatizado que detecte divergências entre Cassandra e ScyllaDB e uma decisão clara sobre qual fonte é autoritativa em cada fase. Eu implementaria um pipeline de validação contínua que amostrasse partições aleatórias e comparasse checksums, com alertas automáticos para divergências acima de um threshold. Sem isso, você está voando às cegas durante a migração. 2. Observabilidade de cauda como first-class citizen. O problema original era latência de cauda imprevisível. Depois da migração, você precisa de instrumentação que prove que o problema foi resolvido e que detecte regressões antes que afetem usuários. Isso significa histogramas de latência (não médias) expostos via OpenTelemetry, com alertas em p99 e p999 por operação e por shard do ScyllaDB. Os serviços Rust deveriam ter tracing distribuído integrado desde o primeiro deploy em produção — não como adição posterior. 3. Bucketing dinâmico baseado em atividade do canal. O schema atual usa bucketing fixo por timestamp para evitar partições gigantescas. Isso funciona, mas é um compromisso estático.
Veredicto
O caso do Discord é um exemplo de engenharia de dados madura: identificar a causa raiz correta (não-determinismo de GC na JVM), escolher um substituto com compatibilidade de protocolo para minimizar o risco de migração (ScyllaDB + CQL), executar a migração de forma conservadora (dual-write + backfill gradual), e estender o princípio de determinismo para a camada de serviço (Rust). Cada decisão tem lógica técnica clara e os trade-offs são explicitamente reconhecidos. O que torna este caso valioso como estudo não é a tecnologia específica — ScyllaDB pode não ser a escolha certa para outros contextos — mas o framework de raciocínio: quando a latência de cauda é um requisito de sistema, você precisa eliminar fontes de não-determinismo em cada camada do stack, e isso frequentemente exige trocar conveniência de desenvolvimento por previsibilidade de runtime. A combinação de ScyllaDB + Rust é uma aposta deliberada nessa direção. O risco residual mais relevante que não foi endereçado publicamente é a dependência de um vendor menor (ScyllaDB) para infraestrutura crítica. O Cassandra tem a Apache Foundation e um ecossistema enorme; o ScyllaDB é uma empresa privada.
Post-mortems, ADRs e deep dives de arquitetura no seu email — do jeito que um arquiteto lê.
Sem spam · cancele quando quiser
Pergunte ao Fernando sobre isto
Receba uma resposta focada sobre este estudo do meu assistente de IA, baseada no meu trabalho.
Participe da conversa
Entre para comentar
Confirme seu e-mail para participar — você também recebe a newsletter. Sem senha.