NewSQL = NoSQL+ACID

NewSQL = NoSQL+ACID
Fino a poco tempo fa Odnoklassniki archiviava circa 50 TB di dati elaborati in tempo reale in SQL Server. Per un volume di questo tipo, è quasi impossibile fornire un accesso veloce, affidabile e persino con tolleranza ai guasti al data center utilizzando un DBMS SQL. Tipicamente, in questi casi, viene utilizzato uno degli archivi NoSQL, ma non tutto può essere trasferito su NoSQL: alcune entità richiedono garanzie di transazione ACID.

Questo ci ha portato all’utilizzo dello storage NewSQL, ovvero un DBMS che garantisce tolleranza ai guasti, scalabilità e prestazioni dei sistemi NoSQL, ma allo stesso tempo mantenendo le garanzie ACID familiari ai sistemi classici. Esistono pochi sistemi industriali funzionanti di questa nuova classe, quindi abbiamo implementato noi stessi un sistema del genere e lo abbiamo messo in esercizio commerciale.

Come funziona e cosa è successo: leggi sotto il taglio.

Oggi, il pubblico mensile di Odnoklassniki è di oltre 70 milioni di visitatori unici. Noi Siamo tra i primi cinque più grande social network del mondo e tra i venti siti su cui gli utenti trascorrono più tempo. L'infrastruttura OK gestisce carichi molto elevati: più di un milione di richieste HTTP/sec per fronte. Parti di un parco server di oltre 8000 unità si trovano una accanto all'altra, in quattro data center di Mosca, il che consente una latenza di rete inferiore a 1 ms tra di loro.

Utilizziamo Cassandra dal 2010, a partire dalla versione 0.6. Oggi sono attive diverse dozzine di cluster. Il cluster più veloce elabora più di 4 milioni di operazioni al secondo e il più grande memorizza 260 TB.

Tuttavia, questi sono tutti normali cluster NoSQL utilizzati per l'archiviazione debolmente coordinato dati. Volevamo sostituire lo storage principale e coerente, Microsoft SQL Server, utilizzato sin dalla fondazione di Odnoklassniki. Lo spazio di archiviazione era costituito da oltre 300 macchine SQL Server Standard Edition, che contenevano 50 TB di dati: entità aziendali. Questi dati vengono modificati come parte delle transazioni ACID e richiedono elevata consistenza.

Per distribuire i dati tra i nodi di SQL Server, abbiamo utilizzato sia il verticale che l'orizzontale partizionamento (sharding). Storicamente, utilizzavamo un semplice schema di condivisione dei dati: ogni entità era associata a un token, una funzione dell'ID entità. Le entità con lo stesso token sono state inserite nello stesso server SQL. La relazione master-detail è stata implementata in modo che i token dei record principale e secondario corrispondessero sempre e si trovassero sullo stesso server. In un social network, quasi tutti i record vengono generati per conto dell'utente, il che significa che tutti i dati dell'utente all'interno di un sottosistema funzionale vengono archiviati su un server. Cioè, una transazione commerciale coinvolgeva quasi sempre tabelle di un server SQL, il che rendeva possibile garantire la coerenza dei dati utilizzando transazioni ACID locali, senza la necessità di utilizzare lento e inaffidabile transazioni ACID distribuite.

Grazie allo sharding e alla velocizzazione di SQL:

  • Non utilizziamo vincoli di chiave esterna, poiché durante lo sharding l'ID entità potrebbe trovarsi su un altro server.
  • Non utilizziamo procedure memorizzate e trigger a causa del carico aggiuntivo sulla CPU DBMS.
  • Non utilizziamo JOIN a causa di tutto quanto sopra e di molte letture casuali dal disco.
  • Al di fuori di una transazione, utilizziamo il livello di isolamento Read Uncomtched per ridurre i deadlock.
  • Eseguiamo solo transazioni brevi (in media inferiori a 100 ms).
  • Non utilizziamo UPDATE e DELETE su più righe a causa dell'elevato numero di deadlock: aggiorniamo solo un record alla volta.
  • Eseguiamo sempre query solo sugli indici: una query con un piano di scansione completo della tabella per noi significa sovraccaricare il database e provocarne il fallimento.

Questi passaggi ci hanno permesso di ottenere prestazioni quasi massime dai server SQL. Tuttavia i problemi diventarono sempre più numerosi. Diamo un'occhiata a loro.

Problemi con SQL

  • Poiché abbiamo utilizzato lo sharding autoprodotto, l'aggiunta di nuovi shard è stata eseguita manualmente dagli amministratori. Per tutto questo tempo, le repliche di dati scalabili non hanno soddisfatto le richieste.
  • All'aumentare del numero di record nella tabella, la velocità di inserimento e modifica diminuisce; quando si aggiungono indici a una tabella esistente, la velocità diminuisce di un fattore; la creazione e la ricreazione degli indici avviene con tempi di inattività.
  • Avere una piccola quantità di Windows per SQL Server in produzione rende difficile la gestione dell'infrastruttura

Ma il problema principale è

tolleranza ai guasti

Il classico server SQL ha una scarsa tolleranza agli errori. Supponiamo che tu abbia un solo server di database e che si guasti una volta ogni tre anni. Durante questo periodo il sito resta inattivo per 20 minuti, il che è accettabile. Se disponi di 64 server, il sito non è disponibile una volta ogni tre settimane. E se hai 200 server, il sito non funziona ogni settimana. Questo è un problema.

Cosa si può fare per migliorare la tolleranza agli errori di un server SQL? Wikipedia ci invita a costruire cluster altamente disponibile: dove in caso di guasto di uno qualsiasi dei componenti ce n'è uno di backup.

Ciò richiede una flotta di attrezzature costose: numerose duplicazioni, fibra ottica, storage condiviso e l'inclusione di una riserva non funzionano in modo affidabile: circa il 10% delle commutazioni termina con il guasto del nodo di backup come un treno dietro il nodo principale.

Ma lo svantaggio principale di un cluster ad alta disponibilità è la disponibilità zero in caso di guasto del data center in cui si trova. Odnoklassniki ha quattro data center e dobbiamo garantire il funzionamento in caso di guasto completo in uno di essi.

Per questo potremmo usare Multimaster replica incorporata in SQL Server. Questa soluzione è molto più costosa a causa del costo del software e soffre dei noti problemi di replica: ritardi imprevedibili nelle transazioni con la replica sincrona e ritardi nell'applicazione delle repliche (e, di conseguenza, perdita di modifiche) con la replica asincrona. L'implicito risoluzione manuale dei conflitti rende questa opzione completamente inapplicabile per noi.

Tutti questi problemi richiedevano una soluzione radicale e abbiamo iniziato ad analizzarli in dettaglio. Qui dobbiamo conoscere ciò che fa principalmente SQL Server: le transazioni.

Transazione semplice

Consideriamo la transazione più semplice, dal punto di vista di un programmatore SQL applicato: aggiungere una foto ad un album. Gli album e le fotografie sono archiviati in piastre diverse. L'album ha un contatore di foto pubblico. Quindi tale transazione è suddivisa nei seguenti passaggi:

  1. Chiudiamo l'album con la chiave.
  2. Crea una voce nella tabella delle foto.
  3. Se la foto ha uno stato pubblico, aggiungi un contatore di foto pubbliche all'album, aggiorna il record e conferma la transazione.

Oppure in pseudocodice:

TX.start("Albums", id);
Album album = albums.lock(id);
Photo photo = photos.create(…);

if (photo.status == PUBLIC ) {
    album.incPublicPhotosCount();
}
album.update();

TX.commit();

Vediamo che lo scenario più comune per una transazione commerciale è leggere i dati dal database nella memoria del server delle applicazioni, modificare qualcosa e salvare nuovamente i nuovi valori nel database. Di solito in una transazione del genere aggiorniamo diverse entità, diverse tabelle.

Durante l'esecuzione di una transazione, può verificarsi la modifica simultanea degli stessi dati da un altro sistema. Ad esempio, l'Antispam può decidere che l'utente è in qualche modo sospetto e quindi tutte le foto dell'utente non dovrebbero più essere pubbliche, devono essere inviate per moderazione, il che significa cambiare photo.status con un altro valore e disattivare i contatori corrispondenti. Ovviamente, se questa operazione avviene senza garanzie di atomicità di applicazione e di isolamento delle modifiche concorrenti, come in ACIDO, il risultato non sarà quello necessario: il contatore delle foto mostrerà il valore sbagliato oppure non tutte le foto verranno inviate per moderazione.

Molti codici simili, che manipolano varie entità aziendali all'interno di una transazione, sono stati scritti durante l'intera esistenza di Odnoklassniki. Basato sull'esperienza delle migrazioni a NoSQL da Eventuale coerenza Sappiamo che la sfida più grande (e l'investimento di tempo) deriva dallo sviluppo del codice per mantenere la coerenza dei dati. Pertanto, abbiamo considerato che il requisito principale per il nuovo storage sia la fornitura di transazioni ACID reali per la logica dell'applicazione.

Altri requisiti, non meno importanti, erano:

  • Se il data center si guasta, devono essere disponibili sia la lettura che la scrittura nel nuovo storage.
  • Mantenimento dell'attuale velocità di sviluppo. Cioè, quando si lavora con un nuovo repository, la quantità di codice dovrebbe essere approssimativamente la stessa; non dovrebbe essere necessario aggiungere nulla al repository, sviluppare algoritmi per risolvere i conflitti, mantenere indici secondari, ecc.
  • La velocità del nuovo sistema di archiviazione doveva essere piuttosto elevata, sia durante la lettura dei dati che durante l'elaborazione delle transazioni, il che di fatto significava che soluzioni accademicamente rigorose, universali, ma lente, come ad esempio, non erano applicabili commit a due fasi.
  • Ridimensionamento automatico al volo.
  • Utilizzando normali server economici, senza la necessità di acquistare hardware esotico.
  • Possibilità di sviluppo dello storage da parte degli sviluppatori dell'azienda. In altre parole, è stata data priorità a soluzioni proprietarie o open source, preferibilmente in Java.

Decisioni decisioni

Analizzando le possibili soluzioni siamo arrivati ​​a due possibili scelte architetturali:

Il primo è prendere qualsiasi server SQL e implementare la tolleranza agli errori, il meccanismo di dimensionamento, il cluster di failover, la risoluzione dei conflitti e le transazioni ACID distribuite, affidabili e veloci richieste. Abbiamo valutato questa opzione come molto non banale e ad alta intensità di lavoro.

La seconda opzione è prendere uno spazio di archiviazione NoSQL già pronto con scalabilità implementata, un cluster di failover, risoluzione dei conflitti e implementare transazioni e SQL da soli. A prima vista, anche il compito di implementare SQL, per non parlare delle transazioni ACID, sembra un compito che richiederà anni. Ma poi ci siamo resi conto che il set di funzionalità SQL che utilizziamo nella pratica è tanto lontano da ANSI SQL CQL Cassandra lontano da ANSI SQL. Dando uno sguardo ancora più attento a CQL, ci siamo resi conto che era abbastanza vicino a ciò di cui avevamo bisogno.

Cassandra e CQL

Quindi, cosa è interessante in Cassandra, quali capacità ha?

Innanzitutto, qui puoi creare tabelle che supportano vari tipi di dati; puoi eseguire SELECT o UPDATE sulla chiave primaria.

CREATE TABLE photos (id bigint KEY, owner bigint,…);
SELECT * FROM photos WHERE id=?;
UPDATE photos SET … WHERE id=?;

Per garantire la coerenza dei dati della replica, Cassandra utilizza approccio del quorum. Nel caso più semplice, ciò significa che quando tre repliche della stessa riga vengono posizionate su nodi diversi del cluster, la scrittura viene considerata riuscita se la maggior parte dei nodi (ovvero due su tre) ha confermato il successo di questa operazione di scrittura . I dati della riga sono considerati coerenti se, durante la lettura, la maggior parte dei nodi è stata interrogata e li ha confermati. Pertanto, con tre repliche, è garantita la coerenza completa e istantanea dei dati in caso di guasto di un nodo. Questo approccio ci ha permesso di implementare uno schema ancora più affidabile: inviare sempre le richieste a tutte e tre le repliche, aspettando una risposta dalle due più veloci. In questo caso la risposta tardiva della terza replica viene scartata. Un nodo che tarda a rispondere può avere seri problemi: freni, garbage collection nella JVM, recupero diretto della memoria nel kernel Linux, guasto hardware, disconnessione dalla rete. Tuttavia, ciò non influisce in alcun modo sulle operazioni o sui dati del cliente.

Viene chiamato l'approccio in cui contattiamo tre nodi e riceviamo una risposta da due speculazione: viene inviata una richiesta di repliche extra ancor prima che “cada”.

Un altro vantaggio di Cassandra è Batchlog, un meccanismo che garantisce che un batch di modifiche apportate venga applicato completamente o non applicato affatto. Questo ci permette di risolvere A in ACIDO - atomicità fuori dagli schemi.

La cosa più vicina alle transazioni in Cassandra sono le cosiddette “transazioni leggere". Ma sono ben lontane dalle “vere” transazioni ACID: questa, infatti, è un’opportunità per farlo CAS sui dati di un solo record, utilizzando il consenso utilizzando il protocollo Paxos pesante. Pertanto, la velocità di tali transazioni è bassa.

Quello che ci mancava in Cassandra

Quindi, abbiamo dovuto implementare transazioni ACID reali in Cassandra. Usando il quale potremmo facilmente implementare altre due utili funzionalità del classico DBMS: indici veloci e coerenti, che ci permetterebbero di eseguire selezioni di dati non solo tramite la chiave primaria, e un generatore regolare di ID monotonici autoincrementanti.

Cono

Così è nato un nuovo DBMS Cono, costituito da tre tipologie di nodi server:

  • Archiviazione: server Cassandra (quasi) standard responsabili dell'archiviazione dei dati sui dischi locali. Man mano che il carico e il volume dei dati crescono, la loro quantità può essere facilmente scalata fino a decine e centinaia.
  • Coordinatori delle transazioni: assicurano l'esecuzione delle transazioni.
  • I client sono server applicativi che implementano operazioni aziendali e avviano transazioni. Possono esserci migliaia di clienti di questo tipo.

NewSQL = NoSQL+ACID

I server di tutti i tipi fanno parte di un cluster comune, utilizzano il protocollo di messaggi interno Cassandra per comunicare tra loro e pettegolezzo per lo scambio di informazioni sui cluster. Con Heartbeat, i server apprendono i guasti reciproci, mantengono un unico schema di dati: tabelle, loro struttura e replica; schema di partizionamento, topologia del cluster, ecc.

Clienti

NewSQL = NoSQL+ACID

Invece dei driver standard, viene utilizzata la modalità Fat Client. Tale nodo non memorizza dati, ma può fungere da coordinatore per l'esecuzione delle richieste, ovvero il Cliente stesso funge da coordinatore delle sue richieste: interroga le repliche di archiviazione e risolve i conflitti. Questo non solo è più affidabile e veloce del driver standard, che richiede la comunicazione con un coordinatore remoto, ma consente anche di controllare la trasmissione delle richieste. Al di fuori di una transazione aperta sul client, le richieste vengono inviate ai repository. Se il cliente ha aperto una transazione, tutte le richieste all'interno della transazione vengono inviate al coordinatore della transazione.
NewSQL = NoSQL+ACID

C*Un coordinatore delle transazioni

Il coordinatore è qualcosa che abbiamo implementato per C*One da zero. È responsabile della gestione delle transazioni, dei blocchi e dell'ordine in cui vengono applicate le transazioni.

Per ogni transazione servita, il coordinatore genera un timestamp: ogni transazione successiva è maggiore della transazione precedente. Poiché il sistema di risoluzione dei conflitti di Cassandra si basa sui timestamp (di due record in conflitto, quello con l'ultimo timestamp è considerato attuale), il conflitto verrà sempre risolto a favore della transazione successiva. Così abbiamo implementato Orologio Lamport - un modo economico per risolvere i conflitti in un sistema distribuito.

blocco

Per garantire l'isolamento, abbiamo deciso di utilizzare il metodo più semplice: blocchi pessimistici basati sulla chiave primaria del record. In altre parole, in una transazione, un record deve prima essere bloccato, e solo dopo letto, modificato e salvato. Solo dopo un commit riuscito è possibile sbloccare un record in modo che le transazioni concorrenti possano utilizzarlo.

L'implementazione di tale blocco è semplice in un ambiente non distribuito. In un sistema distribuito, ci sono due opzioni principali: implementare il blocco distribuito sul cluster o distribuire le transazioni in modo che le transazioni che coinvolgono lo stesso record siano sempre gestite dallo stesso coordinatore.

Poiché nel nostro caso i dati sono già distribuiti tra gruppi di transazioni locali in SQL, si è deciso di assegnare i gruppi di transazioni locali ai coordinatori: un coordinatore esegue tutte le transazioni con token da 0 a 9, il secondo - con token da 10 a 19, e così via. Di conseguenza, ciascuna istanza del coordinatore diventa il master del gruppo di transazioni.

Quindi i blocchi possono essere implementati sotto forma di una banale HashMap nella memoria del coordinatore.

Fallimenti del coordinatore

Poiché un coordinatore serve esclusivamente un gruppo di transazioni, è molto importante determinare rapidamente il fatto del suo fallimento in modo che il secondo tentativo di eseguire la transazione scada. Per renderlo veloce e affidabile, abbiamo utilizzato un protocollo di heartbeat del quorum completamente connesso:

Ogni data center ospita almeno due nodi coordinatori. Periodicamente, ogni coordinatore invia un messaggio heartbeat agli altri coordinatori e li informa sul suo funzionamento, nonché su quali messaggi heartbeat ha ricevuto da quali coordinatori nel cluster l'ultima volta.

NewSQL = NoSQL+ACID

Ricevendo informazioni simili da altri come parte dei loro messaggi heartbeat, ciascun coordinatore decide autonomamente quali nodi del cluster funzionano e quali no, guidato dal principio del quorum: se il nodo X ha ricevuto informazioni dalla maggior parte dei nodi del cluster sulla normale ricezione di messaggi dal nodo Y, quindi Y funziona. E viceversa, non appena la maggioranza segnala la mancanza di messaggi dal nodo Y, allora Y ha rifiutato. È curioso che se il quorum informa il nodo X che non riceve più messaggi da esso, allora il nodo X stesso si considererà fallito.

I messaggi heartbeat vengono inviati ad alta frequenza, circa 20 volte al secondo, con un periodo di 50 ms. In Java è difficile garantire una risposta dell'applicazione entro 50 ms a causa della lunghezza comparabile delle pause causate dal Garbage Collector. Siamo stati in grado di ottenere questo tempo di risposta utilizzando il garbage collector G1, che ci consente di specificare un obiettivo per la durata delle pause GC. Tuttavia, a volte, molto raramente, le pause del collettore superano i 50 ms, il che può portare ad un falso rilevamento del guasto. Per evitare che ciò accada, il coordinatore non segnala un guasto di un nodo remoto quando scompare il primo messaggio heartbeat da esso inviato, ma solo se diversi sono scomparsi di seguito. In questo modo siamo riusciti a rilevare un guasto del nodo coordinatore in 200 SM.

Ma non basta capire subito quale nodo ha smesso di funzionare. Dobbiamo fare qualcosa al riguardo.

Prenotazione

Lo schema classico prevede, in caso di fallimento del master, di avviare una nuova elezione utilizzando uno dei alla moda universale algoritmi. Tuttavia, tali algoritmi presentano problemi ben noti legati alla convergenza temporale e alla durata del processo elettorale stesso. Siamo stati in grado di evitare tali ulteriori ritardi utilizzando uno schema di sostituzione del coordinatore in una rete completamente connessa:

NewSQL = NoSQL+ACID

Diciamo che vogliamo eseguire una transazione nel gruppo 50. Determiniamo in anticipo lo schema di sostituzione, cioè quali nodi eseguiranno le transazioni nel gruppo 50 in caso di guasto del coordinatore principale. Il nostro obiettivo è mantenere la funzionalità del sistema in caso di guasto del data center. Determiniamo che la prima riserva sarà un nodo di un altro data center e la seconda riserva sarà un nodo di un terzo. Questo schema viene selezionato una volta e non cambia finché non cambia la topologia del cluster, cioè finché non vi entrano nuovi nodi (cosa che accade molto raramente). La procedura per selezionare un nuovo master attivo in caso di guasto del vecchio sarà sempre la seguente: la prima riserva diventerà il master attivo, e se ha smesso di funzionare, la seconda riserva diventerà il master attivo.

Questo schema è più affidabile dell'algoritmo universale, poiché per attivare un nuovo master è sufficiente determinare il fallimento di quello vecchio.

Ma come faranno i clienti a capire quale master sta funzionando adesso? È impossibile inviare informazioni a migliaia di clienti in 50 ms. È possibile una situazione in cui un client invia una richiesta per aprire una transazione, non sapendo ancora che questo master non funziona più, e la richiesta scade. Per evitare che ciò accada, i clienti inviano speculativamente una richiesta per aprire una transazione al master del gruppo e ad entrambe le sue riserve contemporaneamente, ma solo colui che è il master attivo in questo momento risponderà a questa richiesta. Il cliente effettuerà tutte le comunicazioni successive all'interno della transazione solo con il master attivo.

I master di backup inseriscono le richieste ricevute per transazioni che non sono loro nella coda delle transazioni non ancora nate, dove vengono archiviate per un certo periodo. Se il master attivo muore, il nuovo master elabora le richieste di apertura delle transazioni dalla sua coda e risponde al client. Se il cliente ha già aperto una transazione con il vecchio master, la seconda risposta verrà ignorata (e, ovviamente, tale transazione non verrà completata e verrà ripetuta dal cliente).

Come funziona la transazione

Supponiamo che un cliente abbia inviato una richiesta al coordinatore per aprire una transazione per questa o quella entità con questa o quella chiave primaria. Il coordinatore blocca questa entità e la inserisce nella tabella dei blocchi in memoria. Se necessario, il coordinatore legge questa entità dalla memoria e memorizza i dati risultanti in uno stato di transazione nella memoria del coordinatore.

NewSQL = NoSQL+ACID

Quando un client desidera modificare i dati in una transazione, invia una richiesta al coordinatore per modificare l'entità e il coordinatore inserisce in memoria i nuovi dati nella tabella dello stato della transazione. Ciò completa la registrazione: non viene effettuata alcuna registrazione sulla memoria.

NewSQL = NoSQL+ACID

Quando un cliente richiede i propri dati modificati come parte di una transazione attiva, il coordinatore agisce come segue:

  • se l'ID è già nella transazione, allora i dati vengono presi dalla memoria;
  • se non è presente alcun ID in memoria, allora i dati mancanti vengono letti dai nodi di storage, combinati con quelli già in memoria, e il risultato viene fornito al client.

Pertanto, il client può leggere le proprie modifiche, ma gli altri client non vedono queste modifiche, perché sono archiviate solo nella memoria del coordinatore; non sono ancora nei nodi Cassandra.

NewSQL = NoSQL+ACID

Quando il client invia il commit, lo stato che era nella memoria del servizio viene salvato dal coordinatore in un batch registrato e inviato come batch registrato allo spazio di archiviazione Cassandra. I negozi fanno tutto il necessario per garantire che questo pacchetto venga applicato atomicamente (completamente) e restituiscono una risposta al coordinatore, che sblocca i blocchi e conferma al cliente il successo della transazione.

NewSQL = NoSQL+ACID

E per eseguire il rollback, il coordinatore deve solo liberare la memoria occupata dallo stato della transazione.

Come risultato dei miglioramenti di cui sopra, abbiamo implementato i principi ACID:

  • Atomicita. Questa è una garanzia che nessuna transazione verrà registrata parzialmente nel sistema; o tutte le sue sottooperazioni saranno completate, o nessuna sarà completata. Aderiamo a questo principio attraverso il batch registrato in Cassandra.
  • Consistenza. Ogni transazione andata a buon fine, per definizione, registra solo risultati validi. Se, dopo aver aperto una transazione ed eseguito parte delle operazioni, si scopre che il risultato non è valido, viene eseguito un rollback.
  • Isolamento. Quando una transazione viene eseguita, le transazioni simultanee non dovrebbero influenzarne l'esito. Le transazioni concorrenti vengono isolate utilizzando blocchi pessimistici sul coordinatore. Per le letture esterne a una transazione, il principio di isolamento viene osservato al livello Read Committed.
  • Stabilità. Indipendentemente dai problemi ai livelli inferiori (blackout del sistema, guasti hardware), le modifiche apportate da una transazione completata con successo dovrebbero rimanere preservate quando le operazioni riprendono.

Lettura per indici

Prendiamo una semplice tabella:

CREATE TABLE photos (
id bigint primary key,
owner bigint,
modified timestamp,
…)

Ha un ID (chiave primaria), proprietario e data di modifica. Devi fare una richiesta molto semplice: seleziona i dati del proprietario con la data di modifica "per l'ultimo giorno".

SELECT *
WHERE owner=?
AND modified>?

Affinché tale query possa essere elaborata rapidamente, in un classico DBMS SQL è necessario creare un indice per colonne (proprietario, modificato). Possiamo farlo abbastanza facilmente, poiché ora abbiamo le garanzie ACID!

Indici in C*One

È presente una tabella di origine con fotografie in cui l'ID del record è la chiave primaria.

NewSQL = NoSQL+ACID

Per un indice, C*One crea una nuova tabella che è una copia dell'originale. La chiave è la stessa dell'espressione dell'indice e include anche la chiave primaria del record della tabella di origine:

NewSQL = NoSQL+ACID

Ora la query per "proprietario dell'ultimo giorno" può essere riscritta come selezione da un'altra tabella:

SELECT * FROM i1_test
WHERE owner=?
AND modified>?

La coerenza dei dati nelle foto della tabella sorgente e nella tabella indice i1 viene mantenuta automaticamente dal coordinatore. Basandosi solo sullo schema dei dati, quando viene ricevuta una modifica, il coordinatore genera e memorizza una modifica non solo nella tabella principale, ma anche in copie. Non vengono eseguite azioni aggiuntive sulla tabella degli indici, i log non vengono letti e non vengono utilizzati blocchi. Ciò significa che l'aggiunta di indici non consuma quasi nessuna risorsa e non ha praticamente alcun effetto sulla velocità di applicazione delle modifiche.

Utilizzando ACID, siamo stati in grado di implementare indici simili a SQL. Sono coerenti, scalabili, veloci, componibili e integrati nel linguaggio di query CQL. Non sono necessarie modifiche al codice dell'applicazione per supportare gli indici. Tutto è semplice come in SQL. E, cosa più importante, gli indici non influiscono sulla velocità di esecuzione delle modifiche alla tabella delle transazioni originali.

Quello che è successo

Abbiamo sviluppato C*One tre anni fa e lo abbiamo lanciato in operazioni commerciali.

Cosa abbiamo ottenuto alla fine? Valutiamolo utilizzando l'esempio del sottosistema di elaborazione e archiviazione delle foto, uno dei tipi di dati più importanti in un social network. Non stiamo parlando dei corpi delle fotografie in sé, ma di tutti i tipi di metainformazioni. Ora Odnoklassniki ha circa 20 miliardi di tali record, il sistema elabora 80mila richieste di lettura al secondo, fino a 8mila transazioni ACID al secondo associate alla modifica dei dati.

Quando abbiamo utilizzato SQL con fattore di replica = 1 (ma in RAID 10), le metainformazioni sulle foto sono state archiviate su un cluster ad alta disponibilità di 32 macchine che eseguono Microsoft SQL Server (più 11 backup). Sono stati inoltre allocati 10 server per l'archiviazione dei backup. Un totale di 50 auto costose. Allo stesso tempo, il sistema funzionava al carico nominale, senza riserva.

Dopo la migrazione al nuovo sistema, abbiamo ricevuto un fattore di replica = 3: una copia in ciascun data center. Il sistema è composto da 63 nodi di storage Cassandra e 6 macchine coordinatrici, per un totale di 69 server. Ma queste macchine sono molto più economiche, il loro costo totale è circa il 30% del costo di un sistema SQL. Allo stesso tempo, il carico viene mantenuto al 30%.

Con l'introduzione di C*One è diminuita anche la latenza: in SQL un'operazione di scrittura durava circa 4,5 ms. In C*One - circa 1,6 ms. La durata della transazione è mediamente inferiore a 40 ms, il commit viene completato in 2 ms, la durata di lettura e scrittura è mediamente di 2 ms. 99esimo percentile - solo 3-3,1 ms, il numero di timeout è diminuito di 100 volte, tutto a causa dell'uso diffuso della speculazione.

Ormai la maggior parte dei nodi SQL Server sono stati dismessi; i nuovi prodotti vengono sviluppati solo utilizzando C*One. Abbiamo adattato C*One affinché funzioni nel nostro cloud una nuvola, che ha permesso di accelerare l'implementazione di nuovi cluster, semplificare la configurazione e automatizzare il funzionamento. Senza il codice sorgente, farlo sarebbe molto più difficile e macchinoso.

Ora stiamo lavorando per trasferire le nostre altre strutture di storage nel cloud, ma questa è una storia completamente diversa.

Fonte: habr.com

Aggiungi un commento