NewSQL = NoSQL + ACID

NewSQL = NoSQL + ACID
Fino a poco tempo fa, circa 50 TB di dati trattati in tempo reale erano conservati in SQL Server su Odnoklassniki. Per gestire un volume simile, garantire un accesso veloce, affidabile e anche resistente ai guasti a un centro dati utilizzando un database SQL è praticamente impossibile. Di solito, in questi casi, si utilizza uno dei database NoSQL, ma non tutto può essere trasferito in NoSQL: alcune entità richiedono garanzie di transazioni ACID.

Questo ci ha portato all'uso di un database NewSQL, ovvero un sistema di gestione di database che offre resilienza, scalabilità e velocità simili a quelle dei sistemi NoSQL, ma preserva le garanzie ACID tipiche dei sistemi classici. Ci sono pochi sistemi industriali funzionanti di questa nuova classe, quindi abbiamo realizzato un tale sistema noi stessi e l'abbiamo messo in produzione.

Come funziona e quali risultati abbiamo ottenuto — leggi di seguito.

Oggi, l'audience mensile di Odnoklassniki supera i 70 milioni di visitatori unici. Noi siamo tra i primi cinque una delle più grandi reti sociali al mondo e tra i primi venti siti dove gli utenti trascorrono più tempo. L'infrastruttura di 'OK' gestisce carichi molto elevati: oltre un milione di richieste HTTP al secondo. Le parti del parco server, che superano le 8000 unità, sono posizionate vicine l'una all'altra — in quattro data center a Mosca, permettendo di garantire una latenza di rete inferiore a 1 ms tra di loro.

Utilizziamo Cassandra dal 2010, a partire dalla versione 0.6. Oggi sono in funzione diverse decine di cluster. Il cluster più veloce gestisce oltre 4 milioni di operazioni al secondo, mentre il più grande conserva 260 TB di dati.

Tuttavia, tutti questi sono cluster NoSQL comuni, utilizzati per lo storage di dati debilmente consistenti. Volevamo invece sostituire il principale storage consistente, Microsoft SQL Server, utilizzato sin dalla fondazione di 'Odnoklassniki'. Questo storage era composto da oltre 300 macchine SQL Server Standard Edition, con 50 TB di dati — entità aziendali. Questi dati vengono modificati nell'ambito di transazioni ACID e richiedono alta coerenza.

Per distribuire i dati tra i nodi SQL Server, abbiamo utilizzato sia la partizione verticale che quella orizzontale. partizionamento (sharding). Storicamente, abbiamo utilizzato uno schema semplice di sharding dei dati: ciascuna entità veniva associata a un token, una funzione dell'ID dell'entità. Le entità con lo stesso token venivano collocate su un singolo SQL Server. La relazione di tipo master-detail era implementata in modo che i token del record principale e di quello derivato coincidessero sempre e si trovassero sullo stesso server. Nella rete sociale, quasi tutte le registrazioni vengono create a nome dell'utente, il che significa che tutti i dati dell'utente all'interno di un sotto-sistema funzionale sono memorizzati su un unico server. Ciò implica che nella transazione aziendale quasi sempre erano coinvolte tabelle di un solo SQL Server, consentendo di garantire la coerenza dei dati tramite transazioni ACID locali, senza necessità di utilizzare transazioni ACID distribuite , lenti e inaffidabili.

Grazie allo sharding e per ottimizzare le prestazioni di SQL:

  • Non utilizziamo i vincoli di chiave esterna, poiché durante lo sharding l'ID dell'entità può trovarsi su un altro server.
  • Non utilizziamo le stored procedure e i trigger a causa del carico aggiuntivo sulla CPU del DBMS.
  • Non utilizziamo i JOIN a causa di quanto sopra e per il gran numero di letture casuali dal disco.
  • Al di fuori delle transazioni, per ridurre i deadlock, utilizziamo il livello di isolamento Read Uncommitted.
  • Eseguiamo solo transazioni brevi (in media più brevi di 100 ms).
  • Non utilizziamo UPDATE e DELETE multipli a causa del gran numero di deadlock: aggiorniamo solo un record alla volta.
  • Eseguiamo sempre le query solo sugli indici: una query con un piano di scansione completa della tabella per noi significa un sovraccarico del database e il suo fallimento.

Questi passaggi ci hanno permesso di estrarre quasi il massimo delle prestazioni dai server SQL. Tuttavia, i problemi stavano diventando sempre più numerosi. Esaminiamoli.

Problemi con SQL

  • Poiché utilizzavamo uno sharding personalizzato, l'aggiunta di nuovi shard veniva eseguita manualmente dagli amministratori. Per tutto questo tempo, le repliche di dati scalabili non gestivano le query.
  • Con l'aumento del numero di record nella tabella, la velocità di inserimento e modifica diminuisce; aggiungendo indici a una tabella esistente, la velocità diminuisce drasticamente, e la creazione e ricreazione degli indici avviene con un downtime.
  • La presenza di un numero limitato di Windows per SQL Server in produzione complica la gestione dell'infrastruttura.

Ma il problema principale è —

Affidabilità

Un classico SQL Server ha una scarsa affidabilità. Supponiamo che tu abbia solo un server di database e si guasti ogni tre anni. Durante quel periodo, il sito non funziona per 20 minuti, il che è accettabile. Se hai 64 server, il sito non funziona ogni tre settimane. E se hai 200 server, il sito non funziona ogni settimana. Questo è un problema.

Cosa si può fare per migliorare l'affidabilità del SQL Server? Wikipedia ci suggerisce di costruire un cluster ad alta disponibilità: dove, in caso di guasto di uno qualsiasi dei componenti, c'è una duplicazione.

Questo richiede un parco di costosa attrezzatura: molteplici ridondanze, fibra ottica, storage condiviso, e anche l'inclusione di riserve funziona in modo inaffidabile: circa il 10% delle attivazioni termina con il fallimento del nodo di riserva trainato dal nodo principale.

Ma il principale svantaggio di un cluster così altamente disponibile è la totale indisponibilità in caso di guasto del data center in cui è collocato. "Odnoklassniki" ha quattro data center, e dobbiamo garantire il funzionamento anche in caso di un'interruzione totale in uno di essi.

Per questo si potrebbe utilizzare Multi-Master replication, integrata in SQL Server. Questa soluzione è molto più costosa a causa del costo del software e presenta i ben noti problemi con la replicazione: ritardi imprevedibili delle transazioni nella replicazione sincrona e ritardi nell'applicazione delle replicazioni (e, di conseguenza, modifiche perse) nella replicazione asincrona. Ciò implicando la 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 familiarizzare con ciò che fa principalmente SQL Server: le transazioni.

Transazione semplice

Consideriamo una transazione molto semplice, dal punto di vista di un programmatore SQL applicativo: l'aggiunta di una foto in un album. Gli album e le foto sono memorizzati in tabelle diverse. Ogni album ha un contatore per le foto pubbliche. Quindi, questa transazione si suddivide nei seguenti passaggi:

  1. Blocchiamo l'album tramite la chiave.
  2. Creiamo una voce nella tabella delle foto.
  3. Se la foto ha lo stato pubblico, incrementiamo il contatore delle foto pubbliche nell'album, aggiorniamo la voce e comit diamo la transazione.

O in forma di 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 di una transazione aziendale è leggere i dati dal database in memoria del server applicativo, modificare qualcosa e salvare i nuovi valori di nuovo nel database. Di solito, in una tale transazione, aggiorniamo più entità, ovvero più tabelle.

Durante l'esecuzione di una transazione, potrebbe avvenire una modifica competitiva dei medesimi dati da un altro sistema. Ad esempio, il sistema anti-spam potrebbe decidere che un utente è sospetto e, quindi, tutte le foto dell'utente non dovrebbero più essere pubbliche, necessitando di essere inviate per la moderazione, il che implica cambiare photo.status in un altro valore e ripristinare i contatori corrispondenti. È evidente che se questa operazione avviene senza garanzie di atomicità e isolamento delle modifiche concorrenti, come in ACID, il risultato non sarà quello necessario: o il contatore delle foto mostrerà un valore errato, o non tutte le foto verranno inviate per la moderazione.

Un codice simile, che manipola diverse entità aziendali all'interno di una singola transazione, è stato scritto in quantità enormi nel corso dell'esistenza di Odnoklassniki. Tuttavia, dalle esperienze di migrazione a NoSQL con Eventual Consistency Sappiamo che le maggiori difficoltà (e i maggiori investimenti di tempo) derivano dalla necessità di sviluppare codice per mantenere la coerenza dei dati. Pertanto, il requisito principale per il nuovo storage era assicurare alla logica applicativa vere transazioni ACID.

Altrettanto importanti erano i requisiti:

  • In caso di guasto del data center, sia la lettura che la scrittura nel nuovo storage devono essere disponibili.
  • Mantenere l'attuale velocità di sviluppo. Ciò significa che, lavorando con il nuovo storage, la quantità di codice dovrebbe rimanere sostanzialmente la stessa, senza la necessità di scrivere ulteriori funzioni per lo storage, sviluppare algoritmi di risoluzione dei conflitti, mantenere indici secondari, ecc.
  • La velocità del nuovo storage deve essere sufficientemente elevata sia per la lettura dei dati che per l'elaborazione delle transazioni, il che implica l'inapplicabilità di soluzioni rigorose e universali, ma lente, come ad esempio: i commit a due fasi.
  • Scalabilità automatica in tempo reale.
  • Utilizzo di normali server economici, senza la necessità di acquistare hardware esotico.
  • L'opzione di sviluppare il sistema di archiviazione internamente dai nostri sviluppatori. In altre parole, si dava priorità a soluzioni proprietarie o basate su codice aperto, preferibilmente in Java.

Soluzioni, soluzioni

Analizzando le possibili soluzioni, siamo giunti a due possibili scelte architettoniche:

La prima è prendere un qualsiasi server SQL e implementare la necessaria resilienza, meccanismi di scalabilità, cluster fault-tolerant, risoluzione dei conflitti e transazioni ACID distribuite, affidabili e veloci. Abbiamo valutato questa opzione come piuttosto complessa e laboriosa.

La seconda opzione è utilizzare un archivio NoSQL pronto all'uso con scalabilità implementata, cluster fault-tolerant, risoluzione dei conflitti e implementare le transazioni e SQL noi stessi. A prima vista, anche l'implementazione di SQL, per non parlare delle transazioni ACID, sembra un compito che richiederà anni. Ma poi abbiamo capito che il set di funzionalità SQL che utilizziamo nella pratica è lontano dall'ANSI SQL altrettanto quanto Cassandra CQL è lontano dall'ANSI SQL. Esaminando più da vicino CQL, ci siamo resi conto che è abbastanza vicino a ciò di cui abbiamo bisogno.

Cassandra e CQL

Quindi, cosa rende interessante Cassandra e quali possibilità offre?

In primo luogo, è possibile creare tabelle che supportano vari tipi di dati, è possibile eseguire SELECT o UPDATE utilizzando la 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 delle repliche, Cassandra utilizza un approccio basato sul quorum. In the simplest case, this means that when placing three replicas of the same row on different nodes of the cluster, a write is considered successful if the majority of nodes (i.e., two out of three) confirm the success of this write operation. The data of the row is considered consistent if, during reading, the majority of nodes were queried and confirmed it. Thus, with three replicas, full and immediate data consistency is guaranteed in the event of one node failure. This approach has allowed us to implement an even more reliable scheme: always sending requests to all three replicas, waiting for a response from the two fastest. The delayed response from the third replica is discarded in this case. The node that responds late may have serious issues — lag, garbage collection in JVM, direct memory reclaim in the Linux kernel, hardware failure, or network disconnection. However, this does not affect client operations or the data.

The approach where we query three nodes and receive a response from two is called speculation: a request for excess replicas is sent even before it 'drops out'.

Un altro vantaggio di Cassandra è il Batchlog, un meccanismo che garantisce l'applicazione completa o il rifiuto totale delle modifiche apportate. Ciò ci consente di affrontare A in ACID — l'atomicità fuori dalla scatola.

La cosa più simile alle transazioni in Cassandra sono le cosiddettelightweight transactionsMa sono lontane dalle 'vere' transazioni ACID: in realtà, offrono la possibilità di eseguire CAS su un'unica riga di dati, utilizzando il consenso del protocollo pesante Paxos. Pertanto, la velocità di queste transazioni è limitata.

Cosa ci mancava in Cassandra

Quindi, dovevamo implementare vere transazioni ACID in Cassandra. Con cui avremmo potuto facilmente realizzare altre due funzionalità utili delle classiche DBMS: indici rapidi e consistenti, che ci avrebbero permesso di effettuare query non solo per chiave primaria e un generatore standard di ID auto-incrementali monotoni.

C*One

Così è nata una nuova DBMS C*One, composta da tre tipi di nodi server:

  • I nodi di archiviazione sono server Cassandra (quasi) standard che svolgono il compito di memorizzare i dati sui dischi locali. Con l'aumentare del carico e del volume dei dati, il loro numero può essere facilmente scalato fino a decine e centinaia.
  • I coordinatori delle transazioni garantiscono l'esecuzione delle transazioni.
  • I client sono server delle applicazioni che implementano operazioni di business e avviano transazioni. Possono esserci migliaia di tali client.

NewSQL = NoSQL + ACID

I server di tutti i tipi fanno parte di un cluster comune, utilizzano il protocollo di messaggistica interna di Cassandra per comunicare tra di loro e gossip per scambiare informazioni sul cluster. Con l'ausilio del Heartbeat, i server sono in grado di conoscere i guasti reciproci, mantenere un'unica struttura dati — tabelle, la loro struttura e replica; schema di partizionamento, topologia del cluster, ecc.

Client

NewSQL = NoSQL + ACID

Invece dei normali driver, viene utilizzata la modalità Fat Client. Questo nodo non memorizza dati, ma può fungere da coordinatore per l'esecuzione delle richieste, cioè il Cliente svolge autonomamente la funzione di coordinamento delle proprie richieste: interroga le repliche dello storage e risolve i conflitti. Questo è non solo più affidabile e veloce rispetto al driver standard, che richiede comunicazione con un coordinatore remoto, ma consente anche di gestire la trasmissione delle richieste. Fuori da una transazione aperta sul client, le richieste vengono inviate agli storage. Se il cliente ha aperto una transazione, tutte le richieste all'interno della transazione vengono indirizzate al coordinatore delle transazioni.
NewSQL = NoSQL + ACID

Coordinatore delle transazioni C*One

Il coordinatore è ciò che abbiamo implementato da zero per C*One. È responsabile della gestione delle transazioni, dei blocchi e dell'ordine di applicazione delle transazioni.

Per ogni transazione gestita, il coordinatore genera un timestamp: ogni successivo è maggiore di quello della transazione precedente. Poiché in Cassandra il sistema di risoluzione dei conflitti si basa sui timestamp (tra due record in conflitto, quello con il timestamp più recente è considerato valido), il conflitto sarà sempre risolto a favore della transazione successiva. In questo modo abbiamo implementato orologi di Lamport — un modo economico per risolvere i conflitti in un sistema distribuito.

Blocchi

Per garantire l'isolamento, abbiamo deciso di utilizzare il metodo più semplice: blocchi pessimisti sulla chiave primaria del record. In altre parole, nella transazione, il record deve prima essere bloccato, poi letto, modificato e salvato. Solo dopo un commit avvenuto con successo, il record può essere sbloccato, permettendo alle transazioni concorrenti di utilizzarlo.

Implementare un tale meccanismo di blocco è semplice in un ambiente non distribuito. In un sistema distribuito ci sono due approcci principali: o implementare un blocco distribuito sul cluster, oppure distribuire le transazioni in modo che le transazioni che coinvolgono un singolo record siano sempre gestite dallo stesso coordinatore.

Poiché nel nostro caso i dati sono già distribuiti in gruppi di transazioni locali in SQL, è stato deciso di assegnare ai coordinatori dei gruppi di transazioni locali: un coordinatore gestisce tutte le transazioni con token da 0 a 9, il secondo — con token da 10 a 19, e così via. Come risultato, ciascuno degli istanze del coordinatore diventa il master del gruppo di transazioni.

In tal modo, i blocchi possono essere implementati come una semplice HashMap nella memoria del coordinatore.

Guasti dei coordinatori

Poiché un coordinatore gestisce esclusivamente un gruppo di transazioni, è molto importante identificare rapidamente il suo guasto, affinché il tentativo di esecuzione della transazione sia completato entro il timeout. Per garantire che questo avvenga in modo veloce e affidabile, abbiamo applicato un protocollo di heartbeat a quorum pieno e connesso:

In ogni centro dati sono presenti almeno due nodi coordinatori. Periodicamente, ogni coordinatore invia un messaggio heartbeat agli altri coordinatori per segnalare il proprio funzionamento e informare quali messaggi heartbeat ha ricevuto dai coordinatori nel cluster l'ultima volta.

NewSQL = NoSQL + ACID

Ricevendo informazioni simili dagli altri tramite i loro messaggi heartbeat, ogni coordinatore determina quali nodi del cluster stanno funzionando e quali no, seguendo il principio del quorum: se il nodo X ha ricevuto informazioni dalla maggior parte dei nodi del cluster riguardo al corretto ricevimento dei messaggi dal nodo Y, allora Y è operativo. Viceversa, non appena la maggior parte dei nodi segnala la mancata ricezione dei messaggi dal nodo Y, significa che Y ha subito un guasto. È interessante notare che se il quorum informa il nodo X di non ricevere più messaggi da esso, il nodo X stesso si considererà guasto.

I messaggi Heartbeat vengono inviati ad alta frequenza, circa 20 volte al secondo, con un intervallo di 50 ms. In Java è difficile garantire una risposta dell'applicazione entro 50 ms a causa della durata comparabile delle pause causate dal garbage collector. Siamo riusciti a ottenere questo tempo di risposta utilizzando il garbage collector G1, che consente di specificare un obiettivo per la durata delle pause GC. Tuttavia, a volte, abbastanza raramente, le pause del garbage collector superano i 50 ms, il che può portare a falsi allarmi di guasto. Per evitare ciò, il coordinatore non segnala il guasto di un nodo remoto alla prima mancanza di un messaggio heartbeat, ma solo se ne mancano diversi consecutivi. In questo modo siamo riusciti a rilevare il guasto del nodo del coordinatore in 200 ms.

Ma non basta capire rapidamente quale nodo ha smesso di funzionare. È necessario fare qualcosa al riguardo.

Riserva

Lo schema classico prevede, in caso di guasto del master, di avviare l'elezione di un nuovo master tramite uno dei moderni universal algoritmi. Tuttavia, questi algoritmi presentano problemi ben noti relativi alla convergenza nel tempo e alla durata del processo di voto stesso. Siamo riusciti a evitare ritardi aggiuntivi grazie allo schema di sostituzione dei coordinatori in una rete completamente connessa:

NewSQL = NoSQL + ACID

Supponiamo di voler eseguire una transazione nel gruppo 50. Definiamo in anticipo lo schema di sostituzione, ovvero quali nodi eseguiranno le transazioni del gruppo 50 in caso di guasto del coordinatore principale. Il nostro obiettivo è mantenere la funzionalità del sistema in caso di guasto di un data center. Definiamo che il primo riserva sarà un nodo di un altro data center, e il secondo riserva sarà un nodo di un terzo. Questo schema viene scelto una sola volta e non cambia finché non cambia la topologia del cluster, ovvero finché non entrano nuovi nodi (cosa che accade molto raramente). L'ordine per la scelta di un nuovo master attivo in caso di guasto del precedente sarà sempre il seguente: il primo riserva diventerà master attivo, e se anche lui smette di funzionare, il secondo riserva.

Questo schema è più affidabile rispetto a un algoritmo universale, poiché per attivare un nuovo master è sufficiente verificare il guasto del precedente.

Ma come possono i clienti sapere quale maestro è attivo? Sono necessari 50 ms per inviare informazioni a migliaia di clienti. Può succedere che un cliente invii una richiesta per aprire una transazione senza sapere che quel maestro non è più attivo, e la richiesta si bloccano per timeout. Per evitare questo, i clienti inviano speculative richieste di apertura della transazione sia al maestro del gruppo che a entrambi i suoi riservi, ma solo il maestro attivo in quel momento risponderà. Tutta la successiva comunicazione nell'ambito della transazione avverrà solo con il maestro attivo.

I maestri di riserva collocano le richieste per transazioni non proprie in coda delle transazioni non ancora nate, dove rimangono per un certo tempo. Se il maestro attivo viene a mancare, un nuovo maestro gestisce le richieste di apertura delle transazioni dalla propria coda e risponde al cliente. Se il cliente ha già aperto una transazione con il vecchio maestro, la seconda risposta viene ignorata (e, ovviamente, tale transazione non verrà completata e sarà ripetuta dal cliente).

Come funziona una transazione

Supponiamo che un cliente abbia inviato al coordinatore una richiesta per aprire una transazione per una certa entità con una certa chiave primaria. Il coordinatore blocca questa entità e la inserisce nella tabella dei blocchi in memoria. Se necessario, il coordinatore legge questa entità dal repository e salva i dati ottenuti nello stato della transazione in memoria del coordinatore.

NewSQL = NoSQL + ACID

Quando un cliente desidera modificare i dati in una transazione, invia al coordinatore una richiesta di modifica dell'entità, e quest'ultimo inserisce i nuovi dati nella tabella degli stati delle transazioni in memoria. A questo punto, la registrazione è completata — non viene effettuata alcuna registrazione nel repository.

NewSQL = NoSQL + ACID

Quando un cliente richiede i propri dati modificati all'interno di una transazione attiva, il coordinatore agisce in questo modo:

  • se l'ID è già presente nella transazione, i dati vengono prelevati dalla memoria;
  • se l'ID non è presente in memoria, i dati mancanti vengono letti dai nodi di storage, uniti con quelli già presenti in memoria, e il risultato viene fornito al cliente.

In questo modo, il cliente può leggere le proprie modifiche, mentre gli altri clienti non vedono queste modifiche, poiché sono conservate solo in memoria del coordinatore e non sono ancora presenti nei nodi di Cassandra.

NewSQL = NoSQL + ACID

Quando un cliente invia un commit, lo stato presente in memoria del servizio viene salvato dal coordinatore in un batch registrato, che viene poi inviato ai dati di Cassandra. I dati compiono tutto il necessario affinché questo pacchetto venga applicato in modo atomico (completo) e restituiscono una risposta al coordinatore, che rilascia i blocchi e conferma il successo della transazione al cliente.

NewSQL = NoSQL + ACID

E per l'annullamento, è sufficiente che il coordinatore rilasci semplicemente la memoria occupata dallo stato della transazione.

Come risultato delle ottimizzazioni sopra descritte, abbiamo implementato i principi ACID:

  • Atomicità. Questa è la garanzia che nessuna transazione sarà registrata nel sistema in modo parziale; tutte le sue sotto-operazioni verranno eseguite o nessuna di esse verrà eseguita. Noi rispettiamo questo principio grazie al batch registrato in Cassandra.
  • Coerenza. Ogni transazione riuscita registra per definizione solo risultati validi. Se, dopo l'apertura di una transazione e l'esecuzione di alcune operazioni, si scopre che il risultato non è valido, viene eseguito un annullamento.
  • Isolamento. Durante l'esecuzione di una transazione, le transazioni parallele non devono influenzare il suo risultato. Le transazioni concorrenti sono isolate tramite blocchi pessimistici sul coordinatore. Per le letture fuori dalla transazione, si osserva il principio di isolamento a livello di Read Committed.
  • Affidabilità. Indipendentemente dai problemi nei livelli inferiori — interruzione dell'alimentazione, guasto hardware — le modifiche effettuate da una transazione completata con successo devono rimanere salvate dopo il ripristino del funzionamento.

Lettura tramite indici

Consideriamo una semplice tabella:

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

Essa ha un ID (chiave primaria), proprietario e data di modifica. Dobbiamo eseguire una richiesta molto semplice — selezionare i dati in base al proprietario con la data di modifica "nelle ultime 24 ore".

SELECT *
WHERE owner=?
AND modified>?

Affinché una richiesta del genere venga eseguita rapidamente, in un classico DBMS SQL è necessario creare un indice sulle colonne (owner, modified). Possiamo farlo in modo abbastanza semplice, poiché ora abbiamo garanzie ACID!

Indici in C*One

C'è una tabella di origine con le foto, in cui l'ID della registrazione è la chiave primaria.

NewSQL = NoSQL + ACID

Per l'indice C*One viene creata una nuova tabella che è una copia della tabella originale. La chiave corrisponde all'espressione dell'indice e include anche la chiave primaria della registrazione della tabella originale:

NewSQL = NoSQL + ACID

Ora la query per «il proprietario negli ultimi giorni» può essere riscritta come select da un'altra tabella:

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

La coerenza dei dati della tabella originale photos e dell'indice i1 è mantenuta automaticamente dal coordinatore. Sulla base solo dello schema dei dati, quando vengono ricevute delle modifiche, il coordinatore genera e memorizza le modifiche non solo della tabella principale, ma anche delle copie. Non vengono eseguite azioni aggiuntive sulla tabella dell'indice, i log non vengono letti e non vengono utilizzati blocchi. In altre parole, l'aggiunta di indici consuma quasi nessuna risorsa e influisce praticamente sulla velocità di applicazione delle modifiche.

Grazie ad ACID, siamo riusciti a implementare indici «come in SQL». Questi offrono coerenza, possono scalare, funzionano rapidamente, possono essere composti e sono integrati nel linguaggio di query CQL. Non è necessario apportare modifiche al codice applicativo per supportare gli indici. È tutto semplice, come in SQL. E cosa più importante, gli indici non influenzano la velocità di esecuzione delle modifiche alla tabella delle transazioni.

Cosa abbiamo ottenuto

Abbiamo sviluppato C*One tre anni fa e l'abbiamo messo in produzione.

Qual è il risultato finale? Valutiamo questo esaminando il sottosistema di elaborazione e archiviazione delle foto, uno dei tipi di dati più importanti in un social network. Non stiamo parlando solo delle immagini stesse, ma di tutte le varie informazioni sui metadati. Attualmente, su «Odnoklassniki» ci sono circa 20 miliardi di tali registrazioni, il sistema gestisce 80.000 richieste di lettura al secondo e fino a 8.000 transazioni ACID al secondo, relative alla modifica dei dati.

Quando abbiamo utilizzato SQL con un fattore di replica = 1 (ma in RAID 10), i metadati delle immagini venivano conservati su un cluster altamente disponibile di 32 macchine con Microsoft SQL Server (più 11 di riserva). Sono stati anche riservati 10 server per la memorizzazione dei backup. In totale, 50 macchine costose. In questo modo, il sistema operava a carico nominale, senza margine.

Dopo la migrazione al nuovo sistema, abbiamo ottenuto un fattore di replica = 3 — una copia in ogni data center. Il sistema è composto da 63 nodi di archiviazione Cassandra e 6 macchine coordinatrici, per un totale di 69 server. Queste macchine costano significativamente meno, il loro costo totale è di circa il 30% rispetto al sistema SQL. Il carico rimane al livello del 30%.

Con l'implementazione di C*One sono diminuite anche le latenze: in SQL, l'operazione di scrittura richiedeva circa 4,5 ms. In C*One — circa 1,6 ms. La durata della transazione è mediamente inferiore a 40 ms, il commit avviene in 2 ms, la durata di lettura e scrittura è mediamente di 2 ms. Il 99° percentile è di soli 3-3,1 ms, il numero di timeout è diminuito di 100 volte — tutto grazie all'ampio utilizzo di speculazioni.

Attualmente, la maggior parte dei nodi SQL Server è stata dismessa, e i nuovi prodotti vengono sviluppati esclusivamente utilizzando C*One. Abbiamo adattato C*One per funzionare nel nostro cloud. one-cloud, il che ha permesso di accelerare il dispiegamento di nuovi cluster, semplificare la configurazione e automatizzare l'operatività. Senza il codice sorgente, ciò sarebbe stato notevolmente più complicato e macchinoso.

Ora stiamo lavorando al trasferimento di altri nostri archivi nel cloud, ma questa è tutta un'altra storia.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster