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

Strategii de sharding și capcanele lor

Sharding la nivel de aplicație, sharding nativ în baza de date, Citus și Vitess. Realitățile practice ale rulării unei baze de date SQL sharded.

Lecția precedentă ne-a lăsat cu problemele operaționale ale partiționării: hot keys, rebalancing, coordonatorul care trebuie să țină evidența cui ce-i aparține. Toate erau suficient de abstracte încât să se aplice oricărui data store partiționat. Această lecție aterizează pe o întrebare mult mai specifică, la care majoritatea echipelor trebuie să răspundă într-un final: cum sharded efectiv o bază de date SQL în producție, când Postgres-ul sau MySQL-ul de pe un singur server cu care ai început nu mai e suficient?

Vocabularul, întâi. „Sharding” e horizontal partitioning peste mai multe mașini fizice. Conceptul e același cu partitioning-ul, dar conotația e diferită: când cineva spune partitioning, înseamnă adesea un singur sistem distribuit care are mai multe partiții interne (Cassandra, Mongo, ScyllaDB), iar când zice sharding, înseamnă de obicei o flotă de instanțe independente de baze de date coordonate deasupra layer-ului de bază de date. Linia e neclară, iar cuvintele sunt folosite interschimbabil în practică. Ce contează e arhitectura concretă, nu eticheta.

Această lecție trece prin cele patru opțiuni reale din 2026. Sharding la nivel de aplicație, unde codul tău rutează query-urile la baza de date corectă. Sharding nativ în Postgres prin extensia Citus. Sharding MySQL prin Vitess (și PlanetScale, varianta gestionată populară). Și sharding-ul built-in al sistemelor native distribuite precum MongoDB și Cassandra, pe care l-am atins deja și pe care îl revedem aici pe scurt pentru completitudine.

Sharding la nivel de aplicație

Cel mai simplu model mental. Ai N instanțe Postgres sau MySQL independente, fiecare cu aceeași schemă, fiecare ținând o submulțime a datelor tale, iar codul aplicației știe cum să ruteze un query la cea corectă. Cheia de routing e de obicei tenant_id sau user_id, funcția de routing e de obicei un hash modulo numărul de shard-uri sau un range lookup într-un tabel de configurare, și fiecare query pe care aplicația îl emite include suficiente informații încât aplicația să poată alege shard-ul corect.

Pro-urile sunt reale. Ai control total: fiecare shard e o bază de date vanilla pe care o înțelegi, care are uneltele operaționale pe care le folosești deja, care eșuează în moduri pe care le-ai depanat deja. Nu ai nevoie de un produs de bază de date elegant. Backup-ul, monitorizarea și upgrade-ul per shard sunt doar backup, monitorizare și upgrade per bază de date, repetate de N ori. Fiecare shard scalează independent. Poți rula versiuni diferite pe shard-uri diferite dacă trebuie. Poți opri un shard pentru mentenanță fără să implici celelalte.

Contra-urile sunt și ele reale. Query-urile cross-shard sunt problema ta. Un query precum „găsește-mi toți utilizatorii creați în ultima săptămână din întreaga flotă” trebuie emis către fiecare shard, aplicația trebuie să asambleze rezultatele, iar aplicația trebuie să se ocupe de cazul în care un shard e lent sau jos. Schimbările de schemă trebuie aplicate fiecărui shard, iar tooling-ul de migrare trebuie să gestioneze rollout-ul peste toată flota. Join-urile cross-shard nu sunt join-uri, sunt cod de merge la nivel de aplicație. Tranzacțiile distribuite peste shard-uri sunt în afara scope-ului dacă nu le construiești singur, ceea ce n-ar trebui să faci.

Sumarul onest: sharding la nivel de aplicație e răspunsul corect pentru produse foarte mari cu echipe dedicate de platformă care sunt dispuse să investească în tooling operațional și care nu au nevoie de tranzacții cross-shard pe path-ul fierbinte. Multe dintre cele mai mari deployment-uri SQL din lume (Stripe, Shopify, Notion, Figma) sunt flote Postgres sau MySQL sharded la nivel de aplicație, cu mii de shard-uri și o echipă substanțială care le rulează. Pattern-ul funcționează la scară extremă; e și scump.

Citus, sharding nativ Postgres

Citus e o extensie Postgres care adaugă tabele distribuite, tranzacții distribuite și un planner de query distribuit peste Postgres standard. A început ca o companie independentă, a fost achiziționată de Microsoft și e acum atât o extensie open-source, cât și motorul din spatele „Cosmos DB for PostgreSQL” de la Azure. Arhitectura e un nod coordonator și o flotă de noduri worker; coordonatorul parsează SQL-ul de intrare, planifică ce worker-i ar trebui să ruleze ce fragmente și asamblează rezultatele.

În Citus, declari ce tabele sunt distribuite și pe ce coloană. Un pattern tipic e să distribui o aplicație multi-tenant după tenant_id: fiecare tabel distribuit are tenant_id ca parte din primary key, iar planner-ul folosește acea coloană pentru a ruta query-urile. Un query care filtrează după tenant_id e împins în întregime către worker-ul care deține acel tenant; un query care nu filtrează după coloana de distribuție face fan-out către fiecare worker și e redus la coordonator. Există și reference tables (tabele mici de lookup replicate la fiecare worker) și local tables (ținute doar la coordonator) pentru datele care nu se potrivesc pattern-ului distribuit.

Query planner-ul e piesa cea mai inteligentă. Face join-uri cross-shard fie co-locând tabelele join-uite pe aceeași distribution key (deci join-ul e local pe fiecare worker), fie broadcast-uind o parte la toți worker-ii. Suportă agregate distribuite calculând rezultate parțiale per shard și combinându-le la coordonator. Suportă insert-uri, update-uri și delete-uri care ating un singur shard cu semantici tranzacționale complete, și scrieri multi-shard prin two-phase commit (lecția 15) când optezi pentru asta.

Cazul pentru Citus: vrei SQL sharded, dar vrei să păstrezi semanticile Postgres. Schema e o schemă Postgres, query-urile sunt query-uri Postgres, extensiile pe care te bazezi încă funcționează. Aplicația ta nu trebuie să știe despre shard-uri; coordonatorul știe. Cazul împotriva: operezi acum un sistem mai complex decât Postgres vanilla, path-ul de query cross-shard are propriile moduri de eșec, iar coordonatorul e un hop pe fiecare query. Pentru workload-uri care se potrivesc pattern-ului multi-tenant, Citus e excelent. Pentru workload-uri unde majoritatea query-urilor traversează natural distribution key-ul, e mai puțin potrivit.

Vitess, sharding nativ MySQL

Vitess e echivalentul MySQL. A fost construit la YouTube pentru a gestiona flota lor MySQL, a fost open-source-uit și e acum un proiect CNCF graduated. Arhitectura e o flotă de primary-uri și replici MySQL (numite „tablet-uri”), un layer de routing (vtgate) și un serviciu de topologie (etcd sau ZooKeeper) care ține metadatele despre ce chei trăiesc pe ce shard.

Vitess prezintă un singur endpoint MySQL aplicației: clienții se conectează la vtgate, care parsează SQL-ul și-l rutează la tablet-urile potrivite. Ca și Citus, face join-uri cross-shard, agregate distribuite și tranzacții per shard, cu trade-off-urile la care te-ai aștepta (query-urile cross-shard sunt mai lente și au mai multe moduri de eșec decât query-urile single-shard). Mai gestionează și online resharding: împarte un shard în două, cu rebalancer-ul copiind datele și layer-ul de routing făcând cutover odată ce destinația a recuperat.

PlanetScale e varianta gestionată populară. Rulează Vitess ca serviciu, expune o experiență de bază de date serverless și adaugă un workflow de branching în stil Git pentru schimbările de schemă. Funcționalitatea de branching e genuin interesantă: schimbările de schemă se întâmplă pe un branch, le faci merge în main branch, iar mecanica Vitess de dedesubt gestionează rollout-ul sigur. Pentru echipele care vor MySQL sharded fără să opereze Vitess singure, PlanetScale e punctul de pornire evident.

Cazul pentru Vitess: la fel ca Citus, în formă MySQL. Cazul împotriva: la fel ca Citus, în formă MySQL. Cele două produse rezolvă aceeași problemă în două ecosisteme SQL diferite, iar alegerea se reduce de obicei la ce bază de date rulează deja echipa ta.

flowchart TB
    App[Application] --> Coord[Coordinator vtgate or Citus]
    Coord --> W1[(Worker 1 - shards A,B)]
    Coord --> W2[(Worker 2 - shards C,D)]
    Coord --> W3[(Worker 3 - shards E,F)]
    Topo[(Topology - etcd)] -->|metadata| Coord

Forma e identică pentru ambele produse. Aplicația vorbește cu un coordonator, coordonatorul face fan-out către worker-i, serviciul de topologie ține adevărul despre ce shard trăiește unde. Etichetele se schimbă, arhitectura nu.

Sharding nativ în baza de date

A patra opțiune e să folosești o bază de date care a fost construită sharded de la început. Clusterele sharded MongoDB, Cassandra, ScyllaDB, CockroachDB, YugabyteDB, TiDB. Fiecare dintre aceste sisteme expune o singură bază de date logică, care e intern un cluster partiționat, cu logica de sharding construită în motor în loc să fie atașată ca extensie sau implementată în aplicație.

Pro-urile: sharding-ul e transparent pentru aplicație; rebalancing-ul e automat; povestea operațională e un singur produs în loc de două. Contra-urile: folosești o bază de date diferită de cea SQL pe care o știe deja echipa ta, iar funcționalitățile SQL pe care te bazezi pot fi suportate parțial sau cu rezerve. CockroachDB și Yugabyte vizează compatibilitate completă cu protocolul PostgreSQL și suport larg pentru funcționalitățile SQL; TiDB vizează compatibilitate MySQL; MongoDB și Cassandra nu oferă deloc SQL în sens strict.

Pentru echipele care aleg o bază de date de la zero în 2026, produsele sharded-din-prima-zi sunt o opțiune serioasă, în special CockroachDB, Yugabyte, TiDB și ofertele cloud în stil Spanner. Pentru echipele care au deja o flotă Postgres sau MySQL și au nevoie să o crească, conversația e de obicei între sharding la nivel de aplicație și Citus sau Vitess.

Coșmarul migrării

Majoritatea echipelor nu ajung la „ar trebui să fac sharding?” până când nu au o bază de date unsharded de mai mulți ani pe care trebuie s-o convertească într-una sharded fără să oprească aplicația. Asta e una dintre cele mai grele probleme de inginerie cu care se confruntă majoritatea echipelor și merită o tratare explicită.

Versiunea necinstită e „vom face pre-sharding din ziua unu”. Asta e overkill pentru aproape orice echipă. Nu-ți cunoști încă pattern-urile de acces. Vei alege distribution key-ul greșit și va trebui să-l refaci. Vei plăti costul operațional și de complexitate al unei baze de date sharded înainte ca traficul să-l justifice. Pre-sharding-ul e decizia corectă doar pentru echipele care știu deja, din experiență anterioară, că vor fi la scara care îl cere în decurs de un an sau doi. Pentru toți ceilalți, e prematur.

Versiunea onestă e migrarea live. Pattern-ul e bine documentat, iar postul de blog „online migrations” al lui Stripe e referința canonică (https://stripe.com/blog/online-migrations, consultat 2026-05-01). Forma migrării e:

  1. Ridici noul sistem sharded alături de baza de date monolitică veche.
  2. Începi dual-writing: fiecare scriere care merge la baza de date veche merge și la noul sistem sharded, idempotent.
  3. Backfill: copiezi datele istorice de la baza de date veche la cea nouă în batch-uri, cu verificări că dual-write-urile n-au fost depășite de backfill.
  4. Validare: rulezi citiri pe ambele sisteme pentru o perioadă, compari rezultatele, repari inconsistențele până când sistemele sunt de acord pe fiecare cheie.
  5. Comuți citirile: rutezi traficul de citire către noul sistem. Sistemul vechi încă primește scrieri prin path-ul de dual-write.
  6. Oprești dual-write-ul către sistemul vechi: scrierile merg acum doar către sistemul nou.
  7. Decomisionare: arhivezi și ștergi sistemul vechi.

Asta durează luni, nu săptămâni. Există sub-probleme la fiecare pas (cum faci fiecare scriere idempotentă dacă aplicația n-a fost proiectată pentru asta; cum gestionezi citirile în zbor în timpul cutover-ului; cum faci backfill la un miliard de rânduri fără să striveșți baza de date sursă; cum validezi că două sisteme sunt de acord fără să iei un global lock). Fiecare echipă care a făcut asta are povești de război.

Arborele decizional pentru „ar trebui să fac sharding și, dacă da, cum?” arată cam așa. Dacă datele tale sunt confortabil sub un terabyte și rata de scriere e confortabil sub câteva mii pe secundă, nu face sharding. Postgres pe o instanță mare singulară îl va gestiona ani de zile. Dacă împingi acele limite, întreabă-te dacă poți scala vertical mai întâi: instanță mai mare, mai multă memorie, discuri mai rapide, replici de citire pentru workload-uri dominate de citiri. Dacă scalarea verticală nu e suficientă, ia în considerare dacă workload-ul tău se potrivește unui pattern multi-tenant; dacă da, Citus sau Vitess e opțiunea SQL sharded cu cea mai mică fricțiune. Dacă workload-ul tău are nevoie genuin de SQL distribuit cu strong consistency peste shard-uri, uită-te la CockroachDB, Yugabyte sau Spanner. Dacă ai nevoie de control mai presus de orice și ai echipa pentru asta, sharding-ul la nivel de aplicație rămâne plafonul.

Ce ar trebui să renunți să crezi e ideea că sharding-ul e gratuit sau că migrarea va fi rapidă. Ambele sunt mituri neamabile.

Ce pregătește această lecție

Am parcurs realitățile practice ale partiționării și sharding-ului: cum alegi o distribution key, cum gestionezi hot keys, cum faci rebalancing live, cum sharded efectiv o bază de date SQL. Forma întrebării următoare e una care a planat tot timpul: când sistemul se împarte, când rețeaua eșuează, când două jumătăți ale unui cluster nu pot vorbi una cu alta, ce se întâmplă cu garanțiile de leadership și de consistency pe care le-ai stabilit în Modulul 2?

Modul de eșec principal e split brain: o partiție de rețea care face ca două jumătăți ale unui cluster să creadă amândouă că sunt liderul, ambele jumătăți acceptând scrieri conflictuale. Lecția 30 e despre acel mod de eșec, de ce quorum e singura apărare fiabilă și rețetele de deployment care îl țin departe.

Citări și lecturi suplimentare

  • Stripe Engineering, “Online migrations at scale”, https://stripe.com/blog/online-migrations (consultat 2026-05-01). Referința canonică pentru migrările live de baze de date.
  • Documentația Citus, https://docs.citusdata.com/ (consultat 2026-05-01). Sistemul de sharding ca extensie Postgres.
  • Documentația Vitess, https://vitess.io/docs/ (consultat 2026-05-01). Sistemul de sharding MySQL.
  • Documentația PlanetScale, https://planetscale.com/docs (consultat 2026-05-01). Serviciul Vitess gestionat.
  • Documentația CockroachDB, https://www.cockroachlabs.com/docs/ (consultat 2026-05-01). Una dintre opțiunile SQL sharded-din-prima-zi.
  • Martin Kleppmann, “Designing Data-Intensive Applications” (O’Reilly, 2017), Capitolul 6. Background pe strategiile de sharding.
Caută