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

Pattern di replication: leader/follower, multi-leader, leaderless

Le tre famiglie di replication dei database, i trade-off che ciascuna fa tra consistency e availability, e dove ognuna si colloca nei sistemi reali.

Si apre qui il Modulo 4. Il Modulo 3 trattava di quale database scegliere. Il Modulo 4 tratta di cosa succede quando una singola istanza di quel database non basta più: quando un solo server non riesce a contenere i dati, quando un solo server non riesce a servire il carico, quando il fallimento di un solo server fa crollare l’intera applicazione. I pattern sono gli stessi indipendentemente dal fatto che il tuo store sia Postgres, MongoDB, Cassandra o DynamoDB. Sono anche vecchi. La letteratura sui sistemi distribuiti ci lavora dagli anni ‘80, e i trade-off che fanno compaiono in ogni data store moderno, a volte come configurazione e a volte cuciti dentro al design.

Questa prima lezione parla di replication: tenere più di una copia dei tuoi dati su più di una macchina. La lezione successiva parla delle conseguenze. Quella ancora dopo parla di partitioning, che è l’asse ortogonale. Insieme coprono gran parte di ciò che devi sapere per scalare un database orizzontalmente.

Perché replicare, in fondo

Quattro motivi, più o meno nell’ordine in cui i team li scoprono.

Durability. Un singolo server può fallire in molti modi: il disco muore, il kernel va in panic, il datacentre perde corrente, qualcuno inciampa in un cavo. Se la tua unica copia dei dati vive su quella macchina, il guasto è un evento di data-loss. Se i dati sono replicati su una seconda macchina in un failure domain separato, sopravvivi al primo guasto con i dati intatti. Questa è la ragione originale per cui esiste la replication.

Latency. Un utente a Singapore che legge da un database a Londra paga circa centosettanta millisecondi di round-trip per query. Se i dati sono replicati in una region a Singapore, la stessa lettura prende due millisecondi. I read replica in region geograficamente diverse barattano un po’ di consistency per guadagni di latency molto grossi sui workload pesanti in lettura.

Read throughput. Un singolo primary può servire solo un certo numero di letture al secondo prima che la sua CPU, l’IO o la rete saturino. Aggiungere read replica distribuisce il carico in lettura su più macchine. Le scritture devono comunque passare dal primary, ma nella maggior parte dei workload le letture superano le scritture in rapporto da dieci a cento a uno, e i read replica assorbono la gran parte della pressione.

Sicurezza di upgrade e maintenance. Quando il primary ha bisogno di un upgrade di major version, di una patch al sistema operativo o di nuovo hardware, non vuoi prenderti un’outage di diverse ore. Con la replication puoi promuovere un follower a primary, fare il lavoro sul vecchio primary e poi tornare indietro. L’applicazione vede una breve finestra di failover invece di un’outage lunga.

Tre famiglie di strategia di replication coprono praticamente ogni design in produzione. Si differenziano su un asse: dove vengono accettate le scritture.

Leader/follower

Un nodo è il leader. Accetta ogni scrittura. Applica la scrittura al proprio storage, poi spedisce il cambiamento a ogni follower. I follower applicano i cambiamenti nello stesso ordine. Le letture possono andare a qualunque replica che sia abbastanza aggiornata per le esigenze dell’applicazione.

Questo è il pattern standard in Postgres, MySQL, MongoDB replica set, Redis, SQL Server, Oracle e nella maggior parte dei database relazionali. I nomi cambiano: leader e follower, primary e secondary, master e slave (la terminologia più vecchia, ormai di solito sostituita). La forma è la stessa.

Due manopole di configurazione importanti.

Synchronous contro asynchronous. Synchronous replication significa che il leader aspetta la conferma di almeno N follower, che hanno applicato la scrittura, prima che la scrittura sia confermata al client. Asynchronous significa che il leader conferma non appena il proprio storage ha la scrittura, e la spedisce ai follower in background. Synchronous dà garanzie di durability più forti: se il leader muore subito dopo aver confermato, la scrittura è già su un follower. Asynchronous dà una latency in scrittura più bassa ma accetta una piccola finestra in cui una scrittura confermata può essere persa per un guasto del leader. Postgres lo chiama synchronous_commit e ti permette di configurarlo per transazione. La maggior parte dei deployment in produzione gira un mix: synchronous verso uno standby vicino per la durability, asynchronous verso le repliche più lontane per la latency.

Failover. Quando il leader muore, qualcuno deve promuovere un follower a nuovo leader. È più difficile di quanto sembri. Il nuovo leader deve essere il follower più aggiornato, altrimenti vanno perse scritture che erano state confermate. Bisogna dire ai client di mandare le scritture al nuovo leader. Il vecchio leader, quando torna su, va riconciliato con il nuovo stato. Se due nodi credono entrambi di essere il leader nello stesso momento (uno split-brain), puoi avere scritture in conflitto che il database non ha modo di fondere. I sistemi in produzione usano strumenti di coordinazione come Patroni, Orchestrator o meccanismi integrati (le election di MongoDB, Redis Sentinel) per rendere il failover automatico e sicuro. Farlo a mano alle 3 di notte è il tipo di storia operativa che finisce nei postmortem.

Il pattern leader/follower si sposa bene con il modello mentale relazionale: le scritture sono linearizzate attraverso un solo nodo, le transazioni hanno un ordine chiaro, la storia della consistency è semplice. Il costo è che le scritture non scalano oltre quello che il singolo leader riesce a gestire, e che il problema del failover è sempre presente.

Multi-leader

Due o più nodi accettano scritture. Ogni leader replica le proprie scritture verso gli altri leader. Le letture possono andare a qualunque leader.

I casi d’uso che lo motivano sono quelli che leader/follower gestisce in modo scomodo.

Multi-region active-active. Hai utenti in tre region. Con leader/follower, solo una region ha un database scrivibile; gli utenti nelle altre due region pagano latency cross-region su ogni scrittura. Con multi-leader, ogni region ha un leader, gli utenti scrivono sul proprio locale, e i leader si sincronizzano tra loro in background. La latency in scrittura resta bassa ovunque.

Multi-datacentre on-prem. Stesso ragionamento, applicato a deployment enterprise con due o più datacentre che hanno bisogno di continuare a funzionare in modo indipendente se il link tra loro va giù.

Applicazioni offline-first. App mobile, certi editor collaborativi, il tipo di sistema in cui ogni dispositivo è di fatto un leader per le proprie scritture e si sincronizza con un sistema centrale o con i peer la prossima volta che ha connettività. CouchDB e sistemi simili sono stati progettati attorno a questo pattern.

Il problema difficile con multi-leader è la conflict resolution. Due leader possono accettare scritture in conflitto durante la finestra che precede la loro sincronizzazione. L’utente A a Londra aggiorna la quantità dell’ordine a 5; l’utente B a Francoforte aggiorna la quantità dello stesso ordine a 7; entrambe le scritture vengono accettate; quando i leader si sincronizzano, ci sono ora due versioni valide dello stesso record e il sistema deve decidere quale sia quella corretta. Tre famiglie di risoluzione.

Last-write-wins. Ogni scrittura ha un timestamp; vince la più recente. Semplice, ma dipende da clock sincronizzati (il problema della lezione 13 sotto mentite spoglie) e scarta dati silenziosamente: l’aggiornamento di un utente viene perso senza preavviso. Accettabile per alcuni workload (l’ultima posizione nota di un utente, un campo “last seen at”), inaccettabile per la maggior parte.

CRDT (Conflict-free Replicated Data Types). Strutture matematiche progettate in modo che qualunque sequenza di operazioni provenienti da qualunque coppia di repliche fonda allo stesso stato finale, indipendentemente dall’ordine. Counter, set, liste ordinate, documenti tipo JSON: hanno tutti formulazioni in CRDT. Usati in Riak, Redis CRDB, Automerge, Yjs. Potenti ma vincolano il tuo data model: devi esprimere il tuo stato come operazioni CRDT, non come aggiornamenti arbitrari.

Risoluzione manuale o a livello di applicazione. Il sistema fa emergere i conflitti verso l’applicazione o verso l’utente, e sono loro a scegliere. I merge conflict di Git sono l’esempio canonico. Utile nei casi in cui l’applicazione ha il contesto per fare una scelta sensata, ma costoso in termini di complessità operativa.

Strumenti in produzione: Postgres BDR (bidirectional replication), MySQL Group Replication, Couchbase, Riak, certe topologie di CockroachDB. Multi-leader è reale, funziona, ma la storia dei conflitti è ciò che stai sottoscrivendo, e la maturità operativa richiesta è significativamente più alta che con leader/follower.

Leaderless

Non c’è un leader. Qualunque nodo accetta qualunque scrittura. Il client (o un coordinator per conto del client) manda la scrittura a N repliche; le letture interrogano N repliche; un quorum decide qual è il valore. Questo è il design Dynamo-style del paper di Amazon del 2007, ed è la base di Cassandra, ScyllaDB, DynamoDB, Riak e della maggior parte dei key-value store distribuiti su larga scala.

Il meccanismo si regge su tre numeri: N (il numero di repliche a cui va una scrittura), W (il numero che deve confermare perché una scrittura sia considerata riuscita) e R (il numero che deve rispondere perché una lettura sia considerata riuscita). L’applicazione sceglie W e R per richiesta, bilanciando latency contro consistency.

La regola famosa è W + R > N. Se il numero di repliche a cui hai scritto più il numero di repliche da cui hai letto è maggiore del totale delle repliche, allora qualunque lettura ha la garanzia di intersecare la scrittura più recente, e puoi rileggere le tue stesse scritture in modo consistente. Con N=3, W=2 e R=2 soddisfa la regola ed è il default comune. W=1 e R=1 (eventual consistency) è più veloce ma permette letture stantie. W=3 e R=1 rende le scritture più lente ma le letture più economiche.

Quando le repliche divergono, il sistema ha bisogno di un modo per convergere. Due meccanismi.

Read repair. Quando una lettura restituisce valori inconsistenti da repliche diverse, il coordinator sceglie il più recente (per version vector o timestamp), lo restituisce al client, e ripropone il valore corretto alle repliche che erano stantie. La riparazione avviene sul read path, in modo opportunistico.

Anti-entropy. Un processo in background confronta le repliche (tipicamente usando alberi di Merkle, in modo che il confronto costi poco) e sincronizza le differenze che trova. Questo intercetta i valori che nessuna lettura ha avuto modo di toccare.

La tabella dei trade-off per il leaderless: le scritture riescono in molti scenari di guasto che bloccherebbero un sistema leader-based, perché finché qualunque W repliche sono raggiungibili, la scrittura passa. Non c’è failover, perché non c’è un leader da cui fare failover. Il costo è che il modello di consistency è eventual per default, e l’applicazione deve riflettere con cura su W, R e conflict resolution.

I tre pattern affiancati

flowchart LR
    subgraph LF[Leader/follower]
        L1[Leader] --> F1[Follower]
        L1 --> F2[Follower]
        L1 --> F3[Follower]
    end
    subgraph ML[Multi-leader]
        M1[Leader A] <--> M2[Leader B]
        M2 <--> M3[Leader C]
        M1 <--> M3
    end
    subgraph LL[Leaderless]
        N1[Node] <--> N2[Node]
        N2 <--> N3[Node]
        N1 <--> N3
        N3 <--> N4[Node]
        N1 <--> N4
        N2 <--> N4
    end

Le tre famiglie fanno scelte diverse su quattro assi che vale la pena tenere in testa.

AsseLeader/followerMulti-leaderLeaderless
Scritture durante un guasto del leaderBloccate fino al failoverContinuano sugli altri leaderContinuano, nessun leader da far cadere
Consistency di defaultStrong sul leader, in lag sui followerEventual (conflitti possibili)Eventual, regolabile via W e R
Distribuzione geograficaUna sola region scrivibileTutte le region scrivibiliTutti i nodi scrivibili
Complessità della conflict resolutionBassa (niente scritture concorrenti)Alta (CRDT o logica applicativa)Media (versioning e quorum)

Una guida pragmatica: leader/follower è il default per la maggior parte delle applicazioni, e la maggior parte dei team dovrebbe smettere di leggere lì. Multi-leader si guadagna la sua complessità quando la latency di scrittura geografica o un vero active-active sono un requisito non negoziabile. Leaderless ha senso per scale molto grandi, requisiti di availability molto alti e workload key-value in cui la storia della consistency può essere disegnata attorno ai quorum.

Cosa srotolano le prossime lezioni

La lezione 26 prende il pattern di produzione più comune, leader/follower asynchronous, e guarda a come si sentono davvero i suoi trade-off dal punto di vista di un utente. La frase è “replication lag”, e la conseguenza è il bug “l’utente ha visto dati stantii” che ogni team incontra e che ogni team gestisce in modo leggermente diverso.

La lezione 27 si volta verso l’asse ortogonale: il partitioning. Dove la replication tiene più di una copia degli stessi dati, il partitioning spezza i dati tra i nodi in modo che ogni nodo tenga solo un pezzo. La maggior parte dei deployment di produzione reali fa entrambe le cose insieme, e le scelte si compongono.

Riferimenti e approfondimenti

  • Martin Kleppmann, Designing Data-Intensive Applications (O’Reilly, 2017), Capitolo 5. Il trattamento di riferimento dei pattern di replication; tutto in questa lezione è una compressione di quel capitolo.
  • Giuseppe DeCandia et al., “Dynamo: Amazon’s Highly Available Key-value Store”, SOSP 2007, https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf (consultato 2026-05-01). Il paper che ha definito il design leaderless basato su quorum.
  • Documentazione di Postgres, “High Availability, Load Balancing, and Replication”, https://www.postgresql.org/docs/current/high-availability.html (consultato 2026-05-01). Il riferimento per streaming replication, synchronous commit e pattern di failover.
  • AWS DynamoDB Developer Guide, “Best Practices for Designing and Architecting with DynamoDB”, https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html (consultato 2026-05-01). Il design leaderless in produzione.
  • Documentazione di MongoDB, “Replication”, https://www.mongodb.com/docs/manual/replication/ (consultato 2026-05-01). I replica set come implementazione leader/follower con election automatica.
Cerca