Quando utilizzare un database basato su CRDT

Roshan Kumar è un senior product manager presso Redis Labs.

Piegare la coerenza e la disponibilità descritte dal teorema CAP è stata una grande sfida per gli architetti delle applicazioni geo-distribuite. La partizione di rete è inevitabile. L'elevata latenza tra i data center si traduce sempre in una certa disconnessione tra i data center per un breve periodo di tempo. Pertanto, le architetture tradizionali per le applicazioni geo-distribuite sono progettate per rinunciare alla coerenza dei dati o per subire un impatto sulla disponibilità.

Sfortunatamente, non puoi permetterti di sacrificare la disponibilità per le applicazioni utente interattive. In tempi recenti, gli architetti hanno cercato di coerenza e hanno abbracciato il modello di coerenza finale. In questo modello, le applicazioni dipendono dal sistema di gestione del database per unire tutte le copie locali dei dati per renderle eventualmente coerenti.

Tutto sembra a posto con l'eventuale modello di coerenza fino a quando non ci sono conflitti di dati. Alcuni eventuali modelli di coerenza promettono il miglior sforzo per risolvere i conflitti, ma non riescono a garantire una forte coerenza. La buona notizia è che i modelli costruiti attorno a tipi di dati replicati senza conflitti (CRDT) offrono una forte coerenza finale.

I CRDT raggiungono una forte coerenza finale attraverso un set predeterminato di regole e semantica per la risoluzione dei conflitti. Le applicazioni basate su database basati su CRDT devono essere progettate per accogliere la semantica di risoluzione dei conflitti. In questo articolo esploreremo come progettare, sviluppare e testare applicazioni distribuite geograficamente utilizzando un database basato su CRDT. Esamineremo anche quattro casi d'uso di esempio: contatori, memorizzazione nella cache distribuita, sessioni condivise e acquisizione di dati in più regioni.

Il mio datore di lavoro, Redis Labs, ha recentemente annunciato il supporto CRDT in Redis Enterprise, con tipi di dati replicati senza conflitti che si uniscono al ricco portafoglio di strutture di dati: stringhe, hash, elenchi, set, set ordinati, bitfield, Geo, Hyperloglog e flussi in il nostro prodotto database. Tuttavia, la seguente discussione si applica non solo a Redis Enterprise, ma a tutti i database basati su CRDT.

Database per applicazioni geo-distribuite

Per le applicazioni distribuite geograficamente, è comune eseguire servizi in locale sui client. Questo riduce il traffico di rete e la latenza causata dal roundtrip. In molti casi, gli architetti progettano i servizi per connettersi a un database locale. Poi viene la domanda su come mantenere i dati coerenti in tutti i database. Un'opzione è gestirla a livello di applicazione: è possibile scrivere un processo di lavoro periodico che sincronizzerà tutti i database. Oppure puoi fare affidamento su un database che sincronizzerà i dati tra i database.

Per il resto dell'articolo, supponiamo che tu scelga la seconda opzione: lascia che il database faccia il lavoro. Come mostrato nella Figura 1 di seguito, l'applicazione distribuita geograficamente esegue servizi in più aree geografiche, con ogni servizio connesso a un database locale. Il sistema di gestione del database sottostante sincronizza i dati tra i database distribuiti nelle regioni.

Redis Labs

Modelli di coerenza dei dati

Un modello di coerenza è un contratto tra il database distribuito e l'applicazione che definisce la pulizia dei dati tra le operazioni di scrittura e di lettura.

Ad esempio, in un modello di coerenza forte, il database garantisce che le applicazioni leggeranno sempre l'ultima scrittura. Con coerenza sequenziale, il database garantisce che l'ordine dei dati letti sia coerente con l'ordine in cui sono stati scritti nel database. Nell'eventuale modello di coerenza, il database distribuito promette di sincronizzare e consolidare i dati tra le repliche del database dietro le quinte. Pertanto, se scrivi i tuoi dati su una replica del database e li leggi da un altro, è possibile che non leggerai l'ultima copia dei dati.

Consistenza forte

Il commit in due fasi è una tecnica comune per ottenere una forte coerenza. Qui, per ogni operazione di scrittura (aggiunta, aggiornamento, eliminazione) su un nodo del database locale, il nodo del database propaga le modifiche a tutti i nodi del database e attende che tutti i nodi riconoscano. Il nodo locale invia quindi un commit a tutti i nodi e attende un altro riconoscimento. L'applicazione sarà in grado di leggere i dati solo dopo il secondo commit. Il database distribuito non sarà disponibile per le operazioni di scrittura quando la rete si disconnette tra i database.

Eventuale consistenza

Il vantaggio principale dell'eventuale modello di coerenza è che il database sarà disponibile per eseguire operazioni di scrittura anche quando la connettività di rete tra le repliche di database distribuite si interrompe. In generale, questo modello evita il tempo di andata e ritorno sostenuto da un commit a due fasi e quindi supporta molte più operazioni di scrittura al secondo rispetto agli altri modelli. Un problema che l'eventuale coerenza deve affrontare sono i conflitti: le scritture simultanee sullo stesso elemento in due posizioni diverse. In base a come evitano o risolvono i conflitti, i database eventualmente coerenti vengono ulteriormente classificati nelle seguenti categorie:

  1. L'ultimo scrittore vince (LWW).  In questa strategia, i database distribuiti si basano sulla sincronizzazione del timestamp tra i server. I database scambiano il timestamp di ciascuna operazione di scrittura insieme ai dati stessi. In caso di conflitto, vince l'operazione di scrittura con l'ultimo timestamp.

    Lo svantaggio di questa tecnica è che presuppone che tutti gli orologi di sistema siano sincronizzati. In pratica, è difficile e costoso sincronizzare tutti gli orologi di sistema.

  2. Consistenza finale basata sul quorum: questa tecnica è simile al commit in due fasi. Tuttavia, il database locale non attende il riconoscimento da tutti i database; attende solo il riconoscimento dalla maggior parte dei database. Il riconoscimento da parte della maggioranza stabilisce il quorum. In caso di conflitto, vince l'operazione di scrittura che ha stabilito il quorum.

    D'altro canto, questa tecnica aggiunge la latenza di rete alle operazioni di scrittura, il che rende l'app meno scalabile. Inoltre, il database locale non sarà disponibile per le scritture se viene isolato dalle altre repliche di database nella topologia.

  3. Replica di unione: in questo approccio tradizionale, comune tra i database relazionali, un agente di unione centralizzato unisce tutti i dati. Questo metodo offre anche una certa flessibilità nell'implementazione delle proprie regole per la risoluzione dei conflitti.

    La replica di tipo merge è troppo lenta per supportare applicazioni coinvolgenti e in tempo reale. Ha anche un unico punto di errore. Poiché questo metodo non supporta le regole preimpostate per la risoluzione dei conflitti, spesso porta a implementazioni difettose per la risoluzione dei conflitti.

  4. Tipo di dati replicati senza conflitti (CRDT): apprenderete in dettaglio i CRDT nelle prossime sezioni. In poche parole, i database basati su CRDT supportano tipi di dati e operazioni che garantiscono una consistenza finale priva di conflitti. I database basati su CRDT sono disponibili anche quando le repliche del database distribuito non possono scambiare i dati. Forniscono sempre latenza locale alle operazioni di lettura e scrittura.

    Limitazioni? Non tutti i casi d'uso del database traggono vantaggio dai CRDT. Inoltre, la semantica di risoluzione dei conflitti per i database basati su CRDT è predefinita e non può essere sovrascritta.

Cosa sono i CRDT?

I CRDT sono tipi di dati speciali che convergono i dati di tutte le repliche di database. I CRDT popolari sono contatori G (contatori di sola crescita), contatori PN (contatori positivi-negativi), registri, set G (set di sola crescita), set 2P (set bifase), set OR ( set di rimozione osservati), ecc. Dietro le quinte, si basano sulle seguenti proprietà matematiche per far convergere i dati:

  1. Proprietà commutativa: a ☆ b = b ☆ a
  2. Proprietà associativa: a ☆ (b ☆ c) = (a ☆ b) ☆ c
  3. Idempotenza:  a ☆ a = a

Un contatore G è un perfetto esempio di un CRDT operativo che unisce le operazioni. Qui, a + b = b + a e a + (b + c) = (a + b) + c. Le repliche scambiano tra loro solo gli aggiornamenti (aggiunte). Il CRDT unirà gli aggiornamenti sommandoli. Un G-set, ad esempio, applica l'idempotenza ({a, b, c} U {c} = {a, b, c}) per unire tutti gli elementi. L'idempotenza evita la duplicazione degli elementi aggiunti a una struttura di dati mentre viaggiano e convergono attraverso percorsi diversi.

Tipi di dati CRDT e loro semantica di risoluzione dei conflitti

Strutture dati prive di conflitti: contatori G, contatori PN, set G

Tutte queste strutture di dati sono prive di conflitti per progettazione. Le tabelle seguenti mostrano come i dati vengono sincronizzati tra le repliche del database.

Redis Labs Redis Labs

I contatori G e PN sono popolari per casi d'uso come polling globale, conteggio dei flussi, rilevamento delle attività e così via. I G-set sono ampiamente utilizzati per implementare la tecnologia blockchain. I bitcoin, ad esempio, impiegano voci blockchain di sola aggiunta.

Registri: stringhe, hash

I registri non sono privi di conflitti per natura. In genere seguono i criteri di LWW o la risoluzione dei conflitti basata sul quorum. La Figura 4 mostra un esempio di come un registro risolve il conflitto seguendo la politica LWW.

Redis Labs

I registri vengono utilizzati principalmente per memorizzare la cache e i dati di sessione, le informazioni sul profilo utente, il catalogo dei prodotti, ecc.

Set 2P

I set a due fasi mantengono due set di G-set, uno per gli elementi aggiunti e l'altro per gli elementi rimossi. Le repliche si scambiano le aggiunte G-set quando si sincronizzano. Il conflitto sorge quando lo stesso elemento si trova in entrambi gli insiemi. In alcuni database basati su CRDT come Redis Enterprise questo è gestito dal criterio "Aggiungi vince sull'eliminazione".

Redis Labs

Il set 2P è una buona struttura dati per memorizzare dati di sessioni condivise come carrelli della spesa, un documento condiviso o un foglio di calcolo.

Come progettare un'applicazione per utilizzare un database basato su CRDT

Connettere la tua applicazione a un database basato su CRDT non è diverso dal connettere la tua applicazione a qualsiasi altro database. Tuttavia, a causa delle eventuali politiche di coerenza, la tua applicazione deve seguire un certo insieme di regole per offrire un'esperienza utente coerente. Tre chiavi: 

  1. Rendi la tua applicazione senza stato. Un'applicazione senza stato è in genere basata su API. Ogni chiamata a un'API risulta nella ricostruzione del messaggio completo da zero. Ciò garantisce di estrarre sempre una copia pulita dei dati in qualsiasi momento. La bassa latenza locale offerta da un database basato su CRDT rende la ricostruzione dei messaggi più rapida e semplice. 

  2. Seleziona il CRDT giusto che si adatta al tuo caso d'uso. Il contatore è il più semplice dei CRDT. Può essere applicato per casi d'uso come votazione globale, monitoraggio di sessioni attive, misurazione, ecc. Tuttavia, se si desidera unire lo stato degli oggetti distribuiti, è necessario considerare anche altre strutture di dati. Ad esempio, per un'applicazione che consente agli utenti di modificare un documento condiviso, potresti voler conservare non solo le modifiche, ma anche l'ordine in cui sono state eseguite. In tal caso, salvare le modifiche in un elenco basato su CRDT o in una struttura dati di coda sarebbe una soluzione migliore rispetto alla memorizzazione in un registro. È anche importante comprendere la semantica di risoluzione dei conflitti applicata dai CRDT e che la soluzione sia conforme alle regole.
  3. CRDT non è una soluzione valida per tutti. Sebbene CRDT sia davvero un ottimo strumento per molti casi d'uso, potrebbe non essere il migliore per tutti i casi d'uso (transazioni ACID, ad esempio). I database basati su CRDT generalmente si adattano bene all'architettura dei microservizi in cui si dispone di un database dedicato per ogni microservizio.

Il punto principale qui è che la tua applicazione dovrebbe concentrarsi sulla logica e delegare la gestione dei dati e la complessità della sincronizzazione al database sottostante.

Test delle applicazioni con un database multimaster distribuito

Per ottenere un go-to-market più rapido, ti consigliamo di disporre di una configurazione coerente di sviluppo, test, staging e produzione. Tra le altre cose, ciò significa che la configurazione di sviluppo e test deve avere un modello miniaturizzato del database distribuito. Verifica se il tuo database basato su CRDT è disponibile come contenitore Docker o come appliance virtuale. Distribuisci le repliche del database su sottoreti diverse in modo da poter simulare la configurazione del cluster connesso e disconnesso.

Testare le applicazioni con un database multimaster distribuito può sembrare complesso. Ma la maggior parte delle volte tutto ciò per cui verifichi è la coerenza dei dati e la disponibilità dell'applicazione in due situazioni: quando i database distribuiti sono connessi e quando è presente una partizione di rete tra i database.

Impostando un database distribuito a tre nodi nel proprio ambiente di sviluppo, è possibile coprire (e persino automatizzare) la maggior parte degli scenari di test nel test unitario. Ecco le linee guida di base per testare le tue applicazioni: