Architettura di dati e sistemi, dalle fondamenta Lezione 29 / 80

Strategie di sharding e i loro tranelli

Application-level sharding, sharding nativo del database, Citus e Vitess. Le realtà pratiche di gestire un database SQL shardato.

La lezione precedente ci ha lasciato con i problemi operativi del partitioning: hot keys, rebalancing, il coordinatore che deve tenere traccia di chi possiede cosa. Tutto questo era abbastanza astratto da applicarsi a qualsiasi data store partizionato. Questa lezione atterra in una domanda molto più specifica a cui la maggior parte dei team prima o poi deve rispondere: come si fa davvero a shardare un database SQL in produzione, quando il Postgres o il MySQL single-server con cui sei partito non basta più?

Prima il vocabolario. “Sharding” è partitioning orizzontale su più macchine fisiche. Il concetto è lo stesso del partitioning, ma la connotazione è diversa: quando qualcuno dice partitioning intende spesso un singolo sistema distribuito che ha più partizioni interne (Cassandra, Mongo, ScyllaDB), e quando dice sharding di solito intende una flotta di istanze di database indipendenti coordinate sopra il livello del database. La linea è sfumata e le parole vengono usate in modo intercambiabile nella pratica. Quello che conta è l’architettura concreta, non l’etichetta.

Questa lezione attraversa le quattro opzioni reali nel 2026. Application-level sharding, dove il tuo codice instrada le query al database giusto. Sharding nativo di Postgres tramite l’estensione Citus. Sharding di MySQL tramite Vitess (e PlanetScale, la versione managed più popolare). E lo sharding nativo dei sistemi database-native come MongoDB e Cassandra, che abbiamo già toccato e che rivisitiamo brevemente qui per completezza.

Application-level sharding

Il modello mentale più semplice. Hai N istanze indipendenti di Postgres o MySQL, ognuna con lo stesso schema, ognuna con un sottoinsieme dei tuoi dati, e il codice della tua applicazione sa come instradare una query a quella corretta. La chiave di routing è solitamente tenant_id o user_id, la funzione di routing è solitamente un hash modulo il numero di shard o un range lookup in una tabella di configurazione, e ogni query che l’applicazione emette include abbastanza informazione perché l’applicazione possa scegliere lo shard giusto.

I pro sono reali. Hai il controllo totale: ogni shard è un database vanilla che capisci, che ha gli strumenti operativi che già usi, che fallisce in modi che hai già debuggato. Non ti serve un prodotto database fancy. Backup, monitoring e upgrade per shard sono semplicemente backup, monitoring e upgrade per database, ripetuti N volte. Ogni shard scala indipendentemente. Puoi far girare versioni diverse su shard diversi se devi. Puoi fermare uno shard per manutenzione senza coinvolgere gli altri.

Anche i contro sono reali. Le query cross-shard sono un tuo problema. Una query come “trovami tutti gli utenti creati nell’ultima settimana sull’intera flotta” deve essere emessa a ogni shard, l’applicazione deve assemblare i risultati, e l’applicazione deve gestire il caso in cui uno shard è lento o down. I cambi di schema vanno applicati a ogni shard, e il tooling di migration deve gestire il rollout su tutta la flotta. Le join cross-shard non sono join, sono codice di merge a livello applicativo. Le transazioni distribuite cross-shard sono fuori scope a meno che tu non te le costruisca da solo, cosa che non dovresti fare.

Il riassunto onesto: l’application-level sharding è la risposta giusta per prodotti molto grandi con team di piattaforma dedicati che sono disposti a investire in tooling operativo e non hanno bisogno di transazioni cross-shard nel percorso caldo. Molti dei più grandi deployment SQL al mondo (Stripe, Shopify, Notion, Figma) sono flotte Postgres o MySQL con application-level sharding, con migliaia di shard e un team consistente che le gestisce. Il pattern funziona a scala estrema; è anche costoso.

Citus, sharding nativo di Postgres

Citus è un’estensione di Postgres che aggiunge tabelle distribuite, transazioni distribuite e un query planner distribuito sopra Postgres standard. È nato come azienda indipendente, è stato acquisito da Microsoft, ed è ora sia un’estensione open-source sia il motore dietro “Cosmos DB for PostgreSQL” di Azure. L’architettura è un nodo coordinatore e una flotta di nodi worker; il coordinatore parsa l’SQL in arrivo, pianifica quali worker dovrebbero eseguire quali frammenti, e assembla i risultati.

In Citus, dichiari quali tabelle sono distribuite e su quale colonna. Un pattern tipico è distribuire un’applicazione multi-tenant per tenant_id: ogni tabella distribuita ha tenant_id come parte della primary key, e il planner usa quella colonna per instradare le query. Una query che filtra su tenant_id viene spinta interamente al worker che possiede quel tenant; una query che non filtra sulla colonna di distribuzione si dirama a ogni worker e viene ridotta al coordinatore. Ci sono anche reference tables (piccole tabelle di lookup replicate a ogni worker) e local tables (tenute solo al coordinatore) per i dati che non si adattano al pattern distribuito.

Il query planner è il pezzo più ingegnoso. Fa join cross-shard o co-locando le tabelle joined sulla stessa chiave di distribuzione (così la join è locale su ogni worker) o broadcastando un lato a tutti i worker. Supporta aggregati distribuiti calcolando risultati parziali per shard e combinandoli al coordinatore. Supporta insert, update e delete che toccano un singolo shard con semantiche transazionali complete, e scritture multi-shard via two-phase commit (lezione 15) quando lo abiliti.

Il caso a favore di Citus: vuoi SQL shardato ma vuoi mantenere le semantiche di Postgres. Lo schema è uno schema Postgres, le query sono query Postgres, le estensioni su cui ti appoggi continuano a funzionare. La tua applicazione non ha bisogno di sapere degli shard; il coordinatore sì. Il caso contro: ora stai operando un sistema più complesso del Postgres vanilla, il percorso delle query cross-shard ha i suoi modi di fallire, e il coordinatore è un hop su ogni query. Per workload che si adattano al pattern multi-tenant Citus è eccellente. Per workload dove la maggior parte delle query attraversa naturalmente la chiave di distribuzione si adatta meno.

Vitess, sharding nativo di MySQL

Vitess è l’equivalente per MySQL. È stato costruito a YouTube per gestire la loro flotta MySQL, è stato reso open-source, ed è ora un progetto graduated della CNCF. L’architettura è una flotta di primarie e repliche MySQL (i “tablet”), un layer di routing (vtgate), e un servizio di topologia (etcd o ZooKeeper) che tiene i metadata su quali chiavi vivono su quale shard.

Vitess presenta un singolo endpoint MySQL all’applicazione: i client si connettono a vtgate, che parsa l’SQL e lo instrada ai tablet appropriati. Come Citus, fa join cross-shard, aggregati distribuiti e transazioni per shard, con i trade-off che ti aspetteresti (le query cross-shard sono più lente e hanno più modi di fallire delle query single-shard). Gestisce anche l’online resharding: spezza uno shard in due, con il rebalancer che copia i dati e il layer di routing che effettua il cutover una volta che la destinazione si è messa in pari.

PlanetScale è la versione managed più popolare. Fa girare Vitess come servizio, espone un’esperienza serverless-database, e aggiunge un workflow di branching in stile Git per i cambi di schema. La feature di branching è genuinamente interessante: i cambi di schema avvengono su un branch, li mergi nel branch principale, e la macchina Vitess sottostante gestisce il rollout sicuro. Per i team che vogliono MySQL shardato senza operare Vitess da soli, PlanetScale è il punto di partenza ovvio.

Il caso a favore di Vitess: lo stesso di Citus, in sapore MySQL. Il caso contro: lo stesso di Citus, in sapore MySQL. I due prodotti risolvono lo stesso problema in due ecosistemi SQL diversi, e la scelta di solito si riduce a quale database il tuo team già utilizza.

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

La forma è identica per entrambi i prodotti. L’applicazione parla a un coordinatore, il coordinatore si dirama ai worker, il servizio di topologia tiene la verità su quale shard vive dove. Le etichette cambiano, l’architettura no.

Sharding nativo del database

La quarta opzione è usare un database costruito shardato fin dall’inizio. Cluster shardati MongoDB, Cassandra, ScyllaDB, CockroachDB, YugabyteDB, TiDB. Ognuno di questi sistemi espone un singolo database logico che internamente è un cluster partizionato, con la logica di sharding integrata nel motore invece che bullonata sopra come estensione o implementata nell’applicazione.

I pro: lo sharding è trasparente all’applicazione; il rebalancing è automatico; la storia operativa è un prodotto invece di due. I contro: stai usando un database diverso da quello SQL che il tuo team già conosce, e le feature SQL su cui ti appoggi possono essere parzialmente supportate o supportate con caveat. CockroachDB e Yugabyte puntano alla compatibilità completa del wire protocol di PostgreSQL e a un ampio supporto delle feature SQL; TiDB punta alla compatibilità con MySQL; MongoDB e Cassandra non offrono SQL in senso stretto.

Per i team che scelgono un database da zero nel 2026, i prodotti shardati-dal-primo-giorno sono un’opzione seria, specialmente CockroachDB, Yugabyte, TiDB e le offerte cloud in stile Spanner. Per i team che hanno già una flotta Postgres o MySQL e devono farla crescere, la conversazione è di solito tra application-level sharding e Citus o Vitess.

L’incubo della migration

La maggior parte dei team non arriva a “dovrei shardare?” finché non ha un database non shardato di diversi anni che deve convertire in uno shardato senza tirare giù l’applicazione. Questo è uno dei problemi di ingegneria più difficili che la maggior parte dei team affronterà, e merita un trattamento esplicito.

La versione disonesta è “pre-shardiamo dal primo giorno”. Questo è un eccesso per quasi ogni team. Non conosci ancora i tuoi access pattern. Sceglierai la chiave di distribuzione sbagliata e dovrai rifarla. Pagherai il costo operativo e di complessità di un database shardato prima che il tuo traffico lo giustifichi. Il pre-sharding è la chiamata giusta solo per i team che già sanno, da esperienza pregressa, che saranno alla scala che lo richiede entro un anno o due. Per tutti gli altri è prematuro.

La versione onesta è la live migration. Il pattern è ben documentato e il blog post “online migrations” di Stripe è il riferimento canonico (https://stripe.com/blog/online-migrations, consultato 2026-05-01). La forma della migration è:

  1. Tira su il nuovo sistema shardato accanto al vecchio database monolitico.
  2. Inizia a fare dual-writing: ogni scrittura che va al vecchio database va anche al nuovo sistema shardato, in modo idempotente.
  3. Backfill: copia i dati storici dal vecchio database al nuovo a batch, con check che le dual-write non siano state superate dal backfill.
  4. Validate: esegui letture contro entrambi i sistemi per un periodo, confronta i risultati, fissa le inconsistenze finché i sistemi non concordano su ogni chiave.
  5. Sposta le letture: instrada il traffico di lettura al nuovo sistema. Il vecchio sistema continua a ricevere scritture via il percorso di dual-write.
  6. Ferma la dual-write al vecchio sistema: le scritture ora vanno solo al nuovo sistema.
  7. Decommissiona: archivia ed elimina il vecchio sistema.

Questo richiede mesi, non settimane. Ci sono sotto-problemi a ogni passo (come fai a rendere ogni scrittura idempotente se l’applicazione non era progettata per farlo; come gestisci le letture in volo durante il cutover; come fai il back-fill di un miliardo di righe senza schiacciare il database sorgente; come validi che due sistemi concordano senza prendere un lock globale). Ogni team che l’ha fatto ha le sue storie di guerra.

L’albero decisionale per “dovrei shardare, e se sì, come?” va più o meno così. Se i tuoi dati stanno comodamente sotto il terabyte e il tuo write rate è comodamente sotto le poche migliaia al secondo, non shardare. Postgres su una grande istanza singola lo gestirà per anni. Se stai spingendo quei limiti, chiediti se puoi scalare verticalmente prima: istanza più grande, più memoria, dischi più veloci, repliche di lettura per workload read-heavy. Se lo scaling verticale non basta, considera se il tuo workload si adatta a un pattern multi-tenant; se sì, Citus o Vitess è l’opzione SQL shardata a minor frizione. Se il tuo workload ha genuinamente bisogno di SQL distribuito con strong consistency tra shard, guarda CockroachDB, Yugabyte o Spanner. Se hai bisogno di controllo sopra ogni cosa e hai il team per farlo, l’application-level sharding rimane il tetto.

Quello che dovresti abbandonare è l’idea che lo sharding sia gratis o che la migration sarà rapida. Sono entrambi miti scortesi.

Cosa prepara questo

Abbiamo attraversato le realtà pratiche di partitioning e sharding: come scegliere una chiave di distribuzione, come gestire le hot keys, come fare rebalancing live, come shardare davvero un database SQL. La forma della prossima domanda è una che ha aleggiato per tutto il tempo: quando il sistema si spezza, quando la rete fallisce, quando due metà di un cluster non riescono a parlarsi, cosa succede alle garanzie di leadership e consistency che hai passato il Modulo 2 a stabilire?

Il modo di fallire principale è lo split brain: una partizione di rete che fa sì che due metà di un cluster credano entrambe di essere il leader, con entrambe le metà che accettano scritture in conflitto. La lezione 30 è su quel modo di fallire, sul perché il quorum è l’unica difesa affidabile, e sulle ricette di deployment che ti impediscono che ti capiti.

Citazioni e letture di approfondimento

  • Stripe Engineering, “Online migrations at scale”, https://stripe.com/blog/online-migrations (consultato 2026-05-01). Il riferimento canonico per le live database migration.
  • Documentazione Citus, https://docs.citusdata.com/ (consultato 2026-05-01). Il sistema di sharding via estensione Postgres.
  • Documentazione Vitess, https://vitess.io/docs/ (consultato 2026-05-01). Il sistema di sharding MySQL.
  • Documentazione PlanetScale, https://planetscale.com/docs (consultato 2026-05-01). Il servizio Vitess managed.
  • Documentazione CockroachDB, https://www.cockroachlabs.com/docs/ (consultato 2026-05-01). Una delle opzioni SQL shardate-dal-primo-giorno.
  • Martin Kleppmann, “Designing Data-Intensive Applications” (O’Reilly, 2017), Capitolo 6. Background sulle strategie di sharding.
Cerca