Como o Discord armazena trilhões de mensagens (2023)
(discord.com)- À medida que o volume de mensagens do Discord cresceu de dezenas de bilhões para trilhões, no início de 2022 ficou cada vez mais difícil sustentar a carga operacional e a latência imprevisível de um cluster Cassandra com 177 nós
- O design do Cassandra centrado em
channel_ide buckets de tempo criava partições quentes sob tráfego de leitura concentrado em servidores grandes, e as leituras e escritas com quorum faziam a latência se espalhar para um impacto mais amplo nos usuários - Ao colocar um serviço de dados em Rust entre a API e o banco de dados, a empresa reduziu a carga no banco ao mesclar requisições simultâneas para a mesma linha e usar roteamento com hash consistente baseado no ID do canal
- A migração para o ScyllaDB foi feita com um novo cluster e um migrador em Rust, reduzindo um trabalho estimado em 3 meses com Spark para cerca de 9 dias e transferindo até 3,2 milhões de mensagens por segundo
- Após a mudança em maio de 2022, o ambiente caiu de 177 nós no Cassandra para 72 nós no ScyllaDB; o p99 de leitura de mensagens antigas melhorou de 40~125 ms para 15 ms, e o p99 de inserção de mensagens de 5~70 ms para 5 ms
A crescente carga operacional do Cassandra
- Em 2017, o Discord publicou como armazenava bilhões de mensagens, compartilhando o processo de migração do MongoDB para o Cassandra
- Na época, o objetivo era ter um banco de dados escalável, resiliente a falhas e com manutenção relativamente baixa, mas, com o aumento do volume armazenado, o próprio cluster Cassandra passou a representar uma grande carga operacional
- O cluster
cassandra-messages, usado para armazenar mensagens, cresceu de 12 nós em 2017 para 177 nós no início de 2022, armazenando trilhões de mensagens - A equipe de operações era acionada com frequência por problemas no banco de dados, a latência era imprevisível e era necessário reduzir trabalhos de manutenção que haviam se tornado caros demais
Esquema de mensagens e partições quentes
- O esquema simplificado de mensagens incluía
channel_id,bucket,message_id,author_idecontent, e a chave primária era((channel_id, bucket), message_id) - Os IDs do Discord eram baseados no Snowflake, o que permitia ordenação cronológica, e as mensagens eram particionadas por canal e por uma janela de tempo estática chamada bucket
- No Cassandra, todas as mensagens de um determinado canal e bucket eram armazenadas juntas e replicadas em vários nós de acordo com o fator de replicação
- Servidores pequenos de grupos de amigos e servidores com centenas de milhares de pessoas tinham volumes de mensagens com diferenças de várias ordens de magnitude, e isso se transformava em diferenças de carga no nível da partição
- As escritas eram anexadas ao commit log e à estrutura em memória chamada memtable antes de serem descarregadas em disco, mas as leituras eram mais custosas porque podiam precisar consultar a memtable e vários SSTables
- Quando muitas leituras simultâneas se concentravam em um servidor grande, uma determinada partição se tornava uma partição quente, a latência daquele nó aumentava e até outras consultas eram afetadas
- Como leituras e escritas usavam nível de consistência por quorum, o aumento de latência nos nós responsáveis por partições quentes se espalhava para um impacto mais amplo nos usuários
Gargalos de manutenção no Cassandra
- A compaction do Cassandra também era uma causa recorrente de resposta a incidentes
- Era preciso compactar SSTables no disco para melhorar o desempenho de leitura, mas, quando a compaction atrasava, o custo de leitura aumentava
- Enquanto um nó executava compaction, a latência podia aumentar em cadeia
- O Discord executava com frequência uma operação que chamava de “gossip dance”
- Retirava um nó do tráfego para executar a compaction
- Recolocava o nó em operação para que ele recuperasse os hints do hinted handoff do Cassandra
- Repetia esse processo até eliminar o backlog de compaction
- Muito tempo também era gasto ajustando o coletor de lixo da JVM e as configurações de heap, e as pausas de GC geravam grandes picos de latência
Por que migrar para o ScyllaDB
- Além das mensagens, o Discord operava vários outros clusters Cassandra, e problemas semelhantes apareciam em cada um deles
- O ScyllaDB, que já havia despertado interesse em um texto anterior, é um banco de dados compatível com Cassandra, escrito em C++, que oferece melhor desempenho, repair mais rápido, isolamento de carga com arquitetura shard-per-core e uma estrutura sem coleta de lixo
- O ScyllaDB não era isento de problemas, mas o fato de ser escrito em C++ e não em Java, portanto sem coletor de lixo, contrastava diretamente com os problemas operacionais do Cassandra
- Depois de confirmar melhorias em experimentos e testes, o Discord decidiu migrar todos os bancos de dados para o ScyllaDB e, até 2020, já havia migrado todos, exceto o
cassandra-messages - O cluster de mensagens ficou por último por ser enorme, com trilhões de mensagens e quase 200 nós
- O desempenho de consultas reversas no ScyllaDB não atendia aos requisitos nos testes iniciais
- Consultas reversas são casos em que se faz varredura no sentido oposto à ordenação da tabela; um exemplo é ler mensagens em ordem crescente
- Com a equipe do ScyllaDB priorizando melhorias de desempenho, o fator que impedia a migração do último banco foi eliminado
Serviço de dados em Rust
- O Discord avaliou que trocar apenas o banco de dados não resolveria automaticamente todos os problemas e também mudou a camada acima do banco
- Um serviço de dados foi colocado entre o monólito da API e o cluster de banco de dados para controlar as requisições simultâneas direcionadas ao banco
- O serviço de dados foi escrito em Rust
- O Discord já havia usado Rust antes em alguns projetos
- Rust oferece velocidade de nível C/C++ sem abrir mão da segurança
- O ecossistema Tokio foi usado como base para construir um sistema de I/O assíncrono, e havia suporte a drivers para Cassandra e ScyllaDB
- O serviço de dados tinha aproximadamente um endpoint gRPC por consulta ao banco e intencionalmente não continha lógica de negócio
- O ponto central era a mesclagem de requisições
- Quando vários usuários pediam a mesma linha ao mesmo tempo, o banco era consultado apenas uma vez
- A primeira requisição criava uma worker task dentro do serviço, e as requisições seguintes verificavam a existência dessa task e se inscreviam nela
- A worker task consultava o banco e devolvia a linha resultante a todos os assinantes
Aumentando o efeito da mesclagem com roteamento
- Na frente do serviço de dados, foi aplicado roteamento baseado em hash consistente
- Cada requisição ao serviço de dados recebia uma routing key e, no caso das mensagens, essa routing key era o ID do canal
- Como todas as requisições do mesmo canal iam para a mesma instância do serviço, a mesclagem de requisições funcionava melhor
- Por exemplo, quando uma notificação
@everyoneacontecia em um servidor grande, muitos usuários abriam o app e liam a mesma mensagem, o que podia fazer o tráfego ao banco disparar - Na estrutura anterior, esse tipo de situação podia virar uma partição quente e exigir acionamento da equipe de operações, mas o serviço de dados reduziu bastante os picos de tráfego em direção ao banco
- Essa melhoria não eliminou completamente as partições quentes e a latência do cluster Cassandra, mas comprou tempo para preparar o novo cluster ScyllaDB e conduzir a migração
Migração de trilhões de mensagens
- Os requisitos da migração eram claros
- Era preciso mover trilhões de mensagens
- Não podia haver downtime
- Como o Cassandra ainda gerava respostas frequentes a incidentes, era necessário concluir tudo rapidamente
- O Discord primeiro montou um novo cluster ScyllaDB usando super-disk storage topology
- Usou SSD local para garantir velocidade
- Espelhou os dados em disco persistente via RAID
- Era uma configuração que combinava a velocidade do disco local com a durabilidade do disco persistente
- O plano inicial era usar um horário de cutover: os dados novos iriam para o ScyllaDB, enquanto os dados antigos seriam migrados em segundo plano
- Para os dados novos, o Discord começou a fazer escrita dupla em Cassandra e ScyllaDB e, ao mesmo tempo, preparou o migrador Spark do ScyllaDB
- O migrador Spark exigia muito ajuste e, depois de configurado, o tempo estimado para terminar era de 3 meses
- Depois disso, o Discord expandiu sua biblioteca rápida de banco de dados já existente e reescreveu o migrador de dados em Rust
- Lia intervalos de token do banco
- Gravava checkpoints locais em SQLite
- Enviava os dados em lote para o ScyllaDB
- Com o novo migrador em Rust, a estimativa caiu para 9 dias, e essa velocidade permitiu fazer a transição completa de uma vez, em vez de usar uma abordagem temporal mais complexa
Transição e validação
- A migração foi executada a uma velocidade de até 3,2 milhões de mensagens por segundo
- O progresso travou em 99,9999% porque os últimos intervalos de token continham enormes faixas de tombstones ainda não compactadas no Cassandra
- Depois de compactar esses intervalos de token, a migração foi concluída poucos segundos depois
- O Discord realizou validação automática de dados enviando uma pequena porcentagem das requisições de leitura para ambos os bancos de dados e comparando os resultados; não houve problemas
- O cluster ScyllaDB se saiu bem mesmo com todo o tráfego de produção, enquanto o Cassandra vinha sofrendo problemas de latência cada vez mais frequentes
- A equipe fez a virada em produção, tornando o ScyllaDB o banco de dados primário
Desempenho e escala após a mudança
- A migração do banco de dados de mensagens ocorreu em maio de 2022
- Depois disso, a operação ficou tranquila e estável, sem mais fins de semana inteiros dedicados a responder incidentes ou a ficar ajustando nós do cluster para manter o uptime
- O número de nós caiu de 177 nós Cassandra para 72 nós ScyllaDB
- O espaço em disco por nó no ScyllaDB era de 9 TB, maior que a média de 4 TB por nó no Cassandra
- A tail latency também melhorou bastante
- p99 de leitura de mensagens antigas: Cassandra 40~125 ms → ScyllaDB 15 ms
- p99 de inserção de mensagens: Cassandra 5~70 ms → ScyllaDB 5 ms
- Graças à melhora de desempenho, o Discord pôde abrir novos casos de uso de produto com base na confiança conquistada em seu banco de dados de mensagens
Capacidade de lidar com carga confirmada no tráfego da Copa do Mundo
- No fim de 2022, durante a Copa do Mundo, enquanto usuários do mundo todo assistiam aos jogos, lances de gol também apareciam no gráfico de envio de mensagens do Discord
- O gráfico de envio de mensagens da final da Copa do Mundo mostrou vários picos correspondentes aos eventos da partida
- O pênalti de Messi e a vantagem de 1 a 0 para a Argentina
- O segundo gol da Argentina
- Um plateau de 15 minutos ao longo do intervalo
- O pico quando Mbappé marcou para a França e, 90 segundos depois, marcou novamente para empatar
- O fim do tempo regulamentar e a ida para a prorrogação
- O chat durante o intervalo do primeiro tempo da prorrogação
- Mais um gol de Messi
- O gol de empate de Mbappé
- O fim da prorrogação e a ida para os pênaltis
- O erro da França e o título da Argentina
- Mesmo com um grande aumento no volume de mensagens enviadas, o serviço de dados em Rust e o ScyllaDB deram conta do tráfego
- O sistema final foi consolidado como uma arquitetura capaz de lidar com trilhões de mensagens
1 comentários
Opiniões do Hacker News
O texto parece culpar bastante o garbage collection, mas, olhando o post anterior [0], parece mais uma limitação da forma como o Cassandra era usado, do processamento de exclusões em massa pelo Cassandra, ou de ambos
Um usuário apagou milhões de mensagens via API e deixou apenas 1 mensagem no canal; como o Cassandra trata exclusões como tombstones, quando o usuário abria aquele canal, ele precisava varrer milhões de tombstones, mesmo havendo só 1 mensagem real. A explicação é que, nesse processo, o lixo era gerado mais rápido do que a JVM conseguia coletar
Também se fala de tuning de GC, mas, vendo [1], não parece que tenham feito tanto tuning assim, e aparentemente estavam usando uma versão antiga do Cassandra e provavelmente da JVM também. O fato de terem acabado de migrar do CMS também pesa bastante
0) https://discord.com/blog/how-discord-stores-billions-of-messages
Mesmo que estivessem usando mal a solução original, acho melhor uma solução que seja difícil de usar errado
Em 2022, se era Java 11, estavam usando um runtime de 4 anos; se era Java 8, um runtime de 8 anos, então é bem provável que estivessem deixando bastante desempenho na mesa
Needs (2023)
Essa camada de serviço parecia um grande e sofisticado Varnish Cache distribuído. Pelo fato de escolherem a palavra “coalesce” sem mencionar caching, parece que na prática não fazem muito caching, mas isso me lembrou o “grace mode” do Varnish e seu uso para evitar stampede, isto é, o contexto em que ouvi pela primeira vez falar de “request coalescing” https://varnish-cache.org/docs/6.1/users-guide/vcl-grace.htm...
Também é bom ver hashing consistente aparecendo várias vezes. É uma ótima tecnologia “fita adesiva”, comprovadamente útil em situações parecidas. Se você sabe onde algo deve estar, todos também sabem para onde ir procurá-lo
proxy_cache_use_stale updating;Há alguns acréscimos do cofundador do ScyllaDB: o Discord não conseguia concluir o repair com Cassandra, mas isso não acontecia no Scylla; embora o Scylla tenha muito em comum com o Cassandra, como árvores LSM e compaction, ele consegue dar às consultas prioridade maior que à compaction graças ao seu scheduler próprio de CPU/IO
Segundo ele, a compaction pode ser adiada para meio milissegundo em que haja largura de banda ociosa suficiente, e há muitos textos relacionados sobre isso. O Scylla tem um modo mais seguro chamado
tombstone_gc=repair, existente há 1,5 ano, e a nova arquitetura baseada em Raft e tablets, lançada recentemente, seria a próxima grande mudança para os usuáriosEsse problema nem teria existido se tivessem usado IRC, um protocolo de chat distribuído que existe há mais de 40 anos.
Ele também tem a vantagem de ter uma especificação aberta e várias implementações. Não é um jardim murado. Se você acha que o IRC está velho demais para os tempos atuais, pode olhar para Matrix ou XMPP. É difícil entender como o Discord acabou dominando; chega a ser quase uma tragédia.
Para acompanhar conversas em vários dispositivos, o IRC praticamente exige um bouncer. Ele também não criptografa as mensagens; no máximo, criptografa opcionalmente a conexão entre cliente e servidor. Sem criptografia de ponta a ponta, não há privacidade em relação ao servidor ou aos operadores, e esse servidor é um ponto único de falha que vira alvo facilmente.
O protocolo Matrix ainda está mudando, e as implementações não acompanham a especificação. Se você não usa o Element, fica para trás em recursos e segurança. O XMPP, assim como o IRC, depende de extensões opcionais para funcionalidades básicas como criptografia de ponta a ponta, e os clientes podem não oferecer suporte completo e correto a isso.
Recomendo ler a análise do soatok: https://soatok.blog/2024/08/04/against-xmppomemo/ https://soatok.blog/2024/08/14/security-issues-in-matrixs-ol...
O caso Snowden de 2013 foi há 11 anos. Hoje, criptografia de ponta a ponta deveria ser um recurso básico, quase uma commodity, e deveríamos exigi-la com a mesma insistência com que se exigia HTTPS. Claro que o Discord não implementa criptografia de ponta a ponta.
Recentemente, um grupo muito técnico do qual faço parte migrou do Telegram para o Matrix, e a experiência do usuário não é boa. O app tem bugs e também é feio; no novo app “Element”, SSO não era compatível, então eu nem conseguia usar minha conta. Para alguém como eu, que vai atrás e resolve as coisas, são pequenos incômodos aceitáveis, mas eu não conseguiria convencer meus amigos a usarem.
Você baixa um exe, instala, cria uma conta e já começa a usar. Qualquer pessoa consegue fazer isso. Há muito software útil e excelente, mas a maioria não é fácil para o público geral, e parte — ou a maior parte — nem sequer tem GUI. As pessoas preferem vender a própria identidade, e até pagar, a ter de suportar etapas demais.
A conclusão que tirei deste texto talvez seja um pouco diferente da intenção do autor.
“O último? Nosso amigo cassandra-messages. [...] Era um cluster grande desde o início. Com trilhões de mensagens e quase 200 nós, qualquer migração inevitavelmente seria uma tarefa complexa.”
Considerando a escala do Discord, menos de 200 nós para o armazenamento de mensagens parece surpreendentemente pouco. Eu esperava uma arquitetura complexa, mirando uma escalabilidade muito mais rápida e com mais peças móveis. A complexidade real provavelmente é maior do que aparece no texto, mas, lembrando da minha experiência sendo parcialmente responsável por mais de 200 nós físicos que faziam bem menos, fico me perguntando o quanto as arquiteturas modernas em cloud são superprojetadas.
Como alguém que já armazenou dezenas de bilhões de registros em um conjunto muito menor de nós Cassandra, posso dizer que o Cassandra era realmente doloroso para o plantão e foi a causa de algumas panes grandes.
[0] https://news.ycombinator.com/item?id=34950843
O texto é muito bem escrito. Ainda bem que parte da solução não foi introduzir algo completamente diferente, mas trocar para o ScyllaDB, que pode ser usado como substituto do Cassandra.
O Discord torna praticamente impossível apagar mensagens antigas. É um pesadelo de privacidade, e fico me perguntando por que a UE não interveio
A pergunta central se resume a saber se o Matrix deve ser visto mais como e-mail ou mais como Facebook. Se for e-mail, as pessoas ficariam horrorizadas com a ideia de o remetente poder apagar sua mensagem do meu spool de e-mail; se for Facebook, ficariam horrorizadas com a ideia de uma publicação ainda aparecer em algum lugar depois de exercerem o direito de exclusão
A explicação é que, para resolver isso, é necessário julgamento, e eles abordaram a questão considerando primeiro o que o espírito do GDPR realmente tenta alcançar
https://matrix.org/blog/2018/05/08/gdpr-compliance-in-matrix...
O problema do ponto de vista do GDPR é que o Discord dificulta a exclusão disso. Quando detecta a intenção de apagar dados da conta, ele direciona para a “anonimização”; publicamente, o nome de usuário é separado das mensagens, mas ainda é possível rastrear a pessoa específica. Se isso também for tratado assim no lado do servidor, fazer cumprir a solicitação do usuário de apagar suas informações de identificação pessoal exigiria vasculhar uma quantidade gigantesca de mensagens ou apagar mensagens antigas em massa
O Parlamento da UE não é um parlamento de verdade, no sentido de que novas leis só podem ser propostas pela Comissão e o parlamento eleito, em geral, apenas vota; e vejo quem controla a Comissão não como o público, mas como o Departamento de Estado dos EUA. A Newsguard, as big techs americanas que não são de Musk e o Discord também fazem parte da mesma rede de interesses políticos e financeiros estabelecidos, com muita gente que veio do Departamento de Estado
Sem indignação popular, instituições em nível da UE têm grande chance de ser capturadas, e considero que a opinião pública também é controlada por um establishment cyberpunk que treina LLMs com dados ilegais do Discord e mira anúncios eleitorais. No fim, devemos nos preocupar com a possibilidade de chegar a um estado orwelliano do qual não se consegue escapar de um establishment permanente
Ainda assim, considero que a maior parte disso tem apenas algo como mais de 50% de chance de estar correta em sentido amplo, então é bom filtrar bastante
O Cassandra é, em essência, algo próximo de uma tabela hash distribuída e tolerante a falhas, majoritariamente append-only. Se é exatamente disso que você precisa, com alta vazão de escrita, é uma boa escolha
Não entendo por que as pessoas o usam como se fosse um banco de dados. Você esbarra nos limites rapidamente, e a dor de tentar usá-lo como banco de dados só aumenta conforme a escala cresce
Nesse caso, acho que ele não faz sentido como armazenamento de mensagens para um servidor de chat. Parece adequado como destino de coleta de logs em sistemas distribuídos, ou seja, quando muitos clientes despejam dados, mas na maioria das vezes nem é preciso auditar os logs e o número de leituras de um item específico é menor que 1. Mensagens do Discord obviamente não são isso
Na minha experiência, nunca vi um projeto que começou usando Cassandra continuar usando-o depois de cerca de um ano. Leva mais ou menos um ano para bater nos limites, e no fim ele acaba sendo substituído por um banco de dados como o Postgres
Não entendo por que simplesmente não dá para fazer sharding. Cada “servidor” do Discord não é isolado dos outros? Se eles não podem enviar mensagens entre si, por que não dividir os trilhões de mensagens em milhares de shards, com cada shard lidando com alguns bilhões?
“A equipe do ScyllaDB priorizou melhorias e implementou consultas reversas eficientes, removendo o último obstáculo de banco de dados do plano de migração”
Fico curioso para saber quanto eles pagaram para que isso fosse feito antes mesmo de usarem o ScyllaDB