La lezione precedente ha descritto la replicazione leader/follower come il default per la maggior parte dei sistemi di produzione. Ha anche menzionato che la replicazione asincrona è il default dentro a quel default: il leader non aspetta che i follower confermino prima di acknowledgare una scrittura. Il costo in latenza dell’attesa è troppo alto per la maggior parte dei carichi, e qualche secondo di replication lag è di solito invisibile.
Di solito. Questa lezione parla delle volte in cui non è invisibile, dei bug percepiti dall’utente che ne risultano, e dei pattern che li risolvono. Il fenomeno ha un nome (replication lag) e la classe di bug ha un nome (read-after-write inconsistency, o più in generale, le anomalie di consistency che l’utente vede quando le sue letture finiscono su un follower che è indietro rispetto al leader). Entrambi i nomi descrivono la stessa cosa da angolazioni diverse. La lezione è piccola, ben nota, e il punto in cui molti team scoprono che “eventual consistency” non è uno slogan ma una preoccupazione operativa quotidiana.
Il bug, raccontato come una storia
Hai un setup Postgres leader/follower. Un leader, tre read replica, tutte asincrone. L’applicazione legge da qualunque replica via load balancer, e le scritture vanno direttamente al leader. Il replication lag è normalmente sotto i cento millisecondi.
Un utente posta un commento. L’applicazione manda l’INSERT al leader, che ritorna success in pochi millisecondi. L’applicazione redireziona l’utente al thread del commento. Il caricamento della pagina capita di atterrare su una read replica che non ha ancora ricevuto la modifica dal leader. La pagina si renderizza senza il commento dell’utente.
L’utente è confuso. Il commento è andato a buon fine? Cliccano “post” di nuovo. Stavolta il caricamento della pagina capita di colpire una replica che ha recuperato, e ora vedono due copie dello stesso commento. O tre. Oppure, il secondo INSERT arriva al leader prima che la prima replica abbia recuperato, e l’utente posta due volte e ne vede uno solo, e questo è ancora più confuso.
Questo è il replication lag, reso visibile. Il sistema si sta comportando esattamente come progettato: la scrittura è durabile, le read replica funzionano, il load balancer sta facendo il suo lavoro. L’utente è quello che vede un’inconsistenza, e all’utente non importa di CAP, PACELC (lezione 11), o dei modelli di consistency della lezione 12. Gli importa che non riesce a vedere quello che ha appena scritto.
Questo è il bug. È una di quattro anomalie user-visible che la replicazione asincrona può produrre.
Quattro classi di anomalia di consistency
Violazione di read-your-writes. Il bug di sopra. L’utente fa una scrittura, poi legge subito, e non vede la propria scrittura. È la modalità di fallimento più direttamente percepibile dall’utente. La gente nota quando la cosa che ha appena digitato è svanita. Non nota quando una riga che non ha mai visto è qualche centinaio di millisecondi indietro.
Violazione di monotonic-reads. L’utente legge, vede X. Fa refresh. Vede uno stato più vecchio, X meno uno. Questo succede quando due letture consecutive atterrano su replica diverse, e la seconda è più indietro della prima. Dalla prospettiva dell’utente, il tempo sembra scorrere all’indietro. Non è immediatamente visibile come la prima anomalia, ma disorientante quando capita, e particolarmente brutta in dashboard o feed di attività dove “vecchio dopo fresco” sembra un glitch.
Violazione causale. Un utente posta una domanda, e un amico posta una risposta. Un altro utente, che osserva il thread, vede la risposta prima della domanda. Questo succede perché domanda e risposta sono state scritte in tempi diversi, replicate in modo indipendente, e arrivate alla read replica dell’osservatore fuori ordine. La causalità è rotta: B sembra precedere A, anche se A ha causato B. Comune in messaggistica, thread di commenti, e documenti collaborativi.
Dati stale che persistono su scala. Una read replica resta indietro di minuti per via di una query long-running, di un blip di rete, o di un picco di carico sul leader. Ogni utente instradato a quella replica riceve dati stale per tutta la durata. L’applicazione non è visibilmente rotta (nessun errore), ma ogni lettura è sbagliata. Il team se ne accorge da un bug report o, peggio, da una dashboard di metriche che ha iniziato a mentirgli.
Le anomalie sono elencate in ordine grosso modo crescente di sottigliezza e decrescente di quanto spesso l’applicazione viene incolpata per esse. Read-your-writes è quella di cui gli utenti si lamentano. Le altre tre sono quelle che silenziosamente erodono la fiducia nel sistema.
Garanzia di read-your-writes
La soluzione più semplice per la prima anomalia è la più rozza. Dopo che un utente scrive, instrada le sue letture al leader per qualche finestra di tempo. Il leader ha la scrittura per definizione: le letture da esso la vedono sempre.
sequenceDiagram
participant U as User
participant LB as Load balancer
participant L as Leader
participant F as Follower
Note over U,F: Without read-your-writes (the bug)
U->>LB: POST /comment
LB->>L: INSERT comment
L-->>LB: success
LB-->>U: 200 OK
U->>LB: GET /thread
LB->>F: SELECT comments
F-->>LB: stale list (no new comment)
LB-->>U: missing comment
Note over U,F: With read-your-writes (the fix)
U->>LB: POST /comment
LB->>L: INSERT comment
L-->>LB: success
LB-->>U: 200 OK (set sticky flag)
U->>LB: GET /thread (sticky flag)
LB->>L: SELECT comments
L-->>LB: fresh list (with new comment)
LB-->>U: comment present
La domanda sulla “finestra di tempo” è il perno dell’implementazione. Tre varianti.
Finestra time-based. Dopo una scrittura, instrada le letture al leader per i successivi trenta secondi (o qualunque valore ecceda comodamente il replication lag worst-case). Dopo la finestra, torna ai follower. Facile da implementare, ma ogni fallimento di replica o link lento estende il lag worst-case, e la tua finestra deve essere settata in modo conservativo.
Finestra tracked-replica. La sessione di ogni utente memorizza un token: il log sequence number, il GTID, o il timestamp della sua scrittura più recente. Su ogni lettura, l’applicazione sceglie una replica che ha recuperato almeno fino a quel token, ricadendo sul leader se nessun follower lo ha fatto. Postgres espone questo tramite pg_last_wal_replay_lsn() e i client possono passare richieste Sync; MySQL ha WAIT_FOR_EXECUTED_GTID_SET. Più preciso del time-based, più complesso da implementare, e ne vale la pena per sistemi dove il carico di lettura sul leader è un costo reale.
Finestra per-user-and-resource. Tracci su base per-resource: l’utente ha bisogno di read-your-writes consistency solo sulle cose che ha scritto. Leggere dati di altre persone tollera il normale replication lag. Questo richiede che l’applicazione sappia quali letture sono post-write e quali no, cosa che di solito si mappa a “le letture dalla stessa controller action che ha appena fatto una scrittura sono instradate al leader, le altre letture no”. Pragmatico ed efficace.
Sticky session
Se un utente è instradato in modo consistente alla stessa replica per tutta la sessione, ottiene monotonic reads gratis: la replica si muove solo in avanti nel tempo, quindi letture consecutive da essa non possono andare indietro. Questo è il pattern “sticky session”: il load balancer o l’applicazione fissa un utente a una replica.
Il trade-off è chiaro. Fissare a una replica rompe il load balancing: se un utente popolare è fissato a una piccola replica, quella replica si sovraccarica. Se una replica fallisce, ogni utente fissato a essa va re-instradato, possibilmente a una replica più indietro, possibilmente producendo la violazione di monotonic-read che stavi cercando di evitare. Le sticky session non aiutano nemmeno attraverso logout/login, attraverso device, o attraverso sessioni abbastanza lunghe da far driftare il lag di una singola replica.
In pratica, le sticky session sono spesso combinate con un token tracked-replica: fissa dove possibile, e quando il re-instradamento è necessario, re-instrada solo a una replica che ha recuperato fino all’ultima posizione di log vista dall’utente.
Garanzia di monotonic-read
Se non puoi o non vuoi fissare a una singola replica, puoi comunque ottenere monotonic reads garantendo che ogni lettura sia servita da una replica almeno aggiornata quanto la precedente. La sessione traccia la posizione di log più recente che l’utente ha visto in qualunque lettura; le letture successive vanno solo a replica che hanno recuperato fino a quella posizione. Se nessuna replica lo ha fatto, la lettura va al leader.
Questa è una generalizzazione del pattern tracked-replica per read-your-writes: invece di tracciare solo “cosa l’utente ha scritto”, traccia “cosa l’utente ha visto”. Il costo di implementazione è simile; la proprietà di consistency è più forte.
Causal consistency
Read-your-writes e monotonic reads affrontano ognuna un’anomalia. La causal consistency affronta la terza: garantire che se la scrittura A precede causalmente la scrittura B, ogni lettore vede A prima di B. Il meccanismo è più articolato.
L’implementazione classica sono i vector clock: ogni replica traccia un vettore di contatori, uno per ogni altra replica, che registra quante scritture da ciascuna replica ha applicato. Una scrittura da una replica porta con sé un vector clock; un’altra replica applica la scrittura solo quando il proprio vettore domina le dipendenze. Questo garantisce ordine causale su tutto il sistema ma è costoso in metadata: ogni record porta un vettore che cresce con il numero di replica.
La maggior parte dei sistemi di produzione fa qualcosa di più economico. Tracciano dipendenze causali solo all’interno di una sessione (le letture e le scritture che un utente ha visto), le passano come token, e garantiscono che ogni lettura veda tutte le scritture causalmente antecedenti di quella sessione. Questo è il pattern nelle letture causal-consistent di MongoDB (dove il client passa un token clusterTime tra operazioni), in CockroachDB, e in sistemi simili. È più debole della causal consistency completa (i link causali cross-session possono ancora essere violati), ma copre la maggior parte dei casi pratici a una frazione del costo.
Il costo di ogni garanzia
Ogni proprietà di consistency che aggiungi ricompra un po’ di correttezza user-visible a un costo misurabile.
Read-your-writes manda alcune letture al leader, riducendo il beneficio di read-scaling delle replica. Se una pagina popolare fa un pattern write-poi-read, le letture di quella pagina colpiscono tutte il leader, e di fatto le hai de-scalate.
Sticky session rompono la capacità del load balancer di equilibrare il carico e complicano la gestione del fallimento delle replica. Inoltre non sopravvivono in modo pulito alle sessioni cross-device.
Token monotonic-read aggiungono complessità a ogni read path: l’applicazione deve conoscere il token, il load balancer deve filtrare le replica in base ad esso, e il data store deve esporre le posizioni di log. Funziona, ma sono più parti in movimento.
Token di causal-consistency aggiungono metadata a richieste e risposte, il che costa banda. Richiedono inoltre che l’applicazione passi il token attraverso ogni chiamata, anche tra servizi se l’architettura è profonda più di un servizio.
Non paghi tutti questi costi ovunque. La pratica ingegneristica ragionevole è pagarli sui path dove il bug sarebbe più visibile, e accettare il lag di default sui path dove non lo sarebbe.
Cosa significa “abbastanza buono” nel 2026
La maggior parte delle applicazioni può tollerare qualche secondo di replication lag per la maggior parte delle letture. L’utente non sta facendo refresh abbastanza in fretta da notarlo. I dati non stanno cambiando abbastanza in fretta da essere ambigui. L’applicazione si appoggia implicitamente a questo, e va bene così.
I posti dove non dovresti appoggiarti a questo sono quelli dove l’utente ha un modello mentale che il sistema si suppone soddisfi. Tre euristiche utili.
L’utente ha appena salvato il suo profilo e la pagina successiva carica il suo profilo? Leggi dal leader. L’utente ha appena postato un commento e si carica il thread di commenti? Leggi dal leader. L’utente ha appena piazzato un ordine e gli viene mostrato il riepilogo dell’ordine? Leggi dal leader. Sono tutti il pattern read-your-writes, applicato in modo selettivo ai path a più alta visibilità.
L’utente sta sfogliando un feed di contenuti di altre persone? Leggi da un follower. Qualche secondo di staleness è invisibile. L’utente è su una dashboard che si aggiorna ogni minuto? Leggi da un follower. La staleness è parte del contratto. Letture solo interne, come uno strumento di admin che fa girare report, possono tollerare secondi o minuti di lag senza che nessuno se ne accorga.
La cosa che fa male è la staleness percepita dall’utente: quando l’utente ha un modello di come dovrebbero essere i dati, e il sistema fallisce a corrispondervi. La cosa che non fa male è la staleness invisibile: quando nessuno sta guardando da abbastanza vicino uno specifico valore da notare che è uno o due secondi indietro.
Ingegnerizzare le mitigazioni del replication lag riguarda principalmente capire quali letture sono percepite dall’utente e quali no, e applicare i pattern più costosi solo alle prime. Azzecca questa distinzione e il sistema si sente fortemente consistente gratis, sui path che importano all’utente, mantenendo i benefici di read-scaling delle replica ovunque altro.
Cosa copre la prossima lezione
La replicazione tiene più copie degli stessi dati su macchine diverse. Il partitioning, tema della lezione 27, splitta i dati in modo che ogni macchina contenga un sottoinsieme diverso. La maggior parte dei sistemi reali fa entrambi: ogni partition è replicata, ogni replica è leader o follower per i dati della sua partition. I pattern di questa lezione e della prossima si compongono, e il design risultante a quattro quadranti (replicato e partizionato) è il default per qualunque database che opera a scala significativa.
Citazioni e letture di approfondimento
- Martin Kleppmann, Designing Data-Intensive Applications (O’Reilly, 2017), Capitolo 5, “Problems with Replication Lag”. Il trattamento di riferimento, con la stessa tassonomia di anomalie usata qui.
- Documentazione Postgres, “Hot Standby” e
pg_last_wal_replay_lsn(),https://www.postgresql.org/docs/current/hot-standby.html(consultato 2026-05-01). I meccanismi della streaming replication e come tracciare il catch-up della replica. - Documentazione MongoDB, “Causal Consistency and Read and Write Concerns”,
https://www.mongodb.com/docs/manual/core/causal-consistency-read-write-concerns/(consultato 2026-05-01). Un esempio lavorato di token di causal-consistency a livello client/sessione. - Documentazione AWS RDS, “Working with Read Replicas”,
https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html(consultato 2026-05-01). La vista operativa delle read replica, le metriche di replication lag, e il failover. - Doug Terry et al., “Session Guarantees for Weakly Consistent Replicated Data”, IEEE PDIS 1994. Il paper originale che ha dato il nome a read-your-writes, monotonic reads, monotonic writes, e writes-follow-reads.