Le due lezioni precedenti riguardavano la replication: tenere più di una copia degli stessi dati su più di una macchina, in modo che un singolo guasto non faccia perdere i dati e che la capacità di un singolo server non sia il tetto della velocità di lettura. La replication serve per la ridondanza. Da sola non ti permette di memorizzare più dati di quelli che ci stanno su una macchina, e non ti permette di gestire un carico di scrittura più grosso di quello che il leader riesce a sostenere. Per questi problemi la tecnica è il partitioning: dividere i dati in modo che ogni macchina ne conservi un sottoinsieme diverso.
Le due cose sono ortogonali, e la maggior parte dei database grandi le fa entrambe contemporaneamente. Ogni partition è replicata per durabilità e per scalare le letture; ogni replica set è una delle tante partition che coprono parti diverse dei dati. Cassandra, DynamoDB, i cluster sharded di MongoDB, Elasticsearch e quasi tutti gli altri storage distribuiti combinano i due pattern in questo modo. Questa lezione copre il lato partitioning di quel quadro, da solo. Combinalo con le lezioni precedenti e hai la visione completa.
La decisione che uno schema di partitioning deve prendere è, semplicemente: dato un pezzo di dato, quale nodo lo contiene? Il dato in sé non cambia; cambia solo la sua posizione. Tre famiglie di strategie rispondono a quella domanda, e ciascuna fa lo stesso trade-off in modo leggermente diverso: ottimizza per una forma di query e la paga su un’altra.
Range-based partitioning
I dati sono ordinati per chiave. Lo spazio delle chiavi è diviso in range contigui, e ogni range va a una partition. La partition 1 potrebbe coprire le chiavi da A a G; la partition 2 copre da H a N; la partition 3 copre da O a Z. Una riga con chiave “Marco” vive sulla partition 2.
La strategia si adatta naturalmente a una struttura ordinata, quindi compare nei database che già organizzano i dati per chiavi ordinate. HBase, BigTable, e gli storage originali derivati da LevelDB funzionano così. I cluster sharded di MongoDB possono usare il range-based partitioning quando la shard key è configurata per quello. Molti database time-series partizionano per range temporale, che è semplicemente range partitioning dove la chiave è un timestamp.
Il vantaggio sono le range query. Se la tua applicazione chiede “dammi tutte le chiavi tra J e L”, il database sa esattamente quale partition contiene quel range, manda la query lì, e restituisce il risultato senza toccare nessun’altra partition. Le query con vincoli temporali su uno store partizionato per tempo sono l’esempio canonico: “dammi tutti gli eventi di ieri” colpisce una o due partition, mai di più.
Lo svantaggio sono le hot partition con dati skewed. Se la tua chiave è qualcosa come il cognome di un utente, l’alfabeto non è distribuito uniformemente: tantissime più persone hanno cognomi che iniziano con lettere centrali rispetto a quelle vicine alla fine. La partition centrale prende un traffico sproporzionato. Se la tua chiave è un timestamp e le scritture sono in tempo reale, ogni nuova scrittura va alla partition più recente, e quella singola partition prende tutto il carico di scrittura mentre le partition più vecchie restano inattive. Il range partitioning è eccellente per i workload a cui si adatta e pessimo per quelli a cui non si adatta.
I confini delle partition sono di solito adattivi: il database divide una partition quando cresce troppo e fonde partition adiacenti quando si rimpiccioliscono. Questo mantiene le partition all’incirca bilanciate per dimensione di storage, ma non fa nulla per lo skew di accesso. Una partition piccola che è hot resta hot.
Hash-based partitioning
Invece di partizionare direttamente per chiave, si partiziona per hash della chiave. La chiave “Marco” è hashata in qualche intero; lo spazio degli hash è diviso in range; ogni range va a una partition. La partition per “Marco” è determinata da dove cade il suo hash, non da dove cade la sua posizione alfabetica.
Questa è l’opzione predefinita in Cassandra, in DynamoDB, nella maggior parte dei key-value store distribuiti, e nei cluster sharded di MongoDB quando la shard key è configurata per l’hashing. Postgres ha l’hash partitioning nativo dalla versione 11.
Il vantaggio è la distribuzione uniforme. Assumendo che la funzione hash sia buona, le chiavi sono sparse uniformemente nello spazio degli hash indipendentemente da quanto le chiavi originali siano skewed. Il problema “tutti hanno cognomi che iniziano con M” sparisce: gli hash di chiavi simili sono dispersi nello spazio delle partition, quindi nessuna singola partition diventa hot per il solo clustering delle chiavi. Carico di scrittura e storage sono bilanciati per costruzione.
Lo svantaggio è la perdita delle range query. La funzione hash mescola le chiavi, quindi chiavi consecutive nello spazio originale finiscono su partition diverse. “Dammi tutte le chiavi tra J e L” diventa un fan-out: il database non ha modo di sapere quale partition contiene quale chiave senza calcolare l’hash, quindi deve interrogare ogni partition e fare il merge dei risultati. Per i workload che fanno range query sulla chiave di partitioning, l’hash partitioning è lo strumento sbagliato.
In pratica, molti sistemi supportano range query su un secondary index anche quando il partitioning è hash-based, ma quelle query sono comunque fan-out su tutte le partition. Il costo del fan-out è il trade-off centrale.
Un pattern correlato e più sottile è il compound partitioning: hash sulla prima parte della chiave, sort sulla seconda. Cassandra lo chiama “partition key più clustering key”. L’hash sulla partition key sparge i dati uniformemente, e dentro ogni partition le righe sono ordinate per clustering key, quindi le range query dentro una singola partition sono efficienti. La query “tutti i messaggi dell’utente X tra il tempo T1 e T2” colpisce una sola partition (perché lo user ID è la partition key) e restituisce un range ordinato al suo interno. Questo è il pattern di lavoro per time-series su Cassandra e per gli store di chat-message.
Consistent hashing e la vista per chiave
Il terzo pattern è strettamente legato all’hash-based partitioning ma viene inquadrato in modo diverso. Immagina uno spazio di hash circolare, l’hash ring. A ciascun nodo del cluster è assegnata una o più posizioni sull’anello (spesso chiamate virtual node o vnode). Una chiave, una volta hashata, finisce in qualche punto dell’anello; il nodo che la possiede è il successivo in senso orario rispetto a quel punto.
Questo è il consistent hashing, e la proprietà che lo rende utile è il suo comportamento quando i nodi entrano o escono. Aggiungere un nodo gli assegna un nuovo insieme di posizioni sull’anello; solo le chiavi che cadono tra il predecessore del nuovo nodo e il nuovo nodo devono spostarsi. Rimuovere un nodo passa le sue chiavi al nodo successivo in senso orario. In entrambi i casi solo una frazione delle chiavi (proporzionale al numero di nodi) viene ribilanciata, invece del re-hashing catastrofico che richiederebbe un partitioning naive basato sul 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
Il consistent hashing è il meccanismo di partitioning in Cassandra, DynamoDB, Riak, nelle modalità cluster di memcached/Redis distribuiti, e nella maggior parte dei sistemi peer-to-peer. Il paper originale del 1997 di Karger et al. parlava di cache distribuite, ma la tecnica si è generalizzata a quasi ogni key-value store distribuito moderno.
La scelta della partition key
La strategia conta meno della partition key. Una chiave cattiva su una buona strategia crea più dolore di una buona chiave su una strategia mediocre. Le due failure mode che vale la pena nominare.
Hot partition da skew di chiave. Una partition che contiene un singolo valore popolare, o un piccolo insieme di valori popolari, prende una quota sproporzionata del traffico. L’esempio classico è partizionare per user_id in un sistema dove un utente (una celebrità, un account brand, un bot) genera più traffico di migliaia di utenti normali messi insieme. La partition che contiene i dati di quell’utente è sovraccarica; le altre partition sono sottoutilizzate. La stessa forma capita con post virali, prodotti hot su un sito e-commerce, e sessioni attive su un’applicazione di chat.
Le soluzioni sono specifiche per il workload.
La prima soluzione è una partition key migliore. Se user_id da solo crea hot partition, usa (user_id, time_bucket) o (user_id, message_id) come compound key. Adesso i dati di un utente celebrità sono spalmati su molte partition invece che su una, al costo di dover interrogare più partition per leggere tutti i dati di un singolo utente.
La seconda soluzione è il salting: anteporre o aggiungere un suffisso casuale alla partition key per i valori hot, in modo che la stessa entità logica venga divisa su più partition fisiche. Le letture devono interrogare tutte le varianti salted e fare il merge, ma le scritture sono distribuite. La documentazione di Cassandra ha una trattazione dettagliata di questo pattern, e la guida di best practice di DynamoDB sulle partition key è in gran parte dedicata a evitare le hot partition e ad applicare il salting quando evitarle non basta.
La terza soluzione è mettere i dati hot in cache fuori dalla partition. Se il profilo dell’utente celebrità viene letto un milione di volte al minuto, non serve che viva nello store partizionato a ogni lettura. Mettilo in cache in Redis o in un edge CDN, e la partition vede solo le cache miss. Spesso è la soluzione più economica.
Cross-partition query. Una query che ha bisogno di dati da molte partition deve fare fan-out: interrogare ogni partition, raccogliere i risultati, fare il merge, eventualmente ordinare o aggregare. La latenza della query è limitata dalla partition più lenta, non dalla media, e il costo scala con il numero di partition toccate. “Interroga i dati dell’utente stesso” dovrebbe essere una query a una sola partition se lo user ID fa parte della partition key. “Aggrega metriche su tutti gli utenti” è inevitabilmente un fan-out. Il primo tipo dovrebbe essere un’operazione su scala di millisecondi; il secondo sarà nella migliore delle ipotesi su scala di centinaia di millisecondi, e in alcuni sistemi questo giustifica spostare l’aggregazione del tutto in uno store di analytics separato (l’argomento della polyglot persistence dalla lezione 24).
Il principio generale: scegli la partition key allineandola al pattern di query più comune. Se la maggior parte delle letture sono “prendi tutti i dati per l’utente X”, fa’ dello user ID la partition key. Se la maggior parte delle letture sono “prendi tutti gli eventi nel range temporale T”, fa’ del timestamp la partition key. Se entrambi i pattern sono comuni e tirano in direzioni diverse, hai una tensione che nessuna strategia di partitioning risolverà in modo pulito, e potresti aver bisogno di un sistema secondario (un search index, un analytics warehouse) per servire la seconda forma di query.
Il problema del rebalancing
Quando i nodi entrano o escono, le partition devono spostarsi. Un nuovo nodo arriva vuoto e deve prendersi la sua quota di partition. Le partition di un nodo guasto devono essere ridistribuite ai sopravvissuti. Fatto bene, è invisibile all’applicazione: il database sposta i dati in background, le query continuano contro le partition che dovrebbero colpire, e il cluster raggiunge un nuovo stato bilanciato senza che nessuno se ne accorga.
Fatto male, il rebalancing è un evento operativo di più giorni. Il caso classico negativo è uno schema di partitioning che non supporta il rebalancing incrementale: aggiungere un nodo costringe a rimappare ogni chiave, a rispedire ogni byte, e il cluster passa la durata del rebalance girando a capacità significativamente ridotta. Il consistent hashing è stato inventato proprio per evitarlo, e il fatto che ogni tanto vediamo ancora il caso negativo in produzione è di solito segno di un cattivo uso della strategia di partitioning o di una migrazione gestita male.
I pattern da conoscere.
Numero fisso di partition, molte più partition che nodi. Cassandra (con un alto numero di vnode), Riak, e diversi altri mantengono un numero fisso di partition logiche e le assegnano ai nodi fisici. Aggiungere un nodo non cambia il numero di partition, solo l’assegnazione, quindi solo le chiavi nelle partition spostate migrano.
Partitioning dinamico. HBase, BigTable, e i cluster sharded di MongoDB dividono le partition quando crescono troppo e le fondono quando si rimpiccioliscono. Il numero di partition si adatta al volume dei dati. Aggiungere un nodo innesca una redistribuzione, ma la migrazione viene fatta partition per partition, non chiave per chiave.
Quota fissa per nodo. Alcuni sistemi assegnano una quota fissa di partition per nodo (1/N quando ci sono N nodi). Aggiungere un nodo costringe 1/(N+1) dei dati di ogni nodo esistente a spostarsi. Semplice da ragionarci sopra, costoso quando i nodi entrano frequentemente.
La lezione operativa è che il rebalancing è l’operazione costosa nel ciclo di vita di un sistema partizionato, e la velocità con cui puoi scalare in su o in giù è limitata dalla velocità con cui il sistema riesce a ribilanciare senza saturare la rete. Se ti aspetti di aggiungere capacità velocemente, è una proprietà da testare sotto carico prima di averne bisogno.
Replication e partitioning insieme
Per chiudere il cerchio con le due lezioni precedenti: in un deployment di produzione reale, ogni partition è a sua volta replicata. La configurazione standard di Cassandra è N=3 repliche per partition, RF=3 nel cluster. DynamoDB replica ogni partition su tre availability zone. I cluster sharded di MongoDB avvolgono ogni shard in un replica set. I pattern si combinano come una griglia bidimensionale: partition lungo un asse, repliche lungo l’altro.
Anche la storia della consistency è bidimensionale. La replication ha la sua modalità di consistency (sincrona, asincrona, basata su quorum, basata su leader). Il partitioning ha le sue domande transazionali cross-partition (il two-phase commit della lezione 15, il consensus della lezione 14). Una query che attraversa partition e repliche fa scelte di consistency su entrambi gli assi, e le garanzie complessive del database sono il prodotto di quelle scelte.
Le prossime lezioni di questo modulo riprendono questo filo. La lezione 28 va più a fondo nel problema delle hot partition, nei pattern e negli anti-pattern che i sistemi di produzione usano per gestire lo skew. Le lezioni successive si rivolgono alla consistency tra partition: transazioni distribuite, pattern Saga, e i trade-off che emergono quando una singola operazione visibile all’utente deve toccare dati su più store partizionati e replicati.
Citazioni e letture di approfondimento
- Martin Kleppmann, Designing Data-Intensive Applications (O’Reilly, 2017), capitolo 6. Il trattamento di riferimento del partitioning, con lo stesso framing del trade-off hash-versus-range usato qui.
- David Karger et al., “Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web”, STOC 1997. Il paper originale sul consistent hashing, più leggibile di quanto suggerisca il titolo.
- AWS DynamoDB Developer Guide, “Designing Partition Keys to Distribute Workload Evenly”,
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html(consultato 2026-05-01). Indicazioni pratiche sul design delle partition key, sull’evitare le hot partition, e sul write sharding. - Documentazione di Cassandra, “Data Modeling: Partition Keys and Clustering”,
https://cassandra.apache.org/doc/latest/cassandra/data_modeling/index.html(consultato 2026-05-01). Il pattern del compound key, con clustering key per l’ordinamento dentro la partition. - Documentazione di MongoDB, “Sharded Cluster Components” e “Choose a Shard Key”,
https://www.mongodb.com/docs/manual/sharding/(consultato 2026-05-01). Range e hash sharding in un sistema che supporta entrambi. - Fay Chang et al., “Bigtable: A Distributed Storage System for Structured Data”, OSDI 2006. Il design canonico del range-partitioned.