NewSQL = NoSQL+ACID

NewSQL = NoSQL+ACID
Până de curând, Odnoklassniki stoca aproximativ 50 TB de date procesate în timp real în SQL Server. Pentru un astfel de volum, este aproape imposibil să oferiți acces rapid și fiabil și chiar tolerant la eșec la centrul de date folosind un DBMS SQL. De obicei, în astfel de cazuri, se folosește una dintre stocările NoSQL, dar nu totul poate fi transferat către NoSQL: unele entități necesită garanții de tranzacție ACID.

Acest lucru ne-a condus la utilizarea stocării NewSQL, adică a unui DBMS care oferă toleranță la erori, scalabilitate și performanță sistemelor NoSQL, dar menținând în același timp garanțiile ACID familiare sistemelor clasice. Există puține sisteme industriale funcționale din această nouă clasă, așa că am implementat singuri un astfel de sistem și l-am pus în funcțiune comercială.

Cum funcționează și ce s-a întâmplat - citiți sub tăietură.

Astăzi, audiența lunară a Odnoklassniki este de peste 70 de milioane de vizitatori unici. Noi Suntem în primele cinci cele mai mari rețele sociale din lume și printre cele douăzeci de site-uri pe care utilizatorii petrec cel mai mult timp. Infrastructura OK gestionează sarcini foarte mari: peste un milion de solicitări HTTP/sec pe front. Părți ale unei flote de servere de peste 8000 de bucăți sunt situate aproape una de alta - în patru centre de date din Moscova, ceea ce face posibilă asigurarea unei latențe a rețelei de mai puțin de 1 ms între ele.

Folosim Cassandra din 2010, începând cu versiunea 0.6. Astăzi există câteva zeci de clustere în funcțiune. Cel mai rapid cluster procesează peste 4 milioane de operațiuni pe secundă, iar cel mai mare stochează 260 TB.

Cu toate acestea, toate acestea sunt clustere NoSQL obișnuite utilizate pentru stocare slab coordonate date. Am vrut să înlocuim principalul stocare consistent, Microsoft SQL Server, care a fost folosit de la înființarea Odnoklassniki. Stocarea a constat din peste 300 de mașini SQL Server Standard Edition, care conțineau 50 TB de date - entități de afaceri. Aceste date sunt modificate ca parte a tranzacțiilor ACID și necesită consistenta ridicata.

Pentru a distribui datele între nodurile SQL Server, am folosit atât vertical, cât și orizontal compartimentare (fragmentare). Din punct de vedere istoric, am folosit o schemă simplă de împărțire a datelor: fiecare entitate a fost asociată cu un token - o funcție a ID-ului entității. Entitățile cu același token au fost plasate pe același server SQL. Relația master-detaliu a fost implementată astfel încât tokenurile înregistrărilor principale și secundare să se potrivească întotdeauna și să fie localizate pe același server. Într-o rețea socială, aproape toate înregistrările sunt generate în numele utilizatorului - ceea ce înseamnă că toate datele utilizatorului dintr-un subsistem funcțional sunt stocate pe un server. Adică, o tranzacție de afaceri implică aproape întotdeauna tabele de la un server SQL, ceea ce a făcut posibilă asigurarea coerenței datelor folosind tranzacții ACID locale, fără a fi nevoie de a utiliza lent și nesigur tranzacții cu ACID distribuite.

Datorită sharding-ului și pentru a accelera SQL:

  • Nu folosim constrângeri de cheie străină, deoarece la fragmentare, ID-ul entității poate fi localizat pe un alt server.
  • Nu folosim proceduri stocate și declanșatoare din cauza încărcării suplimentare pe procesorul DBMS.
  • Nu folosim JOIN-uri din cauza tuturor celor de mai sus și a multor citiri aleatorii de pe disc.
  • În afara unei tranzacții, folosim nivelul de izolare Read Uncommitted pentru a reduce blocajele.
  • Efectuăm doar tranzacții scurte (în medie mai scurte de 100 ms).
  • Nu folosim UPDATE și DELETE pe mai multe rânduri din cauza numărului mare de blocaje - actualizăm doar o înregistrare la un moment dat.
  • Efectuăm întotdeauna interogări numai pe indexuri - o interogare cu un plan de scanare a tabelului complet înseamnă pentru noi supraîncărcarea bazei de date și cauzarea eșecului acesteia.

Acești pași ne-au permis să obținem performanță aproape maximă din serverele SQL. Problemele au devenit însă din ce în ce mai numeroase. Să ne uităm la ele.

Probleme cu SQL

  • Deoarece am folosit sharding-ul auto-scris, adăugarea de noi shard-uri a fost făcută manual de către administratori. În tot acest timp, replicile de date scalabile nu au fost deservite de solicitări.
  • Pe măsură ce numărul de înregistrări din tabel crește, viteza de inserare și modificare scade atunci când se adaugă indici la un tabel existent, viteza de creare și recreare a indicilor are loc;
  • Având o cantitate mică de Windows pentru SQL Server în producție, gestionarea infrastructurii este dificilă

Dar principala problemă este

toleranta la greseli

Serverul SQL clasic are o toleranță slabă la erori. Să presupunem că aveți un singur server de baze de date și acesta eșuează o dată la trei ani. În acest timp, site-ul este oprit timp de 20 de minute, ceea ce este acceptabil. Dacă aveți 64 de servere, atunci site-ul este oprit o dată la trei săptămâni. Și dacă aveți 200 de servere, atunci site-ul nu funcționează în fiecare săptămână. Aceasta este o problemă.

Ce se poate face pentru a îmbunătăți toleranța la erori a unui server SQL? Wikipedia ne invită să construim cluster foarte disponibil: unde in cazul defectarii oricarei componente exista una de rezerva.

Acest lucru necesită o flotă de echipamente scumpe: dublări numeroase, fibră optică, stocare partajată și includerea unei rezerve nu funcționează în mod fiabil: aproximativ 10% dintre comutări se termină cu defecțiunea nodului de rezervă ca un tren în spatele nodului principal.

Dar principalul dezavantaj al unui astfel de cluster foarte disponibil este disponibilitatea zero dacă centrul de date în care se află eșuează. Odnoklassniki are patru centre de date și trebuie să asigurăm funcționarea în cazul unei defecțiuni complete în unul dintre ele.

Pentru asta am putea folosi Multi-Maestru replicare încorporată în SQL Server. Această soluție este mult mai costisitoare din cauza costului software-ului și suferă de probleme binecunoscute cu replicarea - întârzieri imprevizibile ale tranzacțiilor cu replicare sincronă și întârzieri în aplicarea replicărilor (și, ca urmare, modificări pierdute) cu replicarea asincronă. Implicit rezolvarea manuală a conflictelor face această opțiune complet inaplicabilă pentru noi.

Toate aceste probleme au necesitat o soluție radicală și am început să le analizăm în detaliu. Aici trebuie să ne familiarizăm cu ceea ce face în principal SQL Server - tranzacții.

Tranzacție simplă

Să luăm în considerare cea mai simplă tranzacție, din punctul de vedere al unui programator SQL aplicat: adăugarea unei fotografii într-un album. Albumele și fotografiile sunt stocate în plăci diferite. Albumul are un contor public de fotografii. Apoi, o astfel de tranzacție este împărțită în următorii pași:

  1. Închidem albumul cu cheie.
  2. Creați o intrare în tabelul cu fotografii.
  3. Dacă fotografia are un statut public, atunci adăugați un contor de fotografii public la album, actualizați înregistrarea și efectuați tranzacția.

Sau în pseudocod:

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

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

TX.commit();

Vedem că cel mai frecvent scenariu de tranzacție comercială este citirea datelor din baza de date în memoria serverului de aplicații, modificarea ceva și salvarea noilor valori înapoi în baza de date. De obicei într-o astfel de tranzacție actualizăm mai multe entități, mai multe tabele.

La executarea unei tranzacții, poate apărea modificarea concomitentă a acelorași date dintr-un alt sistem. De exemplu, Antispam poate decide că utilizatorul este cumva suspect și, prin urmare, toate fotografiile utilizatorului nu ar trebui să mai fie publice, trebuie trimise pentru moderare, ceea ce înseamnă schimbarea photo.status cu o altă valoare și dezactivarea contoarelor corespunzătoare. Evident, dacă această operație are loc fără garanții de atomicitate a aplicării și izolarea modificărilor concurente, ca în ACID, atunci rezultatul nu va fi cel necesar - fie contorul de fotografii va afișa valoarea greșită, fie nu toate fotografiile vor fi trimise spre moderare.

O mulțime de coduri similare, care manipulează diverse entități de afaceri în cadrul unei singure tranzacții, au fost scrise de-a lungul întregii existențe a Odnoklassniki. Pe baza experienței migrărilor către NoSQL de la Consecvență eventuală Știm că cea mai mare provocare (și investiția de timp) vine din dezvoltarea codului pentru a menține consistența datelor. Prin urmare, am considerat că principala cerință pentru noul stocare este asigurarea tranzacțiilor ACID reale pentru logica aplicației.

Alte cerințe, nu mai puțin importante, au fost:

  • Dacă centrul de date eșuează, atât citirea, cât și scrierea în noua stocare trebuie să fie disponibile.
  • Menținerea vitezei actuale de dezvoltare. Adică, atunci când lucrați cu un nou depozit, cantitatea de cod ar trebui să fie aproximativ aceeași, nu ar trebui să fie nevoie să adăugați nimic la depozit, să dezvoltați algoritmi pentru rezolvarea conflictelor, menținerea indicilor secundari etc.
  • Viteza noii stocări trebuia să fie destul de mare, atât la citirea datelor, cât și la procesarea tranzacțiilor, ceea ce însemna efectiv că soluțiile riguroase din punct de vedere academic, universale, dar lente, cum ar fi, de exemplu, nu erau aplicabile. comisii în două faze.
  • Scalare automată din mers.
  • Folosind servere ieftine obișnuite, fără a fi nevoie să achiziționați hardware exotic.
  • Posibilitatea dezvoltării stocării de către dezvoltatorii companiei. Cu alte cuvinte, s-a acordat prioritate soluțiilor proprietare sau open source, de preferință în Java.

Decizii, decizii

Analizând posibilele soluții, am ajuns la două posibile variante de arhitectură:

Primul este să luați orice server SQL și să implementați toleranța la erori, mecanismul de scalare, clusterul de failover, rezolvarea conflictelor și tranzacțiile ACID distribuite, fiabile și rapide. Am evaluat această opțiune ca fiind foarte nebanală și care necesită multă muncă.

A doua opțiune este să luați o stocare NoSQL gata făcută cu scalare implementată, un cluster de failover, rezolvare a conflictelor și să implementați singur tranzacțiile și SQL. La prima vedere, chiar și sarcina de a implementa SQL, ca să nu mai vorbim de tranzacțiile ACID, pare o sarcină care va dura ani. Dar apoi ne-am dat seama că setul de caracteristici SQL pe care îl folosim în practică este la fel de departe de ANSI SQL Cassandra CQL departe de ANSI SQL. Privind și mai atent la CQL, ne-am dat seama că era destul de aproape de ceea ce aveam nevoie.

Cassandra și CQL

Deci, ce este interesant la Cassandra, ce capacități are?

În primul rând, aici puteți crea tabele care acceptă diferite tipuri de date, puteți face SELECT sau UPDATE pe cheia primară.

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

Pentru a asigura coerența datelor replicate, Cassandra folosește abordarea cvorumului. În cel mai simplu caz, aceasta înseamnă că atunci când trei replici ale aceluiași rând sunt plasate pe noduri diferite ale clusterului, scrierea este considerată reușită dacă majoritatea nodurilor (adică două din trei) au confirmat succesul acestei operațiuni de scriere. . Datele de rând sunt considerate consistente dacă, la citire, majoritatea nodurilor au fost interogate și le-au confirmat. Astfel, cu trei replici, consistența completă și instantanee a datelor este garantată dacă un nod eșuează. Această abordare ne-a permis să implementăm o schemă și mai fiabilă: trimiteți întotdeauna cereri către toate cele trei replici, așteptând un răspuns de la cele două cele mai rapide. Răspunsul tardiv al celei de-a treia replică este eliminat în acest caz. Un nod care întârzie să răspundă poate avea probleme serioase - frâne, colectarea gunoiului în JVM, recuperarea directă a memoriei în nucleul Linux, defecțiune hardware, deconectare de la rețea. Totuși, acest lucru nu afectează în niciun fel operațiunile sau datele clientului.

Abordarea când contactăm trei noduri și primim un răspuns de la două este numită speculație: o cerere de replici suplimentare este trimisă chiar înainte de a „cădea”.

Un alt beneficiu al Cassandra este Batchlog, un mecanism care asigură că un lot de modificări pe care le faci sunt fie aplicate în totalitate, fie deloc aplicate. Acest lucru ne permite să rezolvăm A în ACID - atomicitate din cutie.

Cel mai apropiat lucru de tranzacțiile în Cassandra sunt așa-numitele „tranzacții ușoare". Dar sunt departe de a fi „adevărate” tranzacții cu ACID: de fapt, aceasta este o oportunitate de a face CAS pe date dintr-o singură înregistrare, folosind consensul utilizând protocolul Paxos grea. Prin urmare, viteza unor astfel de tranzacții este scăzută.

Ce ne lipsea la Cassandra

Deci, a trebuit să implementăm tranzacții reale cu ACID în Cassandra. Folosind care am putea implementa cu ușurință alte două caracteristici convenabile ale SGBD-ului clasic: indexuri rapide consecvente, care ne-ar permite să efectuăm selecții de date nu numai prin cheia primară și un generator obișnuit de ID-uri monotone cu autoincrementare.

Con

Astfel a luat naștere un nou SGBD Con, constând din trei tipuri de noduri de server:

  • Stocare – servere (aproape) standard Cassandra responsabile pentru stocarea datelor pe discuri locale. Pe măsură ce încărcarea și volumul de date cresc, cantitatea acestora poate fi scalată cu ușurință la zeci și sute.
  • Coordonatori de tranzactii - asigura executarea tranzactiilor.
  • Clienții sunt servere de aplicații care implementează operațiuni de afaceri și inițiază tranzacții. Pot exista mii de astfel de clienți.

NewSQL = NoSQL+ACID

Serverele de toate tipurile fac parte dintr-un cluster comun, utilizează protocolul intern de mesaje Cassandra pentru a comunica între ele și bârfă pentru schimbul de informații despre cluster. Cu Heartbeat, serverele învață despre eșecurile reciproce, mențin o singură schemă de date - tabele, structura și replicarea acestora; schema de partiționare, topologia clusterului etc.

Clienti

NewSQL = NoSQL+ACID

În locul driverelor standard, se folosește modul Fat Client. Un astfel de nod nu stochează date, dar poate acționa ca un coordonator pentru execuția cererii, adică Clientul însuși acționează ca un coordonator al cererilor sale: interogează replicile de stocare și rezolvă conflictele. Acest lucru nu este doar mai fiabil și mai rapid decât șoferul standard, care necesită comunicarea cu un coordonator de la distanță, dar vă permite și să controlați transmiterea solicitărilor. În afara unei tranzacții deschise pe client, cererile sunt trimise către depozite. Dacă clientul a deschis o tranzacție, atunci toate cererile din cadrul tranzacției sunt trimise coordonatorului tranzacției.
NewSQL = NoSQL+ACID

C*One Coordonator tranzacție

Coordonatorul este ceva ce am implementat pentru C*One de la zero. Este responsabil pentru gestionarea tranzacțiilor, a blocărilor și a ordinii în care sunt aplicate tranzacțiile.

Pentru fiecare tranzacție deservită, coordonatorul generează un marcaj de timp: fiecare tranzacție ulterioară este mai mare decât tranzacția anterioară. Deoarece sistemul de soluționare a conflictelor al Cassandrei se bazează pe marcaje temporale (din două înregistrări conflictuale, cea cu cea mai recentă marca temporală este considerată actuală), conflictul va fi întotdeauna rezolvat în favoarea tranzacției ulterioare. Astfel am implementat Ceas Lampport - o modalitate ieftină de a rezolva conflictele într-un sistem distribuit.

Încuietori

Pentru a asigura izolarea, am decis să folosim cea mai simplă metodă - încuietori pesimiste bazate pe cheia primară a înregistrării. Cu alte cuvinte, într-o tranzacție, o înregistrare trebuie mai întâi blocată, abia apoi citită, modificată și salvată. Numai după o comitere reușită poate fi deblocată o înregistrare, astfel încât tranzacțiile concurente să o poată utiliza.

Implementarea unei astfel de blocări este simplă într-un mediu nedistribuit. Într-un sistem distribuit, există două opțiuni principale: fie implementați blocarea distribuită pe cluster, fie distribuiți tranzacțiile astfel încât tranzacțiile care implică aceeași înregistrare să fie întotdeauna deservite de același coordonator.

Deoarece în cazul nostru datele sunt deja distribuite între grupurile de tranzacții locale în SQL, s-a decis să atribuim coordonatorilor grupuri de tranzacții locale: un coordonator efectuează toate tranzacțiile cu jetoane de la 0 la 9, al doilea - cu jetoane de la 10 la 19, și așa mai departe. Ca rezultat, fiecare dintre instanțe de coordonator devine stăpânul grupului de tranzacții.

Apoi, blocările pot fi implementate sub forma unui HashMap banal în memoria coordonatorului.

Eșecuri ale coordonatorului

Deoarece un coordonator deservește exclusiv un grup de tranzacții, este foarte important să se determine rapid eșecul acestuia, astfel încât a doua încercare de a executa tranzacția să expire. Pentru a face acest lucru rapid și fiabil, am folosit un protocol hearbeat de cvorum complet conectat:

Fiecare centru de date găzduiește cel puțin două noduri de coordonare. Periodic, fiecare coordonator trimite un mesaj de bătăi inimii celorlalți coordonatori și îi informează despre funcționarea acestuia, precum și ce mesaje de bătăi inimii a primit de la ce coordonatori din cluster ultima dată.

NewSQL = NoSQL+ACID

Primind informații similare de la ceilalți ca parte a mesajelor bătăilor inimii, fiecare coordonator decide singur care noduri de cluster funcționează și care nu, ghidat de principiul cvorumului: dacă nodul X a primit informații de la majoritatea nodurilor din cluster despre normalul primirea mesajelor de la nodul Y, apoi , Y funcționează. Și invers, de îndată ce majoritatea raportează mesaje lipsă de la nodul Y, atunci Y a refuzat. Este curios că dacă cvorumul informează nodul X că nu mai primește mesaje de la acesta, atunci nodul X însuși se va considera a eșuat.

Mesajele Heartbeat sunt trimise cu frecventa mare, de aproximativ 20 de ori pe secunda, cu o perioada de 50 ms. În Java, este dificil să se garanteze răspunsul aplicației în 50 ms din cauza duratei comparabile a pauzelor cauzate de colectorul de gunoi. Am reușit să obținem acest timp de răspuns folosind colectorul de gunoi G1, care ne permite să specificăm o țintă pentru durata pauzelor GC. Cu toate acestea, uneori, destul de rar, pauza colectorului depășește 50 ms, ceea ce poate duce la o detecție falsă a defecțiunii. Pentru a preveni acest lucru, coordonatorul nu raportează o defecțiune a unui nod la distanță când dispare primul mesaj de bătăi ale inimii de la acesta, doar dacă mai multe au dispărut la rând. Așa am reușit să detectăm o defecțiune a nodului coordonator în 200 Domnișoară.

Dar nu este suficient să înțelegeți rapid care nod a încetat să funcționeze. Trebuie să facem ceva în privința asta.

Rezervare

Schema clasică presupune, în cazul unui eșec de master, începerea unei noi alegeri folosind una dintre la modă universal algoritmi. Cu toate acestea, astfel de algoritmi au probleme bine-cunoscute cu convergența timpului și durata procesului electoral în sine. Am reușit să evităm astfel de întârzieri suplimentare folosind o schemă de înlocuire a coordonatorului într-o rețea complet conectată:

NewSQL = NoSQL+ACID

Să presupunem că vrem să executăm o tranzacție în grupul 50. Să determinăm în prealabil schema de înlocuire, adică care noduri vor executa tranzacții în grupul 50 în cazul unei eșecuri a coordonatorului principal. Scopul nostru este să menținem funcționalitatea sistemului în cazul unei defecțiuni a centrului de date. Să stabilim că prima rezervă va fi un nod dintr-un alt centru de date, iar a doua rezervă va fi un nod dintr-un al treilea. Această schemă este selectată o dată și nu se schimbă până când topologia clusterului nu se schimbă, adică până când intră în ea noduri noi (ceea ce se întâmplă foarte rar). Procedura de selectare a unui nou master activ dacă cel vechi eșuează va fi întotdeauna următoarea: prima rezervă va deveni master activ, iar dacă a încetat să mai funcționeze, a doua rezervă va deveni master activ.

Această schemă este mai fiabilă decât algoritmul universal, deoarece pentru a activa un nou master este suficient să determinați eșecul celui vechi.

Dar cum vor înțelege clienții care maestru lucrează acum? Este imposibil să trimiți informații către mii de clienți în 50 ms. Este posibilă o situație când un client trimite o cerere de deschidere a unei tranzacții, neștiind încă că acest master nu mai funcționează, iar cererea va expira. Pentru a preveni acest lucru, clienții trimit în mod speculativ o cerere de deschidere a unei tranzacții către comandantul grupului și ambele rezerve ale acestuia simultan, dar numai cel care este comandantul activ în acest moment va răspunde la această solicitare. Toate comunicările ulterioare în cadrul tranzacției vor fi efectuate de către client numai cu masterul activ.

Maeștrii de rezervă plasează cererile primite pentru tranzacții care nu sunt ale lor în coada tranzacțiilor nenăscute, unde sunt stocate pentru ceva timp. Dacă masterul activ moare, noul master procesează cererile de deschidere a tranzacțiilor din coada sa și răspunde clientului. Dacă clientul a deschis deja o tranzacție cu vechiul master, atunci al doilea răspuns este ignorat (și, evident, o astfel de tranzacție nu se va finaliza și va fi repetată de client).

Cum funcționează tranzacția

Să presupunem că un client a trimis o solicitare coordonatorului de a deschide o tranzacție pentru o astfel de entitate cu o cheie primară. Coordonatorul blochează această entitate și o plasează în tabelul de blocare în memorie. Dacă este necesar, coordonatorul citește această entitate din stocare și stochează datele rezultate într-o stare de tranzacție în memoria coordonatorului.

NewSQL = NoSQL+ACID

Atunci când un client dorește să modifice datele dintr-o tranzacție, acesta trimite o solicitare coordonatorului de modificare a entității, iar coordonatorul plasează în memorie noile date în tabelul de stare a tranzacției. Aceasta finalizează înregistrarea - nu se face nicio înregistrare în stocare.

NewSQL = NoSQL+ACID

Când un client solicită propriile sale date modificate ca parte a unei tranzacții active, coordonatorul acționează după cum urmează:

  • dacă ID-ul este deja în tranzacție, atunci datele sunt preluate din memorie;
  • dacă nu există ID în memorie, atunci datele lipsă sunt citite din nodurile de stocare, combinate cu cele deja în memorie, iar rezultatul este dat clientului.

Astfel, clientul își poate citi propriile modificări, dar ceilalți clienți nu văd aceste modificări, deoarece sunt stocate doar în memoria coordonatorului nu se află încă în nodurile Cassandra;

NewSQL = NoSQL+ACID

Când clientul trimite commit, starea care se afla în memoria serviciului este salvată de coordonator într-un lot înregistrat și este trimisă ca lot înregistrat la stocarea Cassandra. Magazinele fac tot ce este necesar pentru a se asigura că acest pachet este aplicat atomic (complet) și returnează un răspuns coordonatorului, care eliberează încuietorile și confirmă clientului succesul tranzacției.

NewSQL = NoSQL+ACID

Iar pentru a derula înapoi, coordonatorul trebuie doar să elibereze memoria ocupată de starea tranzacției.

Ca urmare a îmbunătățirilor de mai sus, am implementat principiile ACID:

  • Atomicitatea. Aceasta este o garanție că nicio tranzacție nu va fi înregistrată parțial în sistem, fie că toate suboperațiunile sale nu vor fi finalizate; Aderăm la acest principiu prin lotul înregistrat în Cassandra.
  • Consecvență. Fiecare tranzacție reușită, prin definiție, înregistrează doar rezultate valide. Dacă, după deschiderea unei tranzacții și efectuarea unei părți a operațiunilor, se descoperă că rezultatul este invalid, se efectuează un rollback.
  • Izolare. Atunci când o tranzacție este executată, tranzacțiile concurente nu ar trebui să afecteze rezultatul acesteia. Tranzacțiile concurente sunt izolate folosind blocări pesimiste asupra coordonatorului. Pentru citirile din afara unei tranzacții, principiul izolării este respectat la nivelul Read Committed.
  • Stabilitate. Indiferent de problemele de la nivelurile inferioare - întrerupere a sistemului, defecțiune hardware - modificările efectuate printr-o tranzacție finalizată cu succes ar trebui să rămână păstrate atunci când operațiunile se reiau.

Citirea după indici

Să luăm un tabel simplu:

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

Are un ID (cheie primară), proprietar și data modificării. Trebuie să faceți o solicitare foarte simplă - selectați datele proprietarului cu data modificării „pentru ultima zi”.

SELECT *
WHERE owner=?
AND modified>?

Pentru ca o astfel de interogare să fie procesată rapid, într-un SGBD SQL clasic trebuie să construiți un index pe coloane (proprietar, modificat). Putem face acest lucru destul de ușor, deoarece acum avem garanții ACID!

Indici în C*One

Există un tabel sursă cu fotografii în care ID-ul înregistrării este cheia primară.

NewSQL = NoSQL+ACID

Pentru un index, C*One creează un nou tabel care este o copie a originalului. Cheia este aceeași cu expresia index și include, de asemenea, cheia primară a înregistrării din tabelul sursă:

NewSQL = NoSQL+ACID

Acum, interogarea pentru „proprietar pentru ultima zi” poate fi rescrisă ca o selecție dintr-un alt tabel:

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

Coerența datelor din fotografiile din tabelul sursă și din tabelul index i1 este menținută automat de către coordonator. Pe baza schemei de date numai, atunci când se primește o modificare, coordonatorul generează și stochează o modificare nu numai în tabelul principal, ci și în copii. Nu sunt efectuate acțiuni suplimentare pe tabelul de index, jurnalele nu sunt citite și nu sunt utilizate blocări. Adică, adăugarea de indici nu consumă aproape deloc resurse și nu are practic niciun efect asupra vitezei de aplicare a modificărilor.

Folosind ACID, am putut implementa indecși asemănătoare SQL. Sunt consecvente, scalabile, rapide, componabile și încorporate în limbajul de interogare CQL. Nu sunt necesare modificări ale codului aplicației pentru a suporta indexuri. Totul este la fel de simplu ca în SQL. Și cel mai important, indicii nu afectează viteza de execuție a modificărilor la tabelul original de tranzacții.

Ce s-a întâmplat

Am dezvoltat C*One în urmă cu trei ani și l-am lansat în exploatare comercială.

Ce am primit pana la urma? Să evaluăm acest lucru folosind exemplul subsistemului de procesare și stocare a fotografiilor, unul dintre cele mai importante tipuri de date dintr-o rețea de socializare. Nu vorbim despre corpurile fotografiilor în sine, ci despre tot felul de metainformații. Acum Odnoklassniki are aproximativ 20 de miliarde de astfel de înregistrări, sistemul procesează 80 de mii de solicitări de citire pe secundă, până la 8 mii de tranzacții ACID pe secundă asociate cu modificarea datelor.

Când am folosit SQL cu factor de replicare = 1 (dar în RAID 10), metainformațiile foto au fost stocate pe un cluster foarte disponibil de 32 de mașini care rulează Microsoft SQL Server (plus 11 copii de rezervă). De asemenea, au fost alocate 10 servere pentru stocarea copiilor de rezervă. Un total de 50 de mașini scumpe. În același timp, sistemul a funcționat la sarcină nominală, fără rezervă.

După migrarea la noul sistem, am primit factor de replicare = 3 - o copie în fiecare centru de date. Sistemul este format din 63 de noduri de stocare Cassandra și 6 mașini de coordonare, pentru un total de 69 de servere. Dar aceste mașini sunt mult mai ieftine, costul lor total este de aproximativ 30% din costul unui sistem SQL. În același timp, sarcina este menținută la 30%.

Odată cu introducerea C*One, latența a scăzut și ea: în SQL, o operație de scriere a durat aproximativ 4,5 ms. În C*One - aproximativ 1,6 ms. Durata tranzacției este în medie mai mică de 40 ms, commit-ul este finalizat în 2 ms, durata de citire și scriere este în medie de 2 ms. Percentila 99 - doar 3-3,1 ms, numărul de timeout-uri a scăzut de 100 de ori - totul datorită utilizării pe scară largă a speculațiilor.

Până acum, majoritatea nodurilor SQL Server au fost scoase din funcțiune, sunt dezvoltate produse noi numai folosind C*One. Am adaptat C*One pentru a funcționa în cloud-ul nostru un singur nor, care a făcut posibilă accelerarea implementării de noi clustere, simplificarea configurației și automatizarea operațiunii. Fără codul sursă, acest lucru ar fi mult mai dificil și mai greoi.

Acum lucrăm la transferul celorlalte facilități de stocare în cloud - dar aceasta este o cu totul altă poveste.

Sursa: www.habr.com

Adauga un comentariu