Arhitectura datelor și a sistemelor, de la zero Lecția 32 / 80

Caz real: călătoria Discord de la MongoDB la Cassandra la ScyllaDB

Cum a trecut storage-ul de mesaje al Discord de la MongoDB la Cassandra la ScyllaDB pe parcursul a zece ani, ce a costat fiecare migrare și care sunt lecțiile pentru toți ceilalți.

Asta e prima lecție a cursului care e, cap-coadă, un singur case study. Motivul ruperii formatului e că abstracțiile Modulului 4 (sharding, replicare, chei fierbinți, fan-out) sunt mai ușor de internalizat când le vezi jucate într-un sistem de producție real, pe parcursul a zece ani. Călătoria Discord cu storage-ul de mesaje e cel mai curat exemplu public pe care-l știu. Compania a scris despre fiecare tranziție în detaliu pe blogul lor de inginerie, iar cele trei ere ale sistemului se mapează direct pe lecțiile acestui modul.

Discord e o platformă de chat. Până în 2023 avea sute de milioane de useri lunari și un store de mesaje care ținea trilioane de rânduri. Sistemul a fost rescris peste trei baze de date diferite din 2015 încoace: întâi MongoDB, apoi Cassandra, apoi ScyllaDB. Fiecare migrare a fost determinată de atingerea unui plafon operațional pe baza de date anterioară. Modelul de date, după a doua migrare, a rămas același.

Workload-ul, expus precis

Înainte de ere: ce-i cere Discord bazei de date să facă?

Query-ul dominant e „dă-mi cele mai recente N mesaje din acest canal, opțional înainte de timestamp T, pentru paginare pe măsură ce user-ul derulează înapoi prin istoric”. Aproape orice citire din sistem e acest query. Unele citiri includ și filtre: mesaje de la un anumit user, mesaje cu o anumită reacție, mesaje care se potrivesc unei căutări. Scrierea dominantă e „inserează un mesaj nou în acest canal”. Editările și ștergerile sunt vastly mai puțin comune decât inserările.

Datele au două chei naturale. Mesajele sunt scoped la canale: #general-ul unui server, un DM privat între doi useri, un DM de grup. În cadrul unui canal, mesajele sunt ordonate după timp. Unitatea naturală de localitate e „toate mesajele dintr-un canal, în ordine cronologică”. Unitatea naturală de distribuție e canalul.

Traficul e puternic înclinat. Câteva canale sunt gigantice (canalele de anunțuri ale serverelor populare, DM-uri virale în timpul evenimentelor mondiale) și majoritatea canalelor sunt mici (un DM între doi useri care are cincizeci de mesaje în total). Orice strat de storage pentru acest workload trebuie să gestioneze ambele extreme fără să cadă pe niciuna.

Asta e problema. Acum, erele.

2015 până în 2016: MongoDB

Discord a fost lansat în 2015 cu MongoDB ca store de mesaje, din motivul pragmatic al unui startup: MongoDB era ușor de operat la scară mică, modelul schema-less de documente se potrivea cu forma laxă a unui mesaj de chat, iar echipa nu știa încă exact ce query-uri va avea nevoie. MongoDB i-a lăsat să livreze.

Spargerea a venit când working set-ul a încetat să încapă în RAM. Performanța la citire a MongoDB e excelentă când indexurile și datele fierbinți încap în memorie. Când dataset-ul crește peste plafonul de RAM, fiecare citire care ratează cache-ul lovește discul, iar coada de latență explodează. Până în 2017 Discord era la aproximativ o sută de milioane de mesaje, iar latența de citire pe istoricul de chat (query-ul dominant) devenea destul de proastă încât userii observau.

Postul de blog din 2017 merită citit în întregime pentru că e onest despre ce anume a mers prost. Pattern-ul IO de „mesaje recente în acest canal” era servit de un lookup pe index urmat de citiri aleatorii pe documentele mesajelor de pe disc, iar citirile aleatorii erau problema. Cache-uirea indexului era ușoară; cache-uirea tuturor mesajelor nu era. O a doua problemă: modelul de replica-set al MongoDB avea muchii operaționale ascuțite la scara la care operau. Decizia de migrare a fost determinată în mod egal de problema de latență și de oboseala operațională.

2016: trecerea la Cassandra

Ținta migrării a fost Cassandra, iar modelul de date a fost reproiectat de la zero în jurul query-ului dominant. Asta e partea poveștii din care e cel mai ușor să înveți, pentru că exercițiul de modelare a datelor e exact cel descris în lecția 20.

Query-ul e „mesaje în acest canal, ordonate după timp”. Modelul de date Cassandra care transformă asta într-o operațiune rapidă e:

  • Partition key: channel_id (tehnic un compus din channel_id și un time bucket, ca să limiteze dimensiunea partiției). Toate mesajele dintr-un canal trăiesc în aceeași partiție.
  • Clustering key: timestamp-ul mesajului (sau ID-ul mesajului, care încorporează un timestamp). Mesajele dintr-o partiție sunt stocate pe disc în ordine cronologică.

Cu acest layout, „mesajele recente în canalul X” e un singur lookup de partiție urmat de o citire contiguă a celor mai recente rânduri. Cassandra face acest tip de citire foarte rapid, pentru că partiția e pe un singur nod, rândurile sunt contigue pe disc, iar pattern-ul de acces se potrivește exact cu storage-ul LSM-tree. Fără IO aleatoriu, fără join-uri, fără fan-out: un shard, citire secvențială, gata.

Partition key-ul compus cu un time bucket adresează problema „unele canale sunt uriașe”. Fără bucketing, partiția unui singur canal ar crește fără limită, cauzând eventual problemele descrise în lecția 28 (partiție fierbinte, compactări lente, repair dureros). Cu bucketing (de exemplu, partition key = (channel_id, year_month) sau similar), fiecare partiție conține cel mult o lună din mesajele unui canal, ceea ce ține partițiile mărginite indiferent de dimensiunea canalului. Prețul e că „dă-mi ultimele N mesaje” ar putea traversa două bucket-uri lângă o graniță de lună, ceea ce aplicația gestionează interogând bucket-ul curent și revenind la bucket-ul anterior dacă e nevoie.

Migrarea în sine a durat luni. Blogul Discord descrie o fază de dual-write în care mesajele noi erau scrise simultan și în MongoDB și în Cassandra, în timp ce un job de backfill copia mesajele istorice din Mongo în Cassandra. Odată ce backfill-ul a prins din urmă, citirile au fost comutate pe Cassandra, iar după o fereastră de verificare MongoDB a fost scos din uz. Ăsta e playbook-ul standard pentru migrările de baze de date, și e cel corect. Orice mai agresiv (un singur cutover big-bang) ar fi fost un dezastru la scara la care operau.

Pentru următorii șase ani, Cassandra a fost store-ul de mesaje. Clusterul a crescut de la doisprezece noduri inițial la o sută șaptezeci și șapte de noduri până în 2022, ținând trilioane de mesaje. Modelul de date nu s-a schimbat. Adăugarea de capacitate a însemnat adăugarea de noduri, ceea ce Cassandra a gestionat, cu durere operațională despre care Discord a scris în detaliu.

2022 până în 2023: trecerea la ScyllaDB

Postul de blog din 2022 despre migrarea ScyllaDB e mai recent și mai interesant dintre cele două. Titlul postului e costul: Discord a migrat de la o sută șaptezeci și șapte de noduri Cassandra la șaptezeci și două de noduri ScyllaDB, cu latență mai bună și cost mai mic.

Motivele migrării nu erau probleme de model de date. Modelul de date funcționa. Motivele erau operaționale, și se aliniază exact cu lucrurile spuse în lecția 20 despre Cassandra.

Overhead-ul JVM și variabilitatea latenței. Cassandra e o aplicație JVM. Pauzele de garbage collection cauzează vârfuri periodice de latență care apar în cozile p99 și p99.9. La scara Discord, chiar și o fracțiune mică de citiri lente înseamnă o mulțime de citiri lente în termeni absoluți. Echipa cheltuise efort semnificativ pe tunarea GC, iar coada de latență tot era problematică.

Durerea de compactare. Procesul de compactare al Cassandra unește fișierele sortate pe disc (SSTables) în care se acumulează scrierile. Compactarea e necesară pentru a ține citirile rapide și a recupera spațiu de la rândurile șterse, dar consumă IO și CPU pe aceleași noduri care servesc citirile. La scara Discord, compactarea era o sursă continuă de muncă operațională: tunarea strategiilor de compactare, programarea compactărilor, gestionarea backlog-urilor de compactare, monitorizarea vârfurilor de latență induse de compactare.

Oboseala operațională. Repair-ul (procesul care reconciliază replicile divergente) era dureros la o sută șaptezeci și șapte de noduri. Adăugarea de noduri era dureroasă. Eliminarea de noduri era dureroasă. Echipa devenise foarte bună la rularea Cassandra, iar rularea ei era încă o fracțiune substanțială din timpul lor.

ScyllaDB e rescrierea în C++ a Cassandra. Același wire protocol, același query language, același model de date, implementare complet diferită. Arhitectura thread-per-core, networking-ul în userspace, și absența unui JVM înseamnă că ScyllaDB livrează cu un ordin de mărime mai mult throughput per nod, cu cozi de latență mult mai mici și mai predictibile. Pentru o echipă care rulează un workload Cassandra existent, ScyllaDB e cel mai apropiat lucru de un prânz gratuit care există în acest spațiu: același cod client, același layout de date, aceeași formă operațională, numere mult mai bune.

Migrarea a fost, din nou, un exercițiu inginer de mai multe luni. Discord a scris un serviciu custom de migrare a datelor pentru a copia trilioanele de mesaje de la Cassandra la ScyllaDB în timp ce traficul de producție continua. Serviciul folosea dual-writes pentru mesajele noi, un backfill streaming pentru mesajele istorice și verificare per canal înainte de cutover. Cutover-ul a fost incremental: traficul a fost mutat canal-cu-canal, monitorizat și dat înapoi dacă ceva se comporta prost. După câteva luni, întregul workload era pe ScyllaDB.

Numerele din postul public: o sută șaptezeci și șapte de noduri Cassandra au devenit șaptezeci și două de noduri ScyllaDB, o reducere de aproximativ 60%. Latența la coadă pentru citire a scăzut substanțial. Costul de infrastructură pe acest workload a fost semnificativ mai mic (Discord a încadrat public migrarea ca o mișcare majoră de economisire de costuri). Durerea operațională asociată cu compactarea, GC și managementul nodurilor a fost substanțial redusă, deși nu eliminată: ScyllaDB e tot o bază de date wide-column, și aceleași primitive operaționale se aplică.

flowchart LR
    subgraph E1[2015-2016: MongoDB era]
      M[(MongoDB)]
      M -->|RAM ceiling, IO pattern| L1[Latency degradation]
    end
    subgraph E2[2016-2022: Cassandra era]
      C[(Cassandra, 12 to 177 nodes)]
      C -->|JVM, compaction, ops| L2[Operational ceiling]
    end
    subgraph E3[2022-2026: ScyllaDB era]
      S[(ScyllaDB, 72 nodes)]
      S --> L3[Lower cost, better tail]
    end
    L1 --> E2
    L2 --> E3

Modelul de date care a supraviețuit din era 2 în era 3 e același: canal ca partiție (cu time-bucketing pentru controlul dimensiunii), timestamp-ul mesajului ca clustering key. Migrarea a schimbat implementarea bazei de date dedesubt, nu forma datelor. Ăsta e cel mai important singur fapt despre migrare și cea mai generalizabilă lecție.

Ce ne învață călătoria

Cinci lecții, aproximativ în ordinea în care Discord le-a întâlnit.

Alege modelul de date în jurul pattern-ului tău dominant de query. Motivul pentru care Cassandra a funcționat e că layout-ul partiție-per-canal, clustered după timestamp, face din query-ul dominant o citire secvențială pe o singură partiție. Același model e ce a făcut posibilă migrarea ScyllaDB fără o re-arhitectură: forma datelor era deja potrivită pentru un wide-column store, și ScyllaDB e un wide-column store. Dacă schema originală Cassandra ar fi fost greșită (să zicem, partiție după user) trecerea la ScyllaDB ar fi necesitat o schimbare de model de date, ceea ce e mult mai greu decât un schimb de bază de date. Lecția 20 a spus că wide-column databases își câștigă pâinea când schema se potrivește cu query-ul. Discord e cazul de manual.

Wide-column databases își câștigă pâinea la scară. Sunt operațional grele. Compactare, repair, management de noduri, planificare de capacitate: nimic nu e distractiv. Dar la scara Discord alternativele sunt și ele grele și în plus nu scalează. MongoDB a atins plafonul la o sută de milioane de mesaje; Cassandra le-a cumpărat șase ani și de câteva mii de ori datele.

Migrarea e inginerie, nu magie. Ambele migrări Discord au durat luni și au folosit același playbook: proiectează întâi noul model de date, dual-write, backfill al datelor istorice în timp ce traficul live continuă, verificare, cutover incremental, monitorizare, scoatere din uz. Lecția 29 a acoperit pattern-urile în abstract; migrările Discord sunt exact acel pattern la scară industrială. Nu există scurtătură.

Rescrierea corectă a unei tehnologii vechi poate fi transformatoare. ScyllaDB e case study-ul. Același protocol, același query language, același model de date, implementare complet diferită, cu un ordin de mărime mai bună în părțile care contează. Migrarea a fost aproape gratuită în schimbări de aplicație și dramatică în cost și latență. Asta e posibil pentru că wire protocol-ul Cassandra e documentat și stabil. Sistemele cu protocol închis nu primesc niciodată rescrierea echivalentă.

Oboseala operațională e un semnal real. Clusterul Cassandra funcționa tehnic când Discord a migrat de la el. Modelul de date era corect, clusterul scala, aplicația servea trafic. Migrarea a fost determinată de greutatea cumulativă a durerii operaționale și de realizarea că o rescriere a aceleiași baze de date într-un alt limbaj putea ridica mult din ea. Când o echipă petrece o fracțiune substanțială a timpului rulând baza de date în loc să construiască produsul, baza de date e prea scumpă chiar dacă nu cade. Costul apare în organigrama de inginerie, nu în dashboard-ul de latență.

Ce înseamnă asta pentru sistemele care nu sunt Discord

Aproape sigur n-ai un workload de chat la scara Discord. Lecțiile relevante deci nu sunt „folosește Cassandra” sau „migrează la ScyllaDB”. Sunt meta-lecțiile.

Dacă pattern-ul tău de acces e „elemente recente într-un stream, după timp, scoped la o entitate”, modelul de date wide-column (entitate ca partiție, timestamp ca clustering key) e forma corectă, indiferent dacă store-ul dedesubt e Cassandra, ScyllaDB, BigTable, DynamoDB, sau chiar Postgres cu tabele sharded la scară mai mică.

Dacă alegi o bază de date pentru un produs nou, optimizează pentru query-ul dominant și acceptă că query-urile secundare vor fi urâte. Designul Discord acceptă că „găsește un singur mesaj după ID” e incomod în schimbul de a face operația dominantă extrem de rapidă. Compromisul e corect pentru că operația secundară e rară.

Dacă rulezi o bază de date care funcționează dar doare să o operezi, migrarea e o opțiune reală. E și costisitoare: luni de inginerie. Migrează când costul operațional cumulativ e cu adevărat mai mare decât costul migrării, și când ținta e suficient de matură ca să pariezi pe ea. A fi early adopter e un profil de risc diferit și de obicei mai prost.

Dacă rulezi o singură instanță Postgres cu zece milioane de rânduri: rămâi acolo. Case study-ul nu spune „treci la Cassandra”. Spune „proiectează layout-ul de date pentru query-ul tău dominant, și alege o bază de date care se potrivește cu layout-ul”. Pentru zece milioane de rânduri pe un workload centrat pe user, acea bază de date e aproape sigur Postgres.

Modulul 4 se închide aici

Modulul 4 a deschis cu replicarea, a parcurs partiționarea, strategiile de sharding, cheile fierbinți și rebalansarea, query-urile cross-shard, și se termină aici cu un case study care leagă abstracțiile de un sistem de producție real, pe parcursul unui deceniu. Pattern-urile sunt aceleași indiferent de baza de date pe care ai ales-o în Modulul 3, și sunt fundația pentru tot ce construiește Modulul 5.

Modulul 5 se întoarce la procesare: cum curg datele printr-un sistem după ce sunt stocate. Mai întâi batch processing (descendenții MapReduce, pipeline-urile moderne de data warehouse), apoi stream processing (Kafka, Flink, partea real-time), apoi convergența celor două. Stratul de storage e podeaua. Procesarea e clădirea.

Citări și lecturi suplimentare

  • Discord Engineering, „How Discord Stores Trillions of Messages”, 2023, https://discord.com/blog/how-discord-stores-trillions-of-messages (consultat 2026-05-01). Relatarea detaliată a migrării ScyllaDB, inclusiv numărul de noduri, serviciul de migrare a datelor, procesul de cutover, și rezultatele de cost și latență.
  • Discord Engineering, „How Discord Stores Billions of Messages”, 2017, https://discord.com/blog/how-discord-stores-billions-of-messages (consultat 2026-05-01). Postul anterior despre migrarea MongoDB-to-Cassandra, inclusiv modurile de eșec care au determinat mutarea și designul schemei Cassandra.
  • Apache Cassandra documentation, https://cassandra.apache.org/doc/latest/ (consultat 2026-05-01). Referință pentru modelul de date, strategiile de compactare, și subiectele operaționale pe care le atinge case study-ul.
  • ScyllaDB documentation, https://docs.scylladb.com/ (consultat 2026-05-01). Referință pentru arhitectură și API-ul compatibil Cassandra care a făcut posibilă migrarea.
  • „Designing Data-Intensive Applications” (Martin Kleppmann, O’Reilly, 2017), capitolele 5 și 6. Referința standard pentru replicare și partiționare, cu conceptele pe care le ilustrează case study-ul.
Caută