Ciao, Habr.
Mi chiamo Misha Butrimov e vorrei raccontarvi un po' di Cassandra. Il mio racconto sarà utile a chi non ha mai avuto a che fare con i database NoSQL, poiché presenta molte peculiarità di implementazione e insidie di cui è importante essere a conoscenza. E se oltre a Oracle o ad un altro database relazionale non avete mai visto altro, queste informazioni vi salveranno la vita.
Cosa ha di buono Cassandra? È un database NoSQL progettato senza un singolo punto di errore, che si scalda bene. Se avete bisogno di aggiungere un paio di terabyte per un database, basta aggiungere nodi al cluster. Volete espanderlo a un altro data center? Aggiungete nodi al cluster. Volete aumentare il RPS elaborato? Aggiungete nodi al cluster. Funziona allo stesso modo anche in direzione opposta.
In cosa è ancora eccellente? Nella capacità di gestire molte richieste. Ma molte — quanto è? 10, 20, 30, 40 mila richieste al secondo — è poco. 100 mila richieste al secondo in scrittura — è poco. Ci sono aziende che hanno dichiarato di gestire 2 milioni di richieste al secondo. A loro bisogna crederci.
In linea di principio, Cassandra ha una grande differenza rispetto ai dati relazionali: non assomiglia affatto a loro. È molto importante tenerlo a mente.
Non tutto ciò che appare uguale funziona allo stesso modo.
Una volta un collega è venuto da me e ha chiesto: «Ecco il linguaggio di query Cassandra SQL, e ci sono le istruzioni di selezione, c'è il where, c'è l'and. Scrivo le lettere e non funziona. Perché?». Se si tratta Cassandra come un database relazionale, questo è il modo perfetto per finire la propria vita in modo crudele e senza speranza. E non sto propagandando, è vietato in Russia. Semplicemente progettate qualcosa nel modo sbagliato.
Ad esempio, un cliente viene da noi e dice: «Costruiamo un database per le serie TV, o un database per un ricettario. Avremo piatti con ingredienti o un elenco di serie e attori». Noi rispondiamo felici: «Facciamo!». Sono solo due byte da trasferire, un paio di tabelle e il gioco è fatto, tutto funzionerà molto velocemente e in modo affidabile. E tutto va bene finché i clienti non arrivano e non dicono che le casalinghe devono anche risolvere il problema inverso: hanno un elenco di ingredienti e vogliono sapere quale piatto vogliono cucinare. Siete spacciati.
Tutto perché Cassandra è un database ibrido: è sia una chiave-valore, sia memorizza i dati in colonne ampie. Parlando il linguaggio di Java o Kotlin, questo potrebbe essere descritto così:
Map<RowKey, SortedMap>
Cioè una mappa, all'interno della quale si trova anche un'altra mappa ordinata. La prima chiave di questa mappa è la Row key o Partition key — la chiave di partizionamento. La seconda chiave, che è la chiave della mappa già ordinata, è la Clustering key.
Per illustrare la distribuzione del database, disegniamo tre nodi. Ora dobbiamo capire come distribuire i dati sui nodi. Perché se mettiamo tutto in uno (che, per la cronaca, possono essere mille, duemila, cinque — quanti desideriamo), non si tratta realmente di distribuzione. Di conseguenza, abbiamo bisogno di una funzione matematica che restituisca un numero. Solo un numero, un intero lungo, che rientri in un certo intervallo. E un nodo risponderà a un intervallo, il secondo a un altro, e così via.

Questo numero viene ottenuto attraverso una funzione hash, applicata a ciò che chiamiamo Partition key. Questa è la colonna specificata nella direttiva Primary key, ed è la colonna che sarà la prima e la principale chiave della mappa. Determina quale nodo ospiterà quali dati. La tabella viene creata in Cassandra con una sintassi quasi identica a quella di SQL:
CREATE TABLE users (
user_id uuid,
name text,
year int,
salary float,
PRIMARY KEY(user_id)
)
La chiave primaria in questo caso consiste di una sola colonna, che è anche la chiave di partizionamento.
Come verranno distribuiti gli utenti? Parte andrà a un nodo, parte a un altro, e parte a un terzo. Risultando in una normale tabella hash, ovvero una mappa, ovvero in Python un dizionario, ovvero una semplice struttura chiave-valore, da cui possiamo leggere tutti i valori e leggere e scrivere per chiave.

Select: quando allow filtering si trasforma in full scan, o come non si dovrebbe fare
Scriviamo una dichiarazione select: select * from users where userid = . Sembra funzionare come in Oracle: scriviamo select, specifichiamo le condizioni e tutto funziona, gli utenti vengono recuperati. Ma se scegliamo, ad esempio, un utente con un anno di nascita specifico, Cassandra si lamenta di non poter eseguire la query. Perché non sa nulla su come sono distribuiti i dati riguardo all'anno di nascita — l'unica colonna specificata come chiave è una sola. Allora dice: «Va bene, posso comunque eseguire questa query. Aggiungi allow filtering». Aggiungiamo la direttiva e tutto funziona. E in quel momento succede qualcosa di terribile.
Quando testiamo con i dati di prova, va tutto magnificamente. Ma quando eseguiamo una query in produzione, dove abbiamo, per esempio, 4 milioni di record, le cose non vanno molto bene. Perché allow filtering è una direttiva che consente a Cassandra di raccogliere tutti i dati da questa tabella da tutti i nodi, tutti di data center (se ce ne sono molti in questo cluster), e solo dopo filtrarli. È come un Full Scan, e nessuno potrebbe esserne entusiasta.
Se ci servissero utenti solo per gli identificatori, saremmo a posto. Ma a volte dobbiamo scrivere altre query e applicare altre restrizioni sulla selezione. Quindi ricordiamo: questo è tutto una mappa, di cui abbiamo una chiave di partizionamento, ma al suo interno c'è una mappa ordinata.
E anche questa ha una chiave che chiamiamo Clustering Key. Questa chiave, che a sua volta è composta da colonne che sceglieremo, permette a Cassandra di capire come i dati saranno fisicamente ordinati e disposti su ogni nodo. Cioè, per un certo Partition key, la Clustering key spiegherà come esattamente immettere i dati in questo albero, quale posto occuperanno lì.
È realmente un albero, in cui viene chiamato un comparatore, a cui forniamo un insieme di colonne sotto forma di oggetto, e anche questo viene impostato sotto forma di elenco di colonne.
CREATE TABLE users_by_year_salary_id (
user_id uuid,
name text,
year int,
salary float,
PRIMARY KEY((year), salary, user_id)
Si prega di notare la direttiva Primary key, il cui primo argomento (in questo caso l'anno) è sempre la Partition key. Può consistere in una o più colonne, non importa. Se ci sono più colonne, è necessario racchiuderle di nuovo tra parentesi, affinché il preprocessore del linguaggio capisca che si tratta di una Primary key, e che tutte le altre colonne sono Clustering key. Queste ultime verranno passate nel comparatore nell'ordine in cui si presentano. Cioè, la prima colonna è più significativa, la seconda è meno significativa e così via. Come facciamo per le classi di dati scrivendo, ad esempio, i campi equals: elenchiamo i campi e indichiamo quali sono più importanti e quali meno. In Cassandra, questo è, per così dire, il campo della data class a cui verrà applicato il metodo equals scritto per essa.
Impostiamo l'ordinamento, imponiamo restrizioni
È importante ricordare che l'ordine di ordinamento (decrescente, crescente, non importa) è stabilito nel momento in cui viene creato il key, e modificarlo in seguito non sarà possibile. Esso determina fisicamente come verranno ordinati i dati e come saranno disposti. Se sarà necessario modificare il Clustering key o l'ordine di ordinamento, sarà necessario creare una nuova tabella e trasferirvi i dati. Non si può fare così con una tabella già esistente.

Abbiamo riempito la nostra tabella con utenti e abbiamo visto che sono stati disposti in cerchio prima per anno di nascita e poi all'interno di ciascun nodo per stipendi e per user ID. Ora possiamo fare delle selezioni, imponendo delle restrizioni.
Ritorna di nuovo il nostro funzionante dove, e, e gli utenti ci vengono assegnati, e tutto torna a posto. Ma se proviamo a utilizzare solo una parte del Clustering key, quella meno significativa, Cassandra immediatamente segnalerà che non può trovare nella nostra mappa il posto dove si trova questo oggetto, il quale ha questi campi per il comparatore null, mentre quest'altro, che abbiamo appena definito, - dove si trova. Dovrò sollevare nuovamente tutti i dati da questo nodo e filtrarli. Questo equivale a una Full Scan all'interno del nodo, e non è una buona cosa.
In qualsiasi situazione poco chiara, crea una nuova tabella
Se vogliamo avere la possibilità di recuperare gli utenti per ID, età o stipendio, cosa fare? Nulla. Basta usare due tabelle. Se dobbiamo recuperare gli utenti in tre modi diversi, ci saranno tre tabelle. I tempi in cui cercavamo di risparmiare spazio su disco sono finiti. Questo è il risorsa più economica. Costa molto meno del tempo di risposta, che può essere devastante per l'utente. È molto più piacevole per l'utente ricevere qualcosa in un secondo piuttosto che in 10 minuti.
Scambiamo lo spazio occupato in eccesso, dati denormalizzati, con la possibilità di scalare bene e funzionare in modo affidabile. Infatti, un cluster composto da tre data center, ognuno con cinque nodi, in grado di mantenere un livello accettabile di integrità dei dati (quando nulla viene perso), può sopportare la completa perdita di un data center. E ne restano altri due con due nodi ciascuno. Solo dopo inizieranno i problemi. Questo è un buon tipo di ridondanza, che costa un paio di SSD o CPU in più. Pertanto, per utilizzare Cassandra, che non è SQL, in cui non ci sono relazioni né chiavi esterne, è necessario conoscere alcune semplici regole.
Progettiamo tutto a partire dalla richiesta. I dati non sono più al centro, ma piuttosto come l'applicazione lavorerà con essi. Se deve recuperare dati diversi in modi diversi o gli stessi dati in modi diversi, dobbiamo organizzarli in modo che siano comodi per l'applicazione. Altrimenti, ci ritroveremo a dover eseguire una scansione completa e Cassandra non ci darà alcun vantaggio.
Denormalizzare i dati è la norma. Dimentichiamo le forme normali, non abbiamo più database relazionali. Se inseriamo qualcosa 100 volte, ci sarà 100 volte. È comunque più economico che rallentare.
Scegliamo le chiavi per il partizionamento in modo che siano distribuite normalmente. Non vogliamo che l'hash delle nostre chiavi cada in un intervallo ristretto. Cioè, l'anno di nascita nell'esempio precedente è un cattivo esempio. Già, è buono se i nostri utenti sono distribuiti normalmente per anno di nascita, ed è cattivo se si parla di studenti di quinta elementare, dove sarà difficile effettuare il partizionamento.
Il tipo di ordinamento viene scelto una sola volta durante la creazione della Clustering Key. Se è necessario modificarlo, sarà necessario riversare la nostra tabella con un'altra chiave.
E la cosa più importante: se abbiamo bisogno di estrarre gli stessi dati in 100 modi diversi, avremo 100 tabelle diverse.
Fonte: habr.com
