# Discord: Como Armazenar Trilhões de Mensagens — Teardown da Migração Cassandra → ScyllaDB

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.

- URL: https://fernando.moretes.com/studies/discord-trillions-of-messages

- Markdown: https://fernando.moretes.com/studies/discord-trillions-of-messages/study.md?lang=pt

- Type: Teardown

- Company: Discord

- Domain: Dados

- Date: 2023-03-06

- Tags: discord, scylladb, cassandra, rust, data-platform, migration, wide-column, messaging

- Reading time: 7 min

---

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.

### 👤 Clients

- Discord Client (Web / Mobile / Desktop) (user)

### 🌐 Edge & Gateway

- Gateway Service (WebSocket) (edge)
- API Service (REST / Internal RPC) (frontend)

### ⚙️ Data Services (Rust)

- Message Data Service (Rust) (compute)
- Hot Message Cache (in-process / Redis) (data)

### 🗄️ Storage Layer

- ScyllaDB Cluster (C++, Shard-per-core) (storage)
- Cassandra Cluster (Legacy / Migration Source) (storage)

### 🔄 Migration Path

- Data Migrator (Dual-write / Backfill) (compute)

### Fluxos

- client -> gateway: WebSocket (eventos RT)
- client -> api: REST (histórico, busca)
- gateway -> msg_service: Enviar mensagem
- api -> msg_service: Ler histórico
- msg_service -> cache: Cache read-through
- msg_service -> scylla: Leitura / Escrita primária
- migrator -> cassandra_old: Leitura (backfill)
- migrator -> scylla: Escrita (migração)
- msg_service -> cassandra_old: Dual-write (fase de migração)

## 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)

**Pros**
- Zero custo de migração
- Equipe já conhece o sistema
- Ecossistema maduro, tooling amplo

**Cons**
- 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

**Verdict:** Rejeitado: trata sintoma, não causa

### ScyllaDB (C++, shard-per-core)

**Pros**
- 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)

**Cons**
- Migração de trilhões de registros é operação de alto risco
- Ecossistema menor que Cassandra
- Vendor menor — risco de suporte de longo prazo

**Verdict:** Aceito: resolve a causa raiz com compatibilidade de protocolo

### PostgreSQL / CockroachDB (SQL distribuído)

**Pros**
- ACID completo, queries flexíveis
- Ecossistema e tooling excelentes

**Cons**
- 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

**Verdict:** Rejeitado: custo de migração alto sem ganho claro no caso de uso

### DynamoDB / Bigtable (cloud-native)

**Pros**
- Operação gerenciada, sem overhead de cluster
- Escalabilidade virtualmente ilimitada

**Cons**
- Lock-in de cloud significativo
- Custo em escala de trilhões de registros pode ser proibitivo
- Menor controle sobre performance de cauda

**Verdict:** 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

- **security**: 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.
- **reliability**: 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.
- **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.
- **cost**: 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.
- **sustainability**: 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.

> **O Que Eu Faria Diferente:** 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. Canais com altíssima atividade (servidores grandes c

## 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. Para uma plataforma do t

## Referências

- [Discord — How Discord Stores Trillions of Messages (Official Blog)](https://discord.com/blog/how-discord-stores-trillions-of-messages)

## Fontes do caso

- [Discord — How Discord stores trillions of messages](https://discord.com/blog/how-discord-stores-trillions-of-messages)
