Cómo Discord almacena billones de mensajes (2023)
(discord.com)- A medida que el volumen de mensajes almacenados por Discord creció de miles de millones a billones, para inicios de 2022 la carga operativa y la latencia impredecible de un clúster Cassandra de 177 nodos se volvieron difíciles de sostener
- El diseño de Cassandra centrado en
channel_idy buckets de tiempo generaba particiones calientes bajo tráfico de lectura concentrado en servidores grandes, y la latencia se propagaba a un impacto más amplio en los usuarios por las lecturas y escrituras con quorum - Se colocó un servicio de datos basado en Rust entre la API y la base de datos, que fusiona solicitudes concurrentes a la misma fila y reduce la carga sobre la base de datos mediante enrutamiento con hashing consistente basado en ID de canal
- La migración a ScyllaDB se hizo con un nuevo clúster y un migrador en Rust, reduciendo una tarea que con Spark se estimaba en 3 meses a unos 9 días, y migrando hasta 3.2 millones de mensajes por segundo
- Tras el cambio en mayo de 2022, los nodos bajaron de 177 en Cassandra a 72 en ScyllaDB; el p99 de consultas de mensajes antiguos mejoró de 40–125 ms a 15 ms, y el p99 de inserción de mensajes pasó de 5–70 ms a 5 ms
La creciente carga operativa de Cassandra
- En 2017, Discord publicó cómo almacenaba miles de millones de mensajes y compartió el proceso de migración de MongoDB a Cassandra
- En ese momento, el objetivo era una base de datos escalable, resistente a fallas y con relativamente poco mantenimiento, pero a medida que creció el volumen almacenado, el propio clúster Cassandra se convirtió en una gran carga operativa
- El clúster
cassandra-messagespara almacenar mensajes pasó de 12 nodos en 2017 a 177 nodos a inicios de 2022, y almacenaba billones de mensajes - El equipo de operaciones recibía llamados frecuentes por problemas de base de datos, la latencia era impredecible y era necesario reducir tareas de mantenimiento que se habían vuelto demasiado costosas
Esquema de mensajes y particiones calientes
- El esquema simplificado de mensajes incluía
channel_id,bucket,message_id,author_idycontent, y la clave primaria era((channel_id, bucket), message_id) - Los ID de Discord están basados en Snowflake, por lo que pueden ordenarse cronológicamente, y los mensajes se particionaban por canal y por una ventana de tiempo estática llamada bucket
- En Cassandra, todos los mensajes de un canal y bucket específicos se almacenan juntos y se replican en varios nodos según el factor de replicación
- Entre servidores de pequeños grupos de amigos y servidores de cientos de miles de personas había diferencias de varios órdenes de magnitud en la cantidad de mensajes, y esa diferencia se traducía en diferencias de carga a nivel de partición
- Las escrituras se agregan al commit log y a una estructura en memoria llamada memtable, y luego se vacían a disco; las lecturas, en cambio, pueden consultar la memtable y múltiples SSTable, por lo que son más costosas
- Cuando se concentraban lecturas concurrentes en servidores grandes, una partición específica se convertía en una partición caliente, aumentaba la latencia en ese nodo y también se veían afectadas otras consultas
- Como se usaba un nivel de consistencia quorum para lecturas y escrituras, el aumento de latencia en los nodos a cargo de una partición caliente se propagaba a un impacto más amplio en los usuarios
Cuellos de botella de mantenimiento en Cassandra
- La compactación de Cassandra también se convirtió en una causa recurrente de respuesta a incidentes
- Para que el rendimiento de lectura mejore, las SSTable deben compactarse en disco; si la compactación se atrasa, aumenta el costo de lectura
- Mientras un nodo ejecutaba compactación, la latencia podía aumentar en cadena
- Discord realizaba con frecuencia una operación a la que llamaba “gossip dance”
- Sacaba un nodo del tráfico para ejecutar la compactación
- Lo reincorporaba para que se pusiera al día con los hints de Cassandra hinted handoff
- Repetía este proceso hasta que desapareciera el backlog de compactación
- También dedicaban mucho tiempo a ajustar el recolector de basura de la JVM y la configuración del heap, y las pausas de GC generaban grandes picos de latencia
Por qué migraron a ScyllaDB
- Además de mensajes, Discord operaba varios clústeres Cassandra, y en cada uno aparecían problemas similares
- ScyllaDB, por el que habían mostrado interés en el artículo anterior, es una base de datos compatible con Cassandra escrita en C++; ofrece mejor rendimiento, repairs más rápidos, aislamiento de cargas basado en una arquitectura shard-per-core y una estructura sin recolección de basura
- Aunque ScyllaDB no estaba libre de problemas, el hecho de estar escrita en C++ y no en Java, por lo que no tenía recolector de basura, contrastaba directamente con los problemas operativos de Cassandra
- Tras confirmar mejoras en experimentos y pruebas, Discord decidió migrar todas sus bases de datos a ScyllaDB, y para 2020 ya había migrado todas salvo
cassandra-messages - El clúster de mensajes quedó para el final porque era un clúster grande, con billones de mensajes y casi 200 nodos
- El rendimiento de ScyllaDB para consultas inversas no cumplía con los requisitos en las pruebas iniciales
- Una consulta inversa es un escaneo en la dirección opuesta al orden de la tabla; un ejemplo es escanear mensajes en orden ascendente
- Cuando el equipo de ScyllaDB priorizó e implementó mejoras de rendimiento, se eliminó el factor que impedía la última migración de base de datos
Servicio de datos basado en Rust
- Discord consideró que cambiar solo la base de datos no resolvería automáticamente todos los problemas, así que también cambió la capa superior a la base de datos
- Colocó un servicio de datos entre el monolito de la API y el clúster de base de datos para controlar las solicitudes concurrentes hacia la base de datos
- El servicio de datos fue escrito en Rust
- Discord ya había usado Rust antes en algunos proyectos
- Rust ofrece velocidad de nivel C/C++ sin renunciar a la seguridad
- El ecosistema Tokio se usó como base para construir el sistema de I/O asíncrono, y también había soporte de drivers para Cassandra y ScyllaDB
- El servicio de datos tiene aproximadamente un endpoint gRPC por consulta de base de datos y, de forma deliberada, no contiene lógica de negocio
- La clave es la fusión de solicitudes
- Si varios usuarios solicitan simultáneamente la misma row, la base de datos se consulta una sola vez
- La primera solicitud crea una worker task dentro del servicio, y las solicitudes posteriores detectan la existencia de esa task y se suscriben
- La worker task consulta la base de datos y devuelve las rows resultantes a todos los suscriptores
Aumentar el efecto de fusión mediante enrutamiento
- Delante del servicio de datos se aplicó enrutamiento basado en hashing consistente
- Cada solicitud al servicio de datos recibe una routing key; en el caso de mensajes, la routing key es el ID del canal
- Como todas las solicitudes del mismo canal van a la misma instancia del servicio, la fusión de solicitudes funciona mejor
- Por ejemplo, si se produce una notificación
@everyoneen un servidor grande, muchos usuarios pueden abrir la app y leer el mismo mensaje, generando un pico de tráfico hacia la base de datos - En la arquitectura anterior, una situación así podía derivar en una partición caliente y requerir un llamado al equipo de operaciones, pero el servicio de datos reduce de forma significativa los picos de tráfico hacia la base de datos
- Esta mejora no eliminó por completo las particiones calientes ni la latencia del clúster Cassandra, pero dio tiempo para preparar el nuevo clúster ScyllaDB y ejecutar la migración
Migración de billones de mensajes
- Los requisitos de la migración eran claros
- Había que migrar billones de mensajes
- No podía haber downtime
- Cassandra seguía generando respuestas frecuentes a incidentes, así que había que terminar rápido
- Discord primero configuró un nuevo clúster ScyllaDB que usaba super-disk storage topology
- Aseguraba velocidad con Local SSD
- Replicaba los datos a persistent disk mediante RAID
- Era una configuración que combinaba la velocidad del disco local con la durabilidad del persistent disk
- El plan inicial era que, a partir del cutover time, los datos nuevos usaran ScyllaDB y los datos antiguos se migraran en segundo plano
- Se empezó a hacer doble escritura de datos nuevos en Cassandra y ScyllaDB, mientras se preparaba el Spark migrator de ScyllaDB
- El Spark migrator requería mucho tuning y, después de configurarlo, el tiempo estimado de finalización era de 3 meses
- Luego Discord extendió su biblioteca rápida de base de datos existente y reescribió el migrador de datos en Rust
- Leía token ranges desde la base de datos
- Registraba checkpoints locales en SQLite
- Enviaba datos masivamente a ScyllaDB
- El tiempo estimado de finalización del nuevo migrador en Rust se redujo a 9 días, y gracias a esa velocidad fue posible hacer todo el cambio de una sola vez en lugar de usar un enfoque complejo basado en tiempo
Cambio y verificación
- La migración avanzó a una velocidad máxima de 3.2 millones de mensajes por segundo
- El progreso se detuvo en 99.9999%, porque en los últimos token ranges había enormes rangos de tombstones no compactados en Cassandra
- Al compactar esos token ranges, la migración terminó unos segundos después
- Discord realizó verificación automática de datos enviando un pequeño porcentaje de solicitudes de lectura a ambas bases de datos y comparando los resultados, que no presentaron problemas
- El clúster ScyllaDB resistió bien todo el tráfico de producción, mientras Cassandra sufría problemas de latencia cada vez más frecuentes
- El equipo cambió en vivo ScyllaDB para que fuera la base de datos primaria
Rendimiento y escala después del cambio
- La migración de la base de datos de mensajes se realizó en mayo de 2022
- Desde entonces, la operación fue tranquila y estable; dejaron de pasar fines de semana enteros respondiendo incidentes o ajustando nodos del clúster para mantener el uptime
- La cantidad de nodos bajó de 177 nodos Cassandra a 72 nodos ScyllaDB
- El espacio en disco por nodo ScyllaDB era de 9 TB, más que el promedio de 4 TB por nodo Cassandra
- La tail latency también mejoró de forma importante
- p99 de consulta de mensajes antiguos: Cassandra 40–125 ms → ScyllaDB 15 ms
- p99 de inserción de mensajes: Cassandra 5–70 ms → ScyllaDB 5 ms
- Gracias a la mejora de rendimiento, Discord pudo habilitar nuevos casos de uso de producto con confianza en la base de datos de mensajes
Manejo de carga comprobado durante el tráfico del World Cup
- A fines de 2022, durante el World Cup, usuarios de todo el mundo veían los partidos y los goles también aparecían en la gráfica de envío de mensajes de Discord
- En la gráfica de envío de mensajes de la World Cup Final aparecieron varios picos correspondientes a eventos del partido
- Penal de Messi y ventaja 1-0 de Argentina
- Gol adicional de Argentina
- Plateau de 15 minutos sostenido durante el entretiempo
- Pico cuando Mbappé anotó para Francia y volvió a anotar 90 segundos después para empatar
- Fin del tiempo regular e ingreso al tiempo extra
- Chat durante el entretiempo de la primera parte del alargue
- Gol adicional de Messi
- Gol del empate de Mbappé
- Fin del alargue e ingreso a la tanda de penales
- Fallo de Francia y campeonato de Argentina
- Incluso ante un gran aumento en el volumen de envío de mensajes, el servicio de datos basado en Rust y ScyllaDB procesaron el tráfico
- El sistema final quedó organizado como una arquitectura capaz de manejar billones de mensajes
1 comentarios
Opiniones de Hacker News
Este artículo parece culpar mucho a la recolección de basura, pero si se ve el artículo anterior [0], parece más bien una limitación de cómo usaban Cassandra, de cómo Cassandra maneja las eliminaciones masivas, o de ambas cosas
Un usuario borró millones de mensajes mediante la API y dejó solo 1 mensaje en el canal; como Cassandra maneja las eliminaciones con tombstones, cuando un usuario abría ese canal tenía que escanear millones de tombstones aunque en realidad solo hubiera 1 mensaje. La explicación es que, durante ese proceso, se generaba basura más rápido de lo que la JVM podía recolectarla
También se habla de tuning del GC, pero si se ve [1], no parece que hubieran hecho mucho tuning, y al parecer estaban usando una versión antigua de Cassandra, y probablemente también de la JVM. También pesa bastante el hecho de que acababan de migrar desde CMS
0) https://discord.com/blog/how-discord-stores-billions-of-messages
Aunque estuvieran usando mal la solución original, creo que una solución difícil de usar mal es mejor
Si en 2022 estaban en Java 11, usaban un runtime de 4 años; si estaban en Java 8, uno de 8 años, así que es muy probable que estuvieran perdiendo bastante rendimiento
Needs (2023)
Esta capa de servicio parecía un Varnish Cache distribuido, grande y vistoso. Como no mencionan caché y eligieron la palabra “coalesce”, parece que en realidad no hacen mucho caching, pero me recordó al “grace mode” de Varnish y a su uso para evitar stampedes, es decir, el contexto en el que escuché por primera vez “fusión de solicitudes (request coalescing)” https://varnish-cache.org/docs/6.1/users-guide/vcl-grace.htm...
También me alegra ver que la hashing consistente aparece una y otra vez. Es una gran tecnología tipo cinta adhesiva cuya utilidad está probada en situaciones similares. Si sabes dónde debería estar algo, todos también saben dónde ir a buscarlo
proxy_cache_use_stale updating;Hay algunos datos adicionales de un cofundador de ScyllaDB: Discord no podía completar el repair con Cassandra, pero con Scylla eso no pasaba; Scylla tiene mucho en común con Cassandra, como los árboles LSM y la compaction, pero con su scheduler propio de CPU/IO puede dar a las consultas mayor prioridad que a la compaction
Dice que pueden posponer la compaction a un intervalo de medio milisegundo en el que haya suficiente ancho de banda ocioso, y que hay muchos artículos relacionados. Scylla tiene un modo más seguro llamado
tombstone_gc=repair, de hace 1,5 años, y la nueva arquitectura basada en Raft y tablets lanzada recientemente será el próximo gran cambio para los usuariosEste problema ni siquiera habría existido si se hubiera usado IRC, un protocolo de chat distribuido que existe desde hace más de 40 años.
También tiene la ventaja de contar con una especificación abierta y varias implementaciones. Tampoco es un jardín amurallado. Si creen que IRC está demasiado viejo para la era moderna, pueden mirar Matrix o XMPP. Es difícil entender cómo Discord llegó a dominar, y más bien se siente cercano a una tragedia.
IRC prácticamente exige un bouncer si se quiere seguir una conversación desde varios dispositivos. Tampoco cifra los mensajes; solo cifra, de forma opcional, la conexión entre cliente y servidor. Sin cifrado de extremo a extremo, no hay privacidad frente al servidor o sus operadores, y ese servidor es un punto único de falla que se convierte fácilmente en objetivo.
El protocolo Matrix todavía está cambiando y las implementaciones no alcanzan a seguir la especificación. Si no usas Element, te quedas atrás en funciones y seguridad. XMPP, igual que IRC, depende de extensiones opcionales para funciones básicas como el cifrado de extremo a extremo, y puede que los clientes no las soporten de forma completa y correcta.
Recomiendo leer el análisis de soatok: https://soatok.blog/2024/08/04/against-xmppomemo/ https://soatok.blog/2024/08/14/security-issues-in-matrixs-ol...
Lo de Snowden en 2013 fue hace 11 años. A estas alturas, el cifrado de extremo a extremo debería ser una función básica y tratarse como una commodity, y deberíamos exigirlo con la misma insistencia con la que se exigía HTTPS. Claro que Discord no implementa cifrado de extremo a extremo.
Hace poco, un grupo muy técnico al que pertenezco se mudó de Telegram a Matrix, y la experiencia de usuario no es buena. La app tiene bugs y se ve mal, y en la nueva app “Element” no se soportaba SSO, así que no pude usar mi cuenta. Son pequeñas molestias aceptables para alguien como yo, que se mete a investigar por su cuenta, pero no podría convencer a mis amigos de usarlo.
Descargas el exe, lo instalas, creas una cuenta y ya funciona. Cualquiera puede hacerlo. Hay mucho software útil y excelente, pero la mayoría no es fácil para el público general, y algunos —o la mayoría— ni siquiera tienen GUI. La gente prefiere vender su identidad e incluso pagar dinero antes que soportar demasiados pasos.
La conclusión que saqué de este artículo quizá sea un poco distinta de la intención del autor.
“¿El último? Nuestro amigo cassandra-messages. [...] Fue un clúster grande desde el comienzo. Con billones de mensajes y casi 200 nodos, cualquier migración inevitablemente iba a ser una tarea compleja”.
Considerando la escala de Discord, que el almacén de mensajes tenga menos de 200 nodos me parece sorprendentemente pequeño. Esperaba una arquitectura compleja, orientada a una escalabilidad mucho más rápida y con muchas más piezas móviles. La complejidad real seguramente sea mayor que la que se muestra en el artículo, pero al recordar mi experiencia siendo parcialmente responsable de más de 200 nodos físicos que hacían mucho menos trabajo, me pregunto cuán sobrediseñadas están las arquitecturas cloud modernas.
Habiendo almacenado miles de millones de registros en un conjunto mucho más pequeño de nodos Cassandra, Cassandra fue realmente doloroso para la guardia on-call y causó varias fallas graves.
[0] https://news.ycombinator.com/item?id=34950843
El artículo está muy bien escrito. Me alegra que parte de la solución no haya sido introducir algo completamente distinto, sino cambiar a ScyllaDB, que puede usarse como reemplazo de Cassandra.
Discord hace que sea prácticamente imposible borrar mensajes antiguos. Es una pesadilla de privacidad, y me pregunto por qué la UE no ha intervenido.
La pregunta central se reduce a si Matrix debería verse más como el correo electrónico o más como Facebook. Si fuera correo electrónico, la gente se escandalizaría ante la idea de que el remitente pudiera borrar sus mensajes de mi buzón; si fuera Facebook, la gente se escandalizaría de que una publicación siguiera viéndose en algún lugar incluso después de ejercer el derecho de eliminación.
Explican que resolver esto requiere criterio, y que lo abordaron considerando primero qué intenta lograr realmente el espíritu del GDPR.
https://matrix.org/blog/2018/05/08/gdpr-compliance-in-matrix...
El problema desde la perspectiva del GDPR es que Discord hace que sea difícil borrar eso. Si detecta la intención de eliminar los datos de la cuenta, guía al usuario hacia la “anonimización”; públicamente el nombre de usuario queda separado de los mensajes, pero todavía se puede rastrear hasta una persona específica. Si del lado del servidor también se maneja así, hacer cumplir la solicitud de un usuario para borrar su información de identificación personal implicaría revisar una cantidad enorme de mensajes o borrar en masa mensajes antiguos.
El Parlamento de la UE no es un parlamento real en el sentido de que solo la Comisión puede proponer nuevas leyes y el parlamento electo en general solo vota; y considero que quien controla la Comisión no es el público, sino el Departamento de Estado de EE. UU. Newsguard, las grandes tecnológicas estadounidenses que no son Musk, y Discord también están dentro de la misma red de intereses políticos y financieros establecidos, con mucho personal proveniente del Departamento de Estado.
Sin indignación pública, es muy probable que las instituciones a nivel de la UE sean capturadas, y creo que la opinión pública también la controlan esos intereses establecidos de estilo cyberpunk que entrenan LLM con datos ilegales de Discord y apuntan publicidad electoral. Al final deberíamos preocuparnos por la posibilidad de llegar a un estado orwelliano del que no se pueda escapar, dominado por intereses establecidos permanentes.
Dicho eso, creo que la mayor parte de esto solo tiene una probabilidad algo superior al 50% de ser cierto en sentido amplio, así que hay que tomarlo con bastante cautela.
Cassandra, en esencia, se parece más a una tabla hash distribuida y tolerante a fallos, mayormente solo de inserciones. Si necesitas exactamente eso con un alto rendimiento de escritura, es una buena opción.
No entiendo por qué la gente la usa como si fuera una base de datos. Te topas enseguida con sus límites, y el dolor de intentar usarla como base de datos empeora a medida que crece la escala.
Con eso, no le veo sentido como almacén de mensajes para un servidor de chat. Parece adecuada como destino de recolección de logs en sistemas distribuidos, donde muchos clientes vierten datos pero la mayoría de los logs ni siquiera necesitan auditarse, de modo que la cantidad de lecturas de un elemento específico es menor que 1. Los mensajes de Discord claramente no son eso.
En mi experiencia, nunca he visto un proyecto que empezara usando Cassandra y siguiera usándola después de alrededor de un año. Toma más o menos un año chocar con sus límites, y al final la reemplazan por una base de datos como Postgres.
No entiendo por qué no pueden simplemente hacer sharding. ¿No está cada “servidor” de Discord aislado de los demás? Si no pueden enviarse mensajes entre sí, me pregunto por qué no dividir billones de mensajes en miles de shards, de modo que cada shard maneje miles de millones.
Dice que “el equipo de ScyllaDB priorizó el trabajo de mejora e implementó consultas inversas de buen rendimiento, eliminando el último obstáculo de base de datos del plan de migración”.
Me pregunto cuánto habrán pagado para que hicieran eso incluso antes de usar ScyllaDB.