Il modo sbagliato per scalare il DB di Cassandra quando gli indici secondari sono a posto
Cassandra è il mio database preferito (non gestito) per molte ragioni: non ha un singolo punto di errore (SPoF), supporta multi-regione, ottimo per operazioni di lettura e scrittura, flessibile sui livelli di coerenza di lettura e scrittura, scala linearmente e non troppo complesso da gestire
Come ogni database, dovresti usare Cassandra in base ai tuoi modelli di accesso ai dati, quindi se hai bisogno di un database flessibile per query ad hoc o abbastanza adattabile per modifiche costanti del modello del database, dovresti prendere in considerazione altre opzioni.
Cassandra è un DB orientato alle colonne ed è davvero potente quando hai già definito le tue query di dati. Datastax, la società che supporta Cassandra, consiglia di iniziare a progettare le query e quindi il modello di dati in Cassandra. Nonostante il fatto della tua struttura colonnare, Cassandra supporta molte strutture dati come tipo di colonna, come le Mappe.
Cassandra è un database di chiavi primarie, il che significa che i dati sono persistenti e organizzati attorno a un cluster in base al valore hash (la chiave di partizione) della chiave primaria. Per le tabelle che hanno più di un PK, Cassandra considera solo la prima parte del PK come chiave di partizione. Scopri di più sulle chiavi composite qui.
Per essere più chiari, torniamo a una delle caratteristiche più importanti di un Cassandra DB: è l’architettura e il fatto che non ha un SPoF.
Un cluster Cassandra è composto da nodi (3 o più) e questi nodi insieme compongono un anello di nodi:
Ogni nodo di Cassandra cluster funziona “in modo indipendente”, ma diversi nodi potrebbero memorizzare i dati stessi, di conseguenza, il fattore di replica (RF) configurazione configurato per il cluster.
Per sapere dove (quale nodo) i tuoi dati sono persistiti, Cassandra utilizza il valore hash (token) calcolato tramite una funzione hash coerente utilizzando la colonna PK di una determinata tabella.
Quando si esegue una query, il nodo coordinatore (normalmente la più vicina delle istanze dell’applicazione) cercherà quali nodi in un anello hanno i dati, in questo modo se un nodo è inattivo per qualche motivo, un altro nodo potrebbe servire i dati (RF ≥2). Questa è la magia di un approccio masterless, in cui ogni nodo in un anello è uguale in termini di lettura e scrittura.
Questo concetto su PK e il fattore di replica è molto importante per capire come scalare il cluster Cassandra quando l’applicazione è in condizioni di carico elevato.
Indici secondari
Cassandra ha anche il concetto di indici secondari. Nei database relazionali, potresti avere molti indici in una determinata tabella, il costo di avere un indice secondario è associato alle operazioni di scrittura, non alle operazioni di lettura. In Cassandra questo non è vero.
Gli indici secondari in Cassandra potrebbero essere utili e allettanti quando il tuo modello di dati è cambiato e devi eseguire una query in base a una nuova colonna.
In che modo, con un indice secondario, è possibile eseguire una query del tipo che:
SELECT * FROM my_table DOVE SECONDARY_INDEX = ‘valore’;
Il problema sull’utilizzo di un Indice Secondario
Immaginate lo scenario: siete in un Blackfriday/CyberMonday e il tuo Cassandra cluster soffre di picco di eventi e avete bisogno di aggiungere più nodi di scalare il database, bilanciamento meglio il traffico e… sopravvivere. Bene, vero?
Normalmente, è una situazione normale in un’applicazione altamente scalabile. Ma che dire se la tua applicazione esegue query utilizzando un indice secondario?
Sì, hai capito.
Ricordate quando ho detto che Cassandra distribuisce i dati in un anello utilizzando la chiave di partizione? Questo sta già accadendo, ma il problema è quando si introduce un indice secondario nella query. Gli indici secondari NON fanno PARTE di una chiave di partizione e Cassandra sa dove vivono i tuoi dati attraverso la chiave di partizione. Quando esegui una query che utilizza questo tipo di indice, ciò che fa Cassandra è cercare ogni nodo nel tuo anello cercando di soddisfare la tua query.
Scenario reale
Durante un Blackfriday, le nostre applicazioni erano con carichi elevati. Molti e molti clienti che vogliono beneficiare degli enormi sconti forniti da un evento Blackfriday.
Abbiamo dato un’occhiata al nostro APM e tutte le analisi ci hanno portato alla nostra persistenza, in questo caso un DB Cassandra. Abbiamo avuto lunghi periodi di latenza, ma non per ogni richiesta, solo per alcuni.
Cercando di riportare le cose allo stato normale, la nostra prima manovra è stata quella di aggiungere più nodi al nostro cluster Cassandra.
Abbiamo aggiunto e stiamo ancora soffrendo di problemi di latenza. La domanda era: perché sta ancora accadendo?
Ci sbagliavamo. È stata una conclusione semplicistica e non ci siamo presi cura di un dettaglio molto importante: questo comportamento stava accadendo non in tutte le richieste, ma in alcune di esse.
Se hai pensato all’indice secondario, bingo! Questo era esattamente il problema.
L’aggiunta di nodi non risolverebbe mai il problema, perché il problema non era correlato a tutte le query che arrivavano nel database, il problema era in alcuni e quelli erano quelli reali che degradavano le prestazioni del database. Era totalmente una cosa di Pareto.
Dettagliare il problema e come mitigarlo
In un momento prima dell’evento Blackfriday, avevamo bisogno di cambiare il nostro modello di dati. Abbiamo regionalizzato la nostra applicazione e la regione del cliente ha iniziato a essere una cosa importante per noi, avevamo bisogno di interrogare i dati in base a un prodotto O una regione.
Guardando indietro e collegando i punti, potremmo renderci conto che eravamo molto preziosi per l’implementazione poiché volevamo riflettere questo nuovo comportamento non solo nel livello API (new query param), ma anche nel modo in cui abbiamo avuto accesso ai dati in Cassandra.
E perché eravamo così preziosi? Perché anche considerando che il nostro tempo di query non è aumentato così tanto, abbiamo fatto il cambiamento.
Tale implementazione non solo ha aumentato il tempo di query utilizzando un indice secondario, ma ha anche generato più problemi in base alla scalabilità dell’infrastruttura di Cassandra. Quando abbiamo aggiunto più nodi nel nostro cluster, significava più nodi da cercare per trovare i dati, quindi il problema stava aumentando esponenzialmente.
Per mitigare il problema, quello che abbiamo fatto è stato recuperare il numero di nodi che avevamo in precedenza e aumentato il fattore di replica per la maggior parte dei nostri nodi nel cluster.
Abbiamo anche cambiato il nostro livello di coerenza di lettura per essere meno coerente. Stavamo usando * QUORUM e invece abbiamo cambiato in uno. Questo ci ha aiutato ad abbassare il carico nei nodi.
Mentre bloccavamo le nostre applicazioni giorni prima dell’evento, sapevamo che non avevamo nuovi dati (operazioni di scrittura) e che i dati sarebbero stati coerenti nel loro stato attuale.
The days after e la soluzione del modello DB
Come parte della soluzione finale, avevamo bisogno di (ri)pensare al nostro modello di database e ripristinare le modifiche che abbiamo fatto come percorso di mitigazione durante l’evento.
Prima dell’evento stavamo usando l’ID prodotto (PID)come chiave di partizione, che è stata una buona decisione, dal momento che il PID ha buoni attributi per essere un PK a causa della sua natura di essere un numero sequenziale (alta cardinalità), e in questo modo diffondere i dati in modo uniforme intorno al cluster.
Informazioni sul nuovo campo “regione”, sfruttiamo il tipo di dati delle raccolte Cassandra e utilizziamo una mappa per ogni regione come colonna nella nostra tabella prodotti.
Gli indici secondari sono sempre una cattiva idea?
La risposta breve è no.
Spiegando un po ‘ meglio, ci sono due tipi di indici in Cassandra: indici locali e globali.
Un indice locale come dice il nome è un tipo di indice che esiste solo localmente, cioè in un nodo. Quando si crea un indice secondario, Cassandra crea una nuova tabella (nascosta) in cui il secondario diventa una chiave primaria in questa tabella. La visibilità di questa nuova tabella è in termini di un nodo, non di un anello (cluster). Questo è il caso degli indici secondari.
D’altra parte, un indice globale ha visibilità ad anello attraverso la sua chiave di partizione, quindi Cassandra sa dove si trovano i dati in un anello attraverso quella chiave di partizione.
Gli indici secondari potrebbero essere un’alternativa, quando si hanno in una query sia: indici primari che secondari. In tal caso, Cassandra sa dove risiedono i dati (quale nodo) attraverso la chiave della partizione e quindi cerca la tabella locale nel nodo che fa riferimento agli indici secondari (locali).
Ci sono anche alcune altre sfumature sugli indici secondari che sono molto ben spiegate qui, ma la migliore pratica è evitarle denormalizzando il modello di dati.