Ciao a tutti. Sono Vladislav Rodin. Attualmente insegno sul portale OTUS corsi dedicati all'architettura del software e architettura del software soggetto a carichi elevati. In vista dell'avvio di un nuovo ciclo del corso ho deciso di scrivere un breve materiale originale che desidero condividere con voi.

Introduzione
Poiché sugli HDD possono essere eseguite solo circa 400-700 operazioni al secondo (cosa che è incomparabile con i tipici rps che ricadono su un sistema ad alto carico), un classico database su disco rappresenta un collo di bottiglia nell'architettura. Pertanto, è necessario prestare particolare attenzione ai modelli di scalabilità di questo storage.
Attualmente ci sono 2 modelli di scalabilità del database: replica e sharding. Lo sharding consente di scalare l'operazione di scrittura e, di conseguenza, ridurre gli rps di scrittura che ricadono su un singolo server del vostro cluster. La replica consente di fare lo stesso, ma con le operazioni di lettura. Questo articolo è dedicato proprio a questo modello.
Replica
Guardando la replica da un punto di vista generale, è una cosa semplice: avevi un server, su cui erano presenti dei dati, e poi quel server ha smesso di gestire il carico di lettura di quei dati. Aggiungi un paio di server, sincronizzi i dati su tutti i server, e l'utente può leggere da qualsiasi server del tuo cluster.
Nonostante la semplicità apparente, esistono diverse varianti di classificazione delle varie implementazioni di questo schema:
- Per ruoli nel cluster (master-master o master-slave)
- Per oggetti trasmessi (row-based, statement-based o mixed)
- Per il meccanismo di sincronizzazione dei nodi
Oggi ci occuperemo precisamente del terzo punto.
Come avviene il commit di una transazione
Questo tema non è direttamente collegato alla replica, potrebbe essere scritta un articolo separato su di esso, tuttavia, poiché senza comprendere il meccanismo del commit della transazione la lettura futura è inutile, mi permetto di ricordare le cose più fondamentali. Il commit della transazione avviene in 3 fasi:
- Registrazione della transazione nel registro del database.
- Applicazione della transazione nel motore del database.
- Restituzione di una conferma al cliente dell'applicazione riuscita della transazione.
Nelle diverse basi, potrebbero sorgere sfumature in questo algoritmo: ad esempio, nel motore InnoDB di MySQL ci sono 2 log: uno per la replicazione (binary log) e l'altro per mantenere l'ACID (undo/redo log), mentre in PostgreSQL esiste un solo log che svolge entrambe le funzioni (write ahead log = WAL). Tuttavia, sopra è stata presentata proprio la concettualizzazione generale, che consente di non considerare tali sfumature.
Replica sincrona (sync)
Aggiungiamo all'algoritmo di commit della transazione la logica per replicare le modifiche ricevute:
- Registrazione della transazione nel registro del database.
- Applicazione della transazione nel motore del database.
- Invio dei dati a tutte le repliche.
- Ricezione della conferma da tutte le repliche sull'esecuzione della transazione.
- Restituzione di una conferma al cliente dell'applicazione riuscita della transazione.
Con questo approccio, otteniamo una serie di svantaggi:
- il cliente attende l'applicazione delle modifiche su tutte le repliche.
- aumentando il numero di nodi nel cluster, diminuiamo la probabilità che l'operazione di scrittura abbia successo.
Se il primo punto è relativamente chiaro, le ragioni del secondo punto meritano una spiegazione. Se con la replicazione sincrona non riceviamo risposta da almeno un nodo, annulliamo la transazione. Pertanto, aumentando il numero di nodi nel cluster, aumentate anche la probabilità che l'operazione di scrittura fallisca.
Possiamo aspettarci una conferma solo da una parte dei nodi, ad esempio, dal 51% (quorum)? Sì, possiamo, ma nella versione classica è richiesta la conferma da tutti i nodi, poiché è così che possiamo garantire la completa coerenza dei dati nel cluster, che è un indiscutibile vantaggio di questo tipo di replica.
Replica asincrona (async)
Modifichiamo l'algoritmo precedente. Invieremo i dati alle repliche "quando sarà", e "quando sarà" le modifiche saranno applicate alle repliche:
- Registrazione della transazione nel registro del database.
- Applicazione della transazione nel motore del database.
- Restituzione di una conferma al cliente dell'applicazione riuscita della transazione.
- Invio dei dati alle repliche e applicazione delle modifiche da parte loro.
Questo approccio porta a un cluster che funziona rapidamente, poiché non teniamo il cliente in attesa mentre i dati arrivano alle repliche e vengono anche confermati.
Ma la condizione di invio dei dati alle repliche "quando sarà" può portare alla perdita di una transazione, in particolare della transazione confermata all'utente, poiché se i dati non riescono a replicarsi in tempo, la conferma di successo dell'operazione viene inviata al cliente, ma se il nodo a cui sono arrivate le modifiche ha un malfunzionamento dell'HDD, perdiamo la transazione, il che può portare a conseguenze molto sgradevoli.
Replica semisincro
Finalmente siamo giunti alla replica semisincro. Questo tipo di replica non è molto noto e non è molto comune, tuttavia suscita un notevole interesse poiché può combinare i vantaggi della replica sincrona e asincrona.
Cerchiamo di unire i due approcci precedenti. Non faremo aspettare a lungo il cliente, ma richiederemo che i dati vengano replicati:
- Registrazione della transazione nel registro del database.
- Applicazione della transazione nel motore del database.
- Invio dei dati alle repliche.
- Ricezione della conferma dalla replica riguardo alla ricezione delle modifiche (saranno applicate «in un secondo momento»).
- Restituzione di una conferma al cliente dell'applicazione riuscita della transazione.
Si noti che con questo algoritmo la perdita di una transazione avviene solo in caso di caduta sia del nodo che riceve le modifiche sia del nodo replica. La probabilità di tale guasto è considerata bassa e i rischi associati sono accettati.
Tuttavia, con questo approccio, esiste il rischio di letture fantasma. Immaginiamo il seguente scenario: al passo 4 non abbiamo ricevuto conferma da nessuna replica. Dobbiamo annullare questa transazione e non restituire conferma al cliente. Poiché i dati sono stati applicati al passo 2, tra la conclusione del passo 2 e l'annullamento della transazione si verifica un intervallo di tempo in cui le transazioni parallele possono vedere quelle modifiche che non dovrebbero esistere nel database.
Replica semisincera senza perdite
Se ci pensiamo un attimo, possiamo semplicemente scambiare i passi dell'algoritmo per risolvere il problema delle letture fantasma in questo scenario:
- Registrazione della transazione nel registro del database.
- Invio dei dati della replica.
- Ricezione della conferma dalla replica riguardo alla ricezione delle modifiche (saranno applicate «in un secondo momento»).
- Applicazione della transazione nel motore del database.
- Restituzione di una conferma al cliente dell'applicazione riuscita della transazione.
Ora confermiamo le modifiche solo se sono state replicate.
Risultato
Come sempre, non esistono soluzioni ideali, ma una serie di opzioni, ognuna con i propri vantaggi e svantaggi, adatte per diversi tipi di problemi. Questo vale anche per la scelta del meccanismo di sincronizzazione dei dati di un database replicato. Il set di vantaggi offerto dalla replicazione semi-sincrona è piuttosto significativo e interessante da considerare, nonostante la sua scarsa diffusione.
Questo è tutto. Ci vediamo al !
Fonte: habr.com
