Introduzione a Cassandra

Logo Cassandra

Con questa introduzione iniziamo una serie di approfondimenti dedicati a Cassandra, un database NoSql che, come tanti altri progetti, è sotto l’ombrello di Apache. Cassandra, per gli amici C*, nasce all’interno di Facebook per supportare il sistema di messaggistica ed è diventato opensource nel 2008 per poi passare sotto Apache nel 2010. La versione più recente è la 3.4 3.5 anche se nella pratica, molti si affidano ancora alla più vecchia 2.2 il cui supporto finirà a fine anno.

Stiamo parlando di un progetto molto utilizzato e i principali adopter, come si vede dall’immagine qui sotto, sono davvero tanti! A noi basta sapere che se Netflix, Spotify, Instagram, Sony, Soundcloud.. (e mi fermo) lo usano, devono esserci delle caratteristiche interessanti e dei buoni risultati per specifici casi d’uso.

apache cassandra users

In effetti Cassandra parte già con un buon DNA, ereditando da Google Big Table il modello di storage e da Amazon Dynamo la gestione del cluster. Questo ha permesso di realizzare un database distribuito pensato per gestire grandi quantità di dati usando diversi server, fornendo alta disponibilità (high availability) ma senza un single point of failure. Senza, cioè, il fatto che il downtime di un nodo del cluster renda il sistema non utilizzabile come può avvenire in certe architetture di tipo master/slave. Un’altra caratteristica interessante è la linear scalability. Supponiamo di avere un cluster Cassandra composto da 10 nodi, aggiungendo altri 2 nodi, ad esempio, siamo abbastanza confidenti nel migliorare le prestazioni del cluster grosso modo del 20%, cosa non sempre così semplice da verificare per altri sistemi. Inoltre, C* è pensato out-of-the-box per lavorare con una topologia di rete distribuita su più data center tenendo conto di come replicare i dati tra server geograficamente distanti.

Dal punto di vista della distanza dal modello relazionale, c’è da dire che Cassandra non è un database relazionale, tuttavia il CQL, che è il linguaggio che si usa per definire lo schema e manipolare i dati, è in molti casi identico a SQL. Ci sono delle profonde differenze però sul modo che C* usa per memorizzare i dati internamente. Di fatto Cassandra è un ibrido tra un database key-value e uno column oriented ma usando CQL è difficile rendersene conto. La vecchia interfaccia Thrift, invece, rendeva molto più evidente il modello interno, tuttavia è stata deprecata e di fatto non viene più mantenuta.

Com’è fatto Cassandra

In Cassandra, ognuno dei nodi appartenente ad un cluster ha lo stesso ruolo degli altri (a parte una piccolissima eccezione): per questa caratteristica C* viene detto peer-to-peer. Già da questo si può capire perché non ci sia un single-point-of-failure: nessuno dei nodi è critico per il funzionamento del sistema, perché può essere sostituito dagli altri. Come è possibile questo? La prima cosa da dire è che i dati del database sono distribuiti equamente all’interno del cluster tra tutti i nodi, ogni nodo però non memorizza solo la sua porzione di dati, ma anche una replica parziale dei dati degli altri.

La figura qui sotto può aiutarci a capire: supponiamo poter memorizzare soltanto 256 possibili chiavi numeriche per un record (da -128 a 127) all’interno di un cluster di 4 nodi. Il primo nodo memorizzerà i valori da -128 a -65, il secondo da -64 a -1 e così via secondo questo andamento circolare.

cluster-token-rev

In effetti il cluster viene rappresentato come un anello di nodi perché questo è effettivamente il modo in cui le cose funzionano: tutti i nodi sono uguali e sono disposti “in cerchio”. Per ogni schema, che in Cassandra si chiama keyspace, possiamo decidere quante volte il dato va replicato. Ad esempio, se scegliamo un fattore di replica RF=3, la chiave 78 verrà replicata tre volte, sul nodo 4 perché è dove va messa in base alla partizione dei dati e poi sui nodi 1 e 2 per continuare il giro dell’anello. Questo vuol dire che anche se il nodo 4 fosse giù, e un nostro client interrogasse il sistema per conoscere il record con chiave 78, il nodo 1 e 2 potrebbero rispondere comunque, ma anche il nodo 3 chiedendo l’informazione al nodo 1 o al nodo 2 perché saprebbe su quali nodi si trova il dato.

Ovviamente Cassandra non usa una chiave a 8 bit come nell’esempio appena fatto, il token ring, così si chiama questo anello, invece è realizzato usando 64 bit, il che ci mette a disposizione 1,8*1034 possibili valori, un numero decisamente accettabile :). Per passare dalla chiave di un record (a prescindere dalla tabella a cui appartiene) alla chiave memorizzata nel token ring si usa una funzione di hash, chiamata partitioner. E’ proprio grazie al partitioner che è possibile sapere dove si trova un dato o dove il dato va scritto.

La consistenza dei dati

A questo punto però, se vi è chiaro il meccanismo, potrebbe venirvi un dubbio. Che succede se, quando inserisco il record con chiave 78, il nodo 4 è spento? Oppure se chiedo l’informazione al nodo 2, ma questo ha una copia vecchia del record perché durante un update era giù?

Qui bisognerebbe tirare in ballo (anche) il teorema CAP su cui sono stati versati fiumi di inchiostro e su cui si rischia di aumentare l’entropia. Mi limiterò invece a dire che la consistenza dei dati in Cassandra è impostabile a piacere e possiamo usare il sistema con livelli di consistenza maggiori o minori a seconda delle prestazioni che vogliamo avere. Tra l’altro è possibile impostare livelli di consistenza differenti per lettura e scrittura.

Consistenza in scrittura

Partiamo dalla scrittura e supponiamo di voler aggiornare proprio il record 78, che vuol dire modificare tutte e tre le sue copie perché il fattore di replica è appunto tre. Ipotizziamo che il nodo 2 sia inattivo (ma lo stesso succederebbe con il nodo 4 e 1). Possiamo decidere, ad esempio, di non permettere la scrittura se la replica non è avvenuta su tutti i nodi. In questo caso stiamo chiedendo un livello di consistenza uguale al fattore di replica. In questo scenario, l’operazione del client viene fatta fallire perché non possiamo soddisfare questa richiesta con questi vincoli. Tuttavia questa non è una cosa che ci piace perché con tutto il cluster a disposizione a meno di un nodo, proprio non ce la sentiamo di rimandare il client a mani vuote e rispondergli che l’inserimento non si può fare. Una strategia alternativa è questa: diamo esito positivo al client e scriviamo solo sui due nodi che sono in piedi. Ci teniamo però da parte il fatto che non appena il nodo 2 viene su, gli manderemo una replica del dato come se fossimo il client, purché ritorni attivo entro una soglia temporale. Questa strategia è detta hinted handoff. Il nodo 2, che magari stava facendo solo un aggiornamento di sistema, non appena ritornerà attivo, verrà informato sugli aggiornamenti del record e la consistenza verrà ripristinata.

Ma non finisce qui: possiamo anche scrivere usando un livello di consistenza più basso, ad esempio due. In questo caso accettiamo comunque l’operazione aggiornando 4 e 1 che danno acknowledge della scrittura ma lasciamo il sistema inconsistente perché non raggiungiamo il livello di replica pari a tre. Sistemeremo le cose durante la lettura del dato.

Consistenza in lettura

Come detto, supponiamo di avere l’informazione più aggiornata per il record 78 sul nodo 4 e 1 (nomino i nodi sempre nell’ordine del token), mentre sul nodo 2 il record è più vecchio; ora tutti i nodi sono attivi. In questo caso l’informazione sul cluster è solo parzialmente consistente come si vede nella figura qui sotto.

cluster2-cj-rev

Il nostro client chiede il record 78 al sistema, però sapendo che Cassandra è eventually consistent, non si fida di leggere il dato da un singolo nodo e richiede un livello di consistenza in lettura pari a due, cioè di recuperare l’informazione da almeno due nodi. Che può accadere? Se chiedo al nodo 4 e 1 che hanno l’informazione aggiornata il client riceve il record più nuovo e felice continua con le sue operazioni. Cassandra però non si ferma e chiede al nodo 2 di fornire la propria copia del record in questione. Usando i timestamp, che in Cassandra vengono associati ad ogni singola cella di una tabella, possiamo scoprire che il nodo 2 ha l’informazione più vecchia. Uno degli altri nodi gli passerà allora il record più nuovo, il nodo 2 sarà allineato ripristinando la consistenza. Una cosa analoga succede se leggo, ad esempio, dal nodo 1 e 2 se hanno informazioni discrepanti. Interrogando le repliche, Cassandra restituirà il record più recente campo per campo al client e poi ripristinerà la consistenza. Questa “riparazione” potrebbe essere costosa durante la normale operatività, per cui il sistema ci permette di farlo con un fattore di probabilità che di default è del 10%. Se questo meccanismo non ci soddisfa, possiamo anche forzare delle riparazioni manuali quando sappiamo che il sistema è scarico.

Cosa capita se invece mi accontento di leggere una sola replica e incappo nella risposta del nodo 2 che contiene il dato più “stantìo”? Facile, ho letto il record vecchio :). Ovviamente questa è un’opzione sempre valida se voglio ridurre al minimo i tempi di risposta leggendo meno repliche. Cassandra permette di aggiustare la consistenza secondo il “rischio” che vogliamo prendere per leggere più velocemente. Questa caratteristica, detta tunable consistency, è una delle cose più interessanti e per certi versi il fondamento del database stesso. Esistono molti altri modi in cui possiamo cambiare la consistenza delle nostre operazioni sul database, tra l’altro possiamo decidere livelli di consistenza diversi per ogni singola operazione sul database privilegiando la velocità di scrittura, di lettura o bilanciandole, ma questo potrebbe essere tema di un altro approfondimento.

Termina invece qui questa introduzione a Cassandra che, anche se breve, ci ha permesso di vedere alcuni meccanismi secondo i quali funziona questo interessante database NoSql. Nel prossimo post vedremo come installare un singolo nodo o un semplice cluster di sviluppo sulla nostra macchina, creare una tabella di prova per inserire qualche record e verificare il funzionamento dei livelli di consistenza. Alla prossima!

Giampaolo Trapasso

Sono laureato in Informatica e attualmente lavoro come Software Engineer in Radicalbit. Mi diverto a programmare usando Java e Scala, Akka, RxJava e Cassandra. Qui mio modesto contributo su StackOverflow e il mio account su GitHub