Cele două lecții anterioare au fost despre replication: păstrarea a mai multor copii ale acelorași date pe mai multe mașini, astfel încât un singur eșec să nu piardă datele și capacitatea unui singur server să nu fie plafonul de citire. Replication e pentru redundanță. Nu îți permite, prin sine, să stochezi mai multe date decât încap pe o mașină și nu îți permite să gestionezi un workload de scriere mai mare decât poate susține leader-ul. Pentru aceste probleme, tehnica este partitioning: împărțirea datelor astfel încât fiecare mașină să țină un subset diferit.
Cele două sunt ortogonale, iar majoritatea bazelor de date mari le fac pe amândouă în același timp. Fiecare partition e replicat pentru durabilitate și scalare a citirilor; fiecare replica set e una dintre multele partitions care acoperă părți diferite din date. Cassandra, DynamoDB, cluster-ele sharded MongoDB, Elasticsearch și majoritatea celorlalte store-uri distribuite combină cele două pattern-uri în acest fel. Lecția de față acoperă partea de partitioning din acel tablou, în sine. Combin-o cu lecțiile anterioare și ai imaginea completă.
Decizia pe care o schemă de partitioning trebuie s-o ia este, simplu: dată o bucată de date, care nod o ține? Datele în sine nu se schimbă; doar locația lor. Trei familii de strategie răspund la acea întrebare, iar fiecare face același trade-off într-un mod ușor diferit: optimizează pentru o formă de query și plătește pentru asta pe alta.
Partitioning bazat pe range
Datele sunt sortate după o cheie. Spațiul cheilor e împărțit în range-uri contigue, iar fiecare range merge la un partition. Partition-ul 1 ar putea acoperi cheile A până la G; partition-ul 2 acoperă H până la N; partition-ul 3 acoperă O până la Z. Un rând cu cheia „Marco” trăiește pe partition-ul 2.
Strategia se potrivește natural pe o structură sortată, deci apare în bazele de date care deja organizează datele după chei sortate. HBase, BigTable și store-urile originale derivate din LevelDB funcționează așa. Cluster-ele sharded MongoDB pot folosi partitioning bazat pe range când shard key-ul e configurat pentru asta. Multe baze de date time-series partiționează pe range de timp, ceea ce e doar range partitioning unde cheia se întâmplă să fie un timestamp.
Avantajul sunt range queries. Dacă aplicația ta întreabă „dă-mi toate cheile între J și L”, baza de date știe exact care partition ține acel range, trimite query-ul acolo și întoarce rezultatul fără să atingă vreun alt partition. Query-urile cu margini de timp pe un store partiționat după timp sunt exemplul canonic: „dă-mi toate evenimentele de ieri” lovește unul sau două partitions, niciodată mai multe.
Dezavantajul sunt hot partitions pentru date asimetrice. Dacă cheia ta e ceva precum numele de familie al unui utilizator, alfabetul nu e distribuit uniform: vastly mai mulți oameni au nume de familie care încep cu litere de la mijloc decât cu litere aproape de capăt. Partition-ul de la mijloc primește trafic disproporționat. Dacă cheia ta e un timestamp și scrierile sunt în timp real, fiecare scriere nouă merge la partition-ul cel mai recent, iar acel singur partition ia tot load-ul de scriere în timp ce partition-urile mai vechi stau degeaba. Range partitioning e excelent pentru workload-urile pe care le potrivește și prost pentru cele pe care nu le potrivește.
Granițele partition-urilor sunt de obicei adaptive: baza de date împarte un partition când crește prea mare și fuzionează partition-urile adiacente când se micșorează. Asta ține partition-urile aproximativ balansate ca dimensiune de storage, dar nu face nimic pentru asimetria de acces. Un partition mic care e fierbinte e tot fierbinte.
Partitioning bazat pe hash
În loc să partiționezi după cheie direct, partiționează după hash-ul cheii. Cheia „Marco” e hash-uită la un întreg; spațiul de hash e împărțit în range-uri; fiecare range merge la un partition. Partition-ul pentru „Marco” e determinat de unde cade hash-ul ei, nu de unde cade poziția ei alfabetică.
Acesta e default-ul în Cassandra, în DynamoDB, în majoritatea key-value stores distribuite și în cluster-ele sharded MongoDB când shard key-ul e configurat pentru hashing. Postgres are partitioning prin hash nativ de la versiunea 11.
Avantajul e distribuția uniformă. Presupunând că funcția de hash e bună, cheile sunt răspândite uniform peste spațiul de hash indiferent cât de asimetrice sunt cheile originale. Problema „toată lumea are nume de familie care încep cu M” dispare: hash-urile cheilor similare sunt împrăștiate peste spațiul de partition, deci niciun singur partition nu devine fierbinte doar din clustering-ul cheilor. Load-ul de scriere și storage-ul sunt balansate prin design.
Dezavantajul e pierderea de range queries. Funcția de hash amestecă cheile, deci cheile consecutive în spațiul original aterizează pe partition-uri diferite. „Dă-mi toate cheile între J și L” devine un fan-out: baza de date n-are cum să știe care partition ține care cheie fără să calculeze hash-ul, deci trebuie să interogheze fiecare partition și să fuzioneze rezultatele. Pentru workload-uri care fac range queries pe cheia de partitioning, hash partitioning e unealta greșită.
În practică, multe sisteme suportă range queries pe un index secundar chiar și când partitioning-ul e bazat pe hash, dar acele query-uri sunt tot fan-outs peste toate partition-urile. Costul de fan-out e trade-off-ul central.
Un pattern subtil înrudit este partitioning compus: hash pe prima parte a cheii, sortat pe a doua. Cassandra îi spune „partition key plus clustering key”. Hash-ul pe partition key răspândește datele uniform, iar în interiorul fiecărui partition, rândurile sunt sortate după clustering key, deci range queries într-un singur partition sunt eficiente. Query-ul „toate mesajele de la utilizatorul X între timpul T1 și T2” lovește un partition (pentru că ID-ul utilizatorului e partition key) și întoarce un range sortat în interiorul lui. Acesta e pattern-ul de bază pentru time-series-on-Cassandra și pentru store-urile de mesaje de chat.
Hashing consistent și vederea după cheie
Al treilea pattern e strâns legat de partitioning bazat pe hash, dar formulat diferit. Imaginează-ți un spațiu de hash circular, inelul de hash. Fiecare nod din cluster e atribuit la una sau mai multe poziții pe inel (deseori numite virtual nodes sau vnodes). O cheie, când e hash-uită, aterizează la un punct pe inel; nodul care o deține e următorul în sensul acelor de ceasornic față de acel punct.
Aceasta e consistent hashing, iar proprietatea care o face utilă e comportamentul ei când nodurile se alătură sau pleacă. Adăugarea unui nod îi atribuie un set nou de poziții pe inel; doar cheile care cad între predecesorul noului nod și noul nod trebuie să se mute. Eliminarea unui nod predă cheile lui către următorul nod în sensul acelor de ceasornic. În ambele cazuri, doar o fracțiune a cheilor (proporțională cu numărul de noduri) e rebalansată, în loc de re-hashing-ul catastrofal pe care l-ar cere partitioning-ul naiv bazat pe modulo.
flowchart TB
subgraph H[Hash partitioning]
K1[key X] --> Hash[hash function]
Hash --> P1[Partition 1]
Hash --> P2[Partition 2]
Hash --> P3[Partition 3]
P1 --> Note1[Even distribution, fan-out range queries]
end
subgraph R[Range partitioning]
K2[key X] --> Lookup[range table]
Lookup --> P4[Partition A-G]
Lookup --> P5[Partition H-N]
Lookup --> P6[Partition O-Z]
P4 --> Note2[Range queries fast, hot partitions on skew]
end
Consistent hashing e mecanismul de partitioning în Cassandra, DynamoDB, Riak, modurile de cluster distribuit memcached/Redis și majoritatea sistemelor peer-to-peer. Lucrarea originală din 1997 a lui Karger et al. era despre cache-uri distribuite, dar tehnica s-a generalizat la aproape fiecare key-value store distribuit modern.
Alegerea cheii de partition
Strategia contează mai puțin decât cheia de partition. O cheie proastă pe o strategie bună creează mai multă durere decât o cheie bună pe o strategie mediocră. Cele două moduri de eșec care merită numite.
Hot partitions din asimetria cheii. Un partition care ține o singură valoare populară, sau un set mic de valori populare, ia o cotă disproporționată de trafic. Exemplul clasic e partitioning după user_id într-un sistem unde un utilizator (o celebritate, un cont de brand, un bot) generează mai mult trafic decât mii de utilizatori normali la un loc. Partition-ul care ține datele acelui utilizator e suprasolicitat; celelalte partition-uri sunt subutilizate. Aceeași formă apare la postări virale, produse fierbinți pe un site de e-commerce și sesiuni active pe o aplicație de chat.
Soluțiile sunt specifice workload-ului.
Prima soluție e o cheie de partition mai bună. Dacă user_id singur creează hot partitions, folosește (user_id, time_bucket) sau (user_id, message_id) ca cheie compusă. Acum datele unui utilizator celebritate sunt răspândite peste multe partition-uri în loc de unul singur, cu costul de a interoga mai multe partition-uri pentru a citi toate datele unui utilizator.
A doua soluție e salting: prependi sau apendi un sufix aleator la cheia de partition pentru valorile fierbinți, astfel încât aceeași entitate logică e împărțită peste mai multe partition-uri fizice. Citirile trebuie să interogheze toate variantele salted și să fuzioneze, dar scrierile sunt distribuite. Documentația Cassandra are tratament detaliat al acestui pattern, iar ghidul de best-practices pentru partition key al DynamoDB e în mare parte despre evitarea hot partitions și aplicarea salting când evitarea eșuează.
A treia soluție e caching-ul datelor fierbinți în afara partition-ului. Dacă profilul utilizatorului celebritate e citit un milion de ori pe minut, n-are nevoie să trăiască în store-ul partiționat la fiecare citire. Pune-l în cache în Redis sau într-un edge CDN, iar partition-ul vede doar cache miss-urile. Aceasta e deseori cea mai ieftină soluție.
Query-uri cross-partition. Un query care are nevoie de date din mai multe partition-uri trebuie să facă fan-out: interoghează fiecare partition, adună rezultatele, fuzionează-le, posibil sortează sau agregă. Latency-ul query-ului e mărginit de cel mai lent partition, nu de medie, iar costul scalează cu numărul de partition-uri atinse. „Interoghează datele proprii ale utilizatorului” ar trebui să fie un query într-un singur partition dacă ID-ul utilizatorului e parte din cheia de partition. „Agregă metrici peste toți utilizatorii” e inevitabil un fan-out. Primul tip ar trebui să fie o operație la scară de milisecundă; al doilea tip va fi sute de milisecunde în cel mai bun caz, iar în unele sisteme asta justifică împingerea agregării către un store de analiză separat în întregime (argumentul de polyglot persistence din lecția 24).
Principiul general: alege cheia de partition să se alinieze cu cel mai comun pattern de query. Dacă majoritatea citirilor sunt „ia toate datele pentru utilizatorul X”, fă ID-ul utilizatorului cheie de partition. Dacă majoritatea citirilor sunt „ia toate evenimentele în range-ul de timp T”, fă timestamp-ul cheie de partition. Dacă ambele pattern-uri sunt comune și trag în direcții diferite, ai o tensiune pe care nicio strategie de partitioning nu o va rezolva curat și s-ar putea să ai nevoie de un sistem secundar (un index de search, un warehouse de analiză) ca să servească a doua formă de query.
Problema de rebalancing
Când nodurile se alătură sau pleacă, partition-urile trebuie să se mute. Un nod nou ajunge gol și are nevoie să preia cota lui de partition-uri. Partition-urile unui nod care eșuează trebuie redistribuite supraviețuitorilor. Făcut bine, asta e invizibil pentru aplicație: baza de date mută datele în background, query-urile continuă către partition-urile pe care ar trebui să le lovească, iar cluster-ul ajunge la o nouă stare balansată fără ca cineva să observe.
Făcut prost, rebalancing-ul e un eveniment operațional de mai multe zile. Cazul rău clasic e o schemă de partitioning care nu suportă rebalancing incremental: adăugarea unui singur nod forțează fiecare cheie să fie re-mapată, fiecare byte să fie re-trimis, iar cluster-ul petrece durata rebalancing-ului rulând la capacitate semnificativ redusă. Consistent hashing a fost inventat specific ca să evite asta, iar faptul că încă vedem ocazional cazul rău în producție e de obicei un semn al utilizării greșite a strategiei de partitioning sau al unei migrări prost gestionate.
Pattern-urile de știut.
Număr fix de partitions, mult mai multe partition-uri decât noduri. Cassandra (cu un număr mare de vnodes), Riak și câteva altele mențin un număr fix de partition-uri logice și le atribuie la noduri fizice. Adăugarea unui nod nu schimbă numărul de partition-uri, doar atribuirea, deci doar cheile din partition-urile mutate migrează.
Partitioning dinamic. HBase, BigTable și cluster-ele sharded MongoDB împart partition-urile când cresc prea mari și le fuzionează când se micșorează. Numărul de partitions se adaptează la volumul de date. Adăugarea unui nod declanșează redistribuirea, dar migrarea se face partition cu partition, nu cheie cu cheie.
Cotă fixă per nod. Unele sisteme atribuie o cotă fixă de partition-uri per nod (1/N când există N noduri). Adăugarea unui nod forțează 1/(N+1) din datele fiecărui nod existent să se mute. Simplu de raționat, scump când nodurile se alătură frecvent.
Lecția operațională e că rebalancing-ul e operația scumpă în ciclul de viață al unui sistem partiționat, iar rata la care poți scala în sus sau în jos e mărginită de cât de repede poate sistemul să rebalanseze fără să satureze rețeaua. Dacă te aștepți să adaugi capacitate rapid, asta e o proprietate de testat sub load înainte să ai nevoie de ea.
Replication și partitioning împreună
Ca să închidem bucla cu cele două lecții anterioare: într-un deployment real de producție, fiecare partition e el însuși replicat. Configurația standard a Cassandrei e N=3 replici per partition, RF=3 peste cluster. DynamoDB replică fiecare partition peste trei availability zones. Cluster-ele sharded MongoDB înfășoară fiecare shard într-un replica set. Pattern-urile se combină ca o grilă bidimensională: partition-uri pe o axă, replici pe cealaltă.
Povestea de consistency e și ea bidimensională. Replication are propriul mod de consistency (synchronous, asynchronous, bazat pe quorum, bazat pe leader). Partitioning are propriile întrebări tranzacționale cross-partition (two-phase commit-ul lecției 15, consensus-ul lecției 14). Un query care se întinde peste partition-uri și replici face alegeri de consistency pe ambele axe, iar garanțiile generale ale bazei de date sunt produsul acelor alegeri.
Lecțiile următoare din acest modul preiau acel fir. Lecția 28 duce mai adânc problema hot-partition, în pattern-urile și anti-pattern-urile pe care sistemele de producție le folosesc ca să gestioneze asimetria. Lecții ulterioare se întorc către consistency peste partition-uri: tranzacții distribuite, pattern-uri Saga și trade-off-urile care apar când o singură operație vizibilă pentru utilizator trebuie să atingă date peste mai multe store-uri partiționate și replicate.
Citări și lecturi suplimentare
- Martin Kleppmann, Designing Data-Intensive Applications (O’Reilly, 2017), Capitolul 6. Tratamentul de referință al partitioning-ului, cu același cadru de trade-off hash-versus-range folosit aici.
- David Karger et al., “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web”, STOC 1997. Lucrarea originală despre consistent hashing, care e mai lizibilă decât sugerează titlul.
- AWS DynamoDB Developer Guide, “Designing Partition Keys to Distribute Workload Evenly”,
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html(consultat 2026-05-01). Ghidare practică despre designul partition key, evitarea hot partitions și write sharding. - Documentația Cassandra, “Data Modeling: Partition Keys and Clustering”,
https://cassandra.apache.org/doc/latest/cassandra/data_modeling/index.html(consultat 2026-05-01). Pattern-ul cu cheie compusă, cu clustering keys pentru ordonare în interiorul partition-ului. - Documentația MongoDB, “Sharded Cluster Components” și “Choose a Shard Key”,
https://www.mongodb.com/docs/manual/sharding/(consultat 2026-05-01). Range și hash sharding într-un sistem care le suportă pe amândouă. - Fay Chang et al., “Bigtable: A Distributed Storage System for Structured Data”, OSDI 2006. Designul canonic partiționat după range.