
Bis vor kurzem wurden in Odnoklassniki etwa 50 TB Daten in Echtzeit in SQL Server verarbeitet. Bei diesem Datenvolumen ist es nahezu unmöglich, einen schnellen und zuverlĂ€ssigen, zudem ausfallsicheren Rechenzentrumszugang mit SQL-Datenbanken zu gewĂ€hrleisten. Ăblicherweise werden in solchen FĂ€llen eine der NoSQL-Datenbanken eingesetzt, doch nicht alles lĂ€sst sich nach NoSQL ĂŒbertragen: Einige EntitĂ€ten benötigen ACID-Transaktionsgarantien.
Das hat uns zur Nutzung einer NewSQL-Datenbank gefĂŒhrt, d.h. einer Datenbank, die die Ausfallsicherheit, Skalierbarkeit und Geschwindigkeit von NoSQL-Systemen bietet, jedoch die gewohnten ACID-Garantien klassischer Systeme beibehĂ€lt. Es gibt nur wenige funktionierende Industriesysteme dieser neuen Klasse, weshalb wir ein solches System selbst entwickelt und in den produktiven Betrieb genommen haben.
Wie es funktioniert und was dabei herausgekommen ist â lesen Sie weiter unten.
Heute hat die monatliche Nutzerzahl von âOdnoklassnikiâ mehr als 70 Millionen einzigartige Besucher. Wir eine der gröĂten sozialen Netzwerke der Welt und gehört zu den zehn Websites, auf denen die Nutzer die meiste Zeit verbringen. Die Infrastruktur von "OK" bewĂ€ltigt extrem hohe Lasten: mehr als eine Million HTTP-Anfragen pro Sekunde fĂŒr die Frontends. Teile des Serverparks mit ĂŒber 8000 Servern sind nahe beieinander in vier Rechenzentren in Moskau untergebracht, was eine Netzwerklatenz von unter 1 ms zwischen ihnen ermöglicht.
Wir verwenden Cassandra seit 2010, beginnend mit Version 0.6. Heute sind mehrere Dutzend Cluster im Einsatz. Der schnellste Cluster verarbeitet mehr als 4 Millionen Operationen pro Sekunde, wĂ€hrend der gröĂte 260 TB speichert.
Allerdings sind das alles gewöhnliche NoSQL-Cluster, die fĂŒr die Speicherung verwendet werden. Daten. Wir wollten jedoch das primĂ€re konsistente Speichersystem, Microsoft SQL Server, ersetzen, das seit der GrĂŒndung von "Odnoklassniki" verwendet wurde. Das Speichersystem bestand aus mehr als 300 SQL Server Standard Edition Maschinen, auf denen 50 TB an GeschĂ€ftseinheiten gespeichert waren. Diese Daten werden im Rahmen von ACID-Transaktionen modifiziert und erfordern .
FĂŒr die Verteilung von Daten ĂŒber SQL Server-Knoten haben wir sowohl vertikale als auch horizontale (Sharding) verwendet. Historisch haben wir ein einfaches Sharding-Schema genutzt: Jeder EntitĂ€t wurde ein Token zugewiesen â eine Funktion, die vom EntitĂ€ts-ID abgeleitet ist. EntitĂ€ten mit demselben Token wurden auf einen SQL-Server verschoben. Das VerhĂ€ltnis zwischen Master- und Detaildatensatz wurde so umgesetzt, dass die Tokens des Haupt- und des erzeugten Datensatzes immer ĂŒbereinstimmten und sich auf demselben Server befanden. In sozialen Netzwerken werden fast alle DatensĂ€tze im Namen des Benutzers erzeugt â das heiĂt, alle Benutzerdaten innerhalb eines funktionalen Subsystems werden auf einem Server gespeichert. Das bedeutet, dass bei GeschĂ€ftstransaktionen fast immer Tabellen eines SQL-Servers beteiligt waren, was die Konsistenz der Daten durch lokale ACID-Transaktionen gewĂ€hrleistete, ohne die Notwendigkeit verteilte ACID-Transaktionen zu verwenden.
Dank Sharding und zur Beschleunigung der SQL-Leistung:
- Verwenden wir keine Foreign Key Constraints, da sich beim Sharding die ID der EntitÀt auf einem anderen Server befinden kann.
- Wir verwenden keine gespeicherten Prozeduren und Trigger, da sie die CPU der Datenbank belasten.
- JOINs vermeiden wir aus denselben GrĂŒnden sowie aufgrund der vielen zufĂ€lligen LesevorgĂ€nge von der Festplatte.
- Um Deadlocks zu reduzieren, nutzen wir auĂerhalb von Transaktionen die Isolationsebene Read Uncommitted.
- Wir fĂŒhren nur kurze Transaktionen durch (im Durchschnitt unter 100 ms).
- Multireihen-UPDATEs und DELETEs vermeiden wir aufgrund der hohen Anzahl von Deadlocks â wir aktualisieren immer nur einen Datensatz.
- Abfragen fĂŒhren wir stets nur ĂŒber Indizes aus â eine Abfrage mit einem vollstĂ€ndigen Tabellenansichtsplan bedeutet fĂŒr uns eine Ăberlastung der Datenbank und deren Ausfall.
Diese MaĂnahmen haben es uns ermöglicht, nahezu die maximale Leistung aus den SQL-Servern herauszuholen. Allerdings wurden die Probleme immer zahlreicher. Lassen Sie uns diese betrachten.
Probleme mit SQL
- Da wir eine eigene Sharding-Lösung verwendet haben, wurden zusĂ€tzliche Shards manuell von den Administratoren hinzugefĂŒgt. WĂ€hrend dieser Zeit haben die skalierbaren Datenreplikate keine Anfragen bedient.
- Wenn die Anzahl der EintrĂ€ge in einer Tabelle wĂ€chst, sinkt die Geschwindigkeit beim EinfĂŒgen und Modifizieren. Bei der HinzufĂŒgung von Indizes zu einer bestehenden Tabelle sinkt die Geschwindigkeit exponentiell, und die Erstellung sowie das Neuanlegen von Indizes erfolgt mit Ausfallzeiten.
- Die PrĂ€senz einer kleinen Anzahl von Windows-Instanzen fĂŒr SQL Server in der Produktion erschwert das Management der Infrastruktur.
Doch das Hauptproblem ist â
Ausfallsicherheit
Ein klassischer SQL-Server hat eine mangelhafte Ausfallsicherheit. Angenommen, Sie haben nur einen Datenbankserver, der alle drei Jahre ausfĂ€llt. WĂ€hrend dieser Zeit ist die Website fĂŒr 20 Minuten nicht verfĂŒgbar, was akzeptabel ist. Bei 64 Servern ist die Website bereits alle drei Wochen nicht verfĂŒgbar. Und bei 200 Servern ist die Website jede Woche nicht verfĂŒgbar. Das ist ein Problem.
Was kann getan werden, um die Ausfallsicherheit des SQL-Servers zu erhöhen? Wikipedia schlÀgt vor, einen einzurichten, bei dem im Falle eines Ausfalls einer der Komponenten eine redundante vorhanden ist.
Das erfordert eine teure Hardware-Infrastruktur: umfangreiche Redundanzen, Glasfaser, gemeinsamen Speicher und auch das Einschalten der Reserve funktioniert unzuverlÀssig: etwa 10 % der Aktivierungen enden mit einem Ausfall der Reserveknoten hinter dem Hauptknoten.
Der Hauptnachteil eines solchen hochverfĂŒgbaren Clusters ist jedoch die vollstĂ€ndige UnzugĂ€nglichkeit, wenn das Rechenzentrum, in dem es betrieben wird, ausfĂ€llt. âOdnoClassnikiâ betreibt vier Rechenzentren, und wir mĂŒssen sicherstellen, dass der Betrieb bei einem vollstĂ€ndigen Ausfall eines dieser Zentren aufrechterhalten wird.
Zu diesem Zweck könnte man Replikation verwenden, die in SQL Server integriert ist. Diese Lösung ist aufgrund der Softwarekosten deutlich teurer und leidet unter den bekannten Problemen der Replikation â unvorhersehbaren Verzögerungen bei Transaktionen bei synchroner Replikation und Verzögerungen bei der Anwendung von Replikationen (und infolgedessen verloren gegangenen Ănderungen) bei asynchroner Replikation. Die implizierte macht diese Option fĂŒr uns völlig unbrauchbar.
Alle diese Probleme erforderten eine radikale Lösung, und wir begannen mit deren detaillierter Analyse. Hier mĂŒssen wir uns mit dem vertraut machen, was SQL Server im Wesentlichen macht â Transaktionen.
Eine einfache Transaktion
Betrachten wir eine einfache Transaktion aus der Perspektive eines praktischen SQL-Programmierers: das HinzufĂŒgen eines Fotos zu einem Album. Alben und Fotos werden in verschiedenen Tabellen gespeichert. Jedes Album hat einen ZĂ€hler fĂŒr öffentliche Fotos. Somit wird diese Transaktion in die folgenden Schritte unterteilt:
- Sperren Sie das Album mit dem SchlĂŒssel.
- Erstellen Sie einen Eintrag in der Fototabelle.
- Wenn das Foto den öffentlichen Status hat, erhöhen wir den ZĂ€hler fĂŒr öffentliche Fotos im Album, aktualisieren den Eintrag und beenden die Transaktion.
Oder in Form von Pseudocode:
TX.start("Alben", id);
Album album = albums.lock(id);
Photo photo = photos.create(âŠ);
if (photo.status == PUBLIC) {
album.incPublicPhotosCount();
}
album.update();
TX.commit();Wir sehen, dass das hĂ€ufigste Szenario einer GeschĂ€ftstransaktion darin besteht, Daten aus der DB in den Arbeitsspeicher des Anwendungsservers zu lesen, etwas zu Ă€ndern und die neuen Werte zurĂŒck in die DB zu speichern. Normalerweise aktualisieren wir in einer solchen Transaktion mehrere EntitĂ€ten, mehrere Tabellen.
Bei der DurchfĂŒhrung einer Transaktion kann es zu einer gleichzeitigen Modifikation derselben Daten aus einem anderen System kommen. Beispielsweise könnte ein Antispam-System entscheiden, dass ein Nutzer verdĂ€chtig ist, und damit alle Fotos des Nutzers nicht mehr öffentlich sein sollten, was bedeutet, dass der photo.status auf einen anderen Wert geĂ€ndert und die entsprechenden ZĂ€hler zurĂŒckgesetzt werden mĂŒssen. Offensichtlich wird das Ergebnis nicht den Anforderungen entsprechen, wenn diese Operation ohne Garantien fĂŒr die atomare Anwendung und Isolation konkurrierender Modifikationen erfolgt, wie in , denn das Ergebnis könnte entweder einen falschen ZĂ€hlerstand fĂŒr die Fotos anzeigen oder es werden nicht alle Fotos zur Moderation geschickt.
Solchen Code, der mit verschiedenen GeschĂ€ftseinheiten innerhalb einer Transaktion manipuliert, wurde wĂ€hrend der gesamten Existenz von Odnoklassniki in groĂer Zahl geschrieben. Aus Erfahrung mit Migrationen zu NoSQL gibt es Wir wissen, dass die gröĂten Herausforderungen (und der damit verbundene Zeitaufwand) darin bestehen, Code zu entwickeln, der die Konsistenz der Daten sicherstellt. Daher war das Hauptaugenmerk bei der Auswahl eines neuen Speichersystems die Bereitstellung echter ACID-Transaktionen fĂŒr die Anwendungsebene.
Weitere ebenso wichtige Anforderungen waren:
- Im Falle eines Ausfalls des Rechenzentrums mĂŒssen sowohl Lese- als auch Schreibzugriffe auf das neue Speichersystem möglich sein.
- Die aktuelle Entwicklungsgeschwindigkeit muss beibehalten werden. Das bedeutet, dass bei der Arbeit mit dem neuen Speichersystem die Menge des Codes etwa gleich bleiben sollte und es keine Notwendigkeit entstehen darf, zusÀtzliche Funktionen im Speicher zu implementieren, Algorithmen zur Konfliktlösung zu entwickeln, sekundÀre Indizes aufrechtzuerhalten usw.
- Die Leistung des neuen Speichersystems muss sowohl bei LesevorgÀngen als auch bei der Verarbeitung von Transaktionen ausreichend hoch sein, was effektiv die Unanwendbarkeit akademisch strenger, universaler, aber langsamer Lösungen impliziert, wie z. B. .
- Automatisches On-the-Fly-Scaling.
- Der Einsatz normaler kostengĂŒnstiger Server, ohne dass exotische Hardware angeschafft werden muss.
- Die Möglichkeit, die Speicherlösungen durch die Entwickler des Unternehmens weiterzuentwickeln. Mit anderen Worten, der Fokus lag auf eigenen oder Open-Source-Lösungen, vorzugsweise in Java.
Lösungen, Lösungen
Bei der Analyse möglicher Lösungen kamen wir zu zwei Architekturentscheidungen:
Die erste ist, einen beliebigen SQL-Server zu verwenden und die erforderliche Fehlertoleranz, Skalierungsmechanismen, ein fehlertolerantes Cluster, Konfliktlösungen sowie verteilte, zuverlÀssige und schnelle ACID-Transaktionen umzusetzen. Wir haben diese Option als recht komplex und arbeitsintensiv eingeschÀtzt.
Die zweite Möglichkeit besteht darin, ein fertiges NoSQL-Speicher-System zu wĂ€hlen, das ĂŒber implementierte Skalierung, ein fehlertolerantes Cluster und Konfliktlösungen verfĂŒgt, wĂ€hrend wir Transaktionen und SQL selbst umsetzen. Auf den ersten Blick scheint die Umsetzung von SQL, ganz zu schweigen von ACID-Transaktionen, eine jahrelange Aufgabe zu sein. Doch dann erkannten wir, dass der Satz von SQL-Funktionen, den wir in der Praxis nutzen, so weit vom ANSI SQL entfernt ist wie weit vom ANSI SQL entfernt ist. Bei nĂ€herer Betrachtung von CQL wurde uns klar, dass es ziemlich nah an dem ist, was wir benötigen.
Cassandra und CQL
Was macht Cassandra so interessant und welche Möglichkeiten bietet sie?
ZunĂ€chst können hier Tabellen mit UnterstĂŒtzung fĂŒr verschiedene Datentypen erstellt werden, und es sind SELECT- oder UPDATE-Abfragen ĂŒber den PrimĂ€rschlĂŒssel möglich.
CREATE TABLE photos (id bigint KEY, owner bigint,âŠ);
SELECT * FROM photos WHERE id=?;
UPDATE photos SET ⊠WHERE id=?;Um die Konsistenz der Replikate zu gewĂ€hrleisten, verwendet Cassandra . Im einfachsten Fall bedeutet dies, dass bei der Bereitstellung von drei Replikaten derselben Zeile auf verschiedenen Knoten des Clusters der Datensatz als erfolgreich betrachtet wird, wenn die Mehrheit der Knoten (d.h. zwei von drei) den Erfolg dieser Schreiboperation bestĂ€tigt hat. Die Daten der Zeile gelten als konsistent, wenn bei der Abfrage die Mehrheit der Knoten befragt wurde und dies bestĂ€tigt haben. So wird bei drei Replikaten vollstĂ€ndige und sofortige Datenkonsistenz im Falle eines Ausfalls eines Knotens garantiert. Dieser Ansatz ermöglicht es uns, ein noch zuverlĂ€ssigeres Schema umzusetzen: immer Anfragen an alle drei Replikate zu senden und auf die Antworten der zwei schnellsten zu warten. Die verspĂ€tete Antwort des dritten Replikats wird in diesem Fall verworfen. Der verspĂ€tet antwortende Knoten könnte ernsthafte Probleme haben â Verzögerungen, Garbage Collection in der JVM, Direct Memory Reclaim im Linux-Kernel, Hardwarefehler, Netzwerkunterbrechungen. Allerdings hat dies keine Auswirkungen auf die Client-Operationen und die Daten.
Der Ansatz, bei dem wir drei Knoten ansprechen und Antworten von zwei erhalten, wird genannt : Anfragen an ĂŒberflĂŒssige Replikate werden gesendet, bevor der Knoten «ausfĂ€llt».
Ein weiterer Vorteil von Cassandra ist der Batchlog â ein Mechanismus, der entweder die vollstĂ€ndige Anwendung oder vollstĂ€ndige Ablehnung eines von Ihnen vorgenommenen Ănderungssets garantiert. Dies ermöglicht es uns, A in ACID â die AtomaritĂ€t â ab Werk zu gewĂ€hrleisten.
Das, was am nĂ€chsten an Transaktionen in Cassandra herankommt, sind die sogenannten ââ. Aber sie sind weit von âechtenâ ACID-Transaktionen entfernt: TatsĂ€chlich handelt es sich um die Möglichkeit, auf Daten von nur einem Datensatz zuzugreifen, unter Verwendung eines Konsens ĂŒber das schwergewichtige Protokoll Paxos. Daher ist die Geschwindigkeit solcher Transaktionen gering.
Was uns in Cassandra gefehlt hat
Also standen wir vor der Herausforderung, echte ACID-Transaktionen in Cassandra zu implementieren. Mit deren Hilfe könnten wir zwei weitere praktische Funktionen klassischer DBMS einfach umsetzen: konsistente schnelle Indizes, die es uns ermöglichen wĂŒrden, Daten nicht nur nach dem PrimĂ€rschlĂŒssel abzurufen, sowie einen ordentlichen Generator fĂŒr monoton steigende Autoinkrement-IDs.
C*One
So entstand ein neues DBMS C*One, das aus drei Typen von Serverknoten besteht:
- Speicher â (fast) standardmĂ€Ăige Cassandra-Server, die fĂŒr die Speicherung von Daten auf lokalen Festplatten verantwortlich sind. Mit zunehmender Last und Datenmenge lĂ€sst sich ihre Anzahl problemlos auf Dutzende oder Hunderte skalieren.
- Transaktionskoordinatoren â sorgen fĂŒr die AusfĂŒhrung von Transaktionen.
- Clients â Anwendungsserver, die GeschĂ€ftsoperationen durchfĂŒhren und Transaktionen initiieren. Solcher Clients kann es Tausende geben.

Server aller Typen sind in einem gemeinsamen Cluster zusammengeschlossen, nutzen das interne Nachrichtenprotokoll von Cassandra zur Kommunikation und fĂŒr den Austausch von Clusterinformationen. Mit Hilfe von Heartbeat erfahren die Server von gegenseitigen AusfĂ€llen, halten ein gemeinsames Datenschema â Tabellen, deren Struktur und Replikation; das Partitionierungsschema, die Cluster-Topologie usw.
Windows XP, Vista, 7, 8, 10, Server 2003, Server 2008, Server 2012

Anstelle von Standardtreibern wird der Modus Fat Client verwendet. Eine solche Node speichert keine Daten, kann jedoch als Koordinator fĂŒr die AusfĂŒhrung von Anfragen fungieren, d. h. der Client ĂŒbernimmt selbst die Funktion des Koordinators seiner Anfragen: er befragt die Replikate des Speichers und löst Konflikte. Dies ist nicht nur zuverlĂ€ssiger und schneller als der Standardtreiber, der eine Kommunikation mit einem entfernten Koordinator erfordert, sondern ermöglicht auch eine bessere Steuerung der AnfrageĂŒbertragung. AuĂerhalb einer offenen Transaktion des Clients werden die Anfragen an die Speicher gesendet. Wenn der Client jedoch eine Transaktion eröffnet hat, werden alle Anfragen innerhalb der Transaktion an den Transaktionskoordinator gesendet.

Transaktionskoordinator C*One
Der Koordinator ist das, was wir fĂŒr C*One von Grund auf neu implementiert haben. Er ist verantwortlich fĂŒr die Verwaltung von Transaktionen, Sperren und Reihenfolge der Anwendungstransaktionen.
FĂŒr jede bearbeitete Transaktion erzeugt der Koordinator einen Zeitstempel: jeder nachfolgende ist gröĂer als der vorhergehende. Da in Cassandra das Konfliktlösungsverfahren auf Zeitstempeln basiert (von zwei konkurrierenden EintrĂ€gen wird der mit dem spĂ€teren Zeitstempel als aktuell betrachtet), wird ein Konflikt immer zugunsten der nachfolgenden Transaktion gelöst. So haben wir realisiert â eine kostengĂŒnstige Lösung zur KonfliktbewĂ€ltigung in verteilten Systemen.
Sperren
Um die Isolation zu gewĂ€hrleisten, haben wir uns fĂŒr die einfachste Methode entschieden â pessimistische Sperren basierend auf dem PrimĂ€rschlĂŒssel des Datensatzes. Das bedeutet, dass in der Transaktion der Datensatz zuerst gesperrt werden muss, bevor er gelesen, modifiziert und gespeichert werden kann. Nur nach einem erfolgreichen Commit kann der Datensatz entsperrt werden, sodass konkurrierende Transaktionen darauf zugreifen können.
Die Umsetzung einer solchen Sperre ist in einer nicht verteilten Umgebung einfach. In einem verteilten System gibt es zwei HauptansÀtze: entweder die Umsetzung einer verteilten Sperre im Cluster oder die Verteilung der Transaktionen so, dass Transaktionen, die einen bestimmten Datensatz betreffen, immer von demselben Koordinator bearbeitet werden.
Da in unserem Fall die Daten bereits auf Gruppen lokaler Transaktionen in SQL verteilt sind, wurde beschlossen, den Koordinatoren der Gruppen lokaler Transaktionen bestimmte Bereiche zuzuweisen: Ein Koordinator bearbeitet alle Transaktionen mit einem Token von 0 bis 9, der zweite von 10 bis 19 und so weiter. Dadurch wird jeder der Koordinatoren zum Master der Transaktionsgruppe.
Damit können Sperren in Form einer einfachen HashMap im Speicher des Koordinators implementiert werden.
AusfÀlle der Koordinatoren
Da ein Koordinator ausschlieĂlich eine Gruppe von Transaktionen bedient, ist es sehr wichtig, schnell zu erkennen, wenn er ausfĂ€llt, damit der erneute Versuch der TransaktionsausfĂŒhrung innerhalb des Timeouts liegt. Um dies schnell und zuverlĂ€ssig zu erreichen, haben wir ein vollverknĂŒpftes Quorum-Hearbeat-Protokoll angewendet:
In jedem Rechenzentrum werden mindestens zwei Koordinator-Knoten untergebracht. RegelmĂ€Ăig sendet jeder Koordinator ein Heartbeat-Signal an die anderen Koordinatoren und informiert sie ĂŒber seinen Status sowie darĂŒber, von welchen Koordinatoren im Cluster er zuletzt Heartbeat-Signale empfangen hat.

Indem er Ă€hnliche Informationen aus den Heartbeat-Signalen der anderen erhĂ€lt, entscheidet jeder Koordinator fĂŒr sich, welche Cluster-Knoten funktionieren und welche nicht, wobei er dem Quorum-Prinzip folgt: Wenn Knoten X von der Mehrheit der Knoten im Cluster die BestĂ€tigung fĂŒr den ordnungsgemĂ€Ăen Empfang von Nachrichten von Knoten Y erhĂ€lt, bedeutet dies, dass Y funktioniert. Umgekehrt gilt, sobald die Mehrheit meldet, dass keine Nachrichten mehr von Knoten Y empfangen wurden, gilt Y als ausgefallen. Interessanterweise wird Knoten X, wenn das Quorum meldet, dass es keine Nachrichten mehr von ihm erhĂ€lt, selbst fĂŒr ausgefallen halten.
Heartbeat-Nachrichten werden in hoher Frequenz gesendet, etwa 20 Mal pro Sekunde, mit einem Intervall von 50 ms. In Java ist es schwierig, eine Anwendungsantwort innerhalb von 50 ms zu garantieren, da die Pausendauer, die durch die Garbage Collection verursacht wird, vergleichbar ist. Es ist uns gelungen, eine derartige Reaktionszeit mit dem G1-Garbage-Collector zu erreichen, der es ermöglicht, ein Ziel fĂŒr die Dauer der GC-Pausen anzugeben. Gelegentlich, recht selten, können jedoch die Pausen des Collectors 50 ms ĂŒberschreiten, was zu einer Fehlinterpretation eines Ausfalls fĂŒhren kann. Um dies zu vermeiden, meldet der Koordinator den Ausfall eines Remote-Knotens nicht, sobald das erste Heartbeat-Nachricht von ihm fehlt, sondern erst wenn mehrere aufeinanderfolgende ausbleiben. So haben wir es geschafft, den Ausfall eines Koordinator-Knotens innerhalb von 200 ms zu erkennen.
Es reicht jedoch nicht aus, schnell zu erkennen, welcher Knoten ausgefallen ist. Da muss etwas unternommen werden.
Redundanz
Das klassische Schema sieht im Falle eines Master-Ausfalls die Wahl eines neuen Masters durch einen der Algorithmen. Allerdings gibt es bei solchen Algorithmen gut bekannte Probleme mit der Konvergenzzeit und der Dauer des Wahlprozesses selbst. Solche zusÀtzlichen Verzögerungen konnten wir durch das Koordinator-Ersatzschema in einem vollstÀndig vernetzten Netzwerk vermeiden:

Angenommen, wir möchten eine Transaktion in der Gruppe 50 durchfĂŒhren. Vorab definieren wir das Ersatzschema, also welche Knoten die Transaktionen der Gruppe 50 im Falle eines Ausfalls des Hauptkoordinators ausfĂŒhren werden. Unser Ziel ist es, die FunktionsfĂ€higkeit des Systems im Falle eines Ausfalls des Rechenzentrums aufrechtzuerhalten. Wir legen fest, dass der erste Ersatz aus einem anderen Rechenzentrum und der zweite Ersatz aus einem dritten Rechenzentrum besteht. Dieses Schema wird einmal festgelegt und bleibt unverĂ€ndert, solange sich die Topologie des Clusters nicht Ă€ndert, also bis neue Knoten hinzukommen (was sehr selten passiert). Die Reihenfolge der Auswahl eines neuen aktiven Masters im Falle eines Ausfalls des alten wird immer so aussehen: der erste Ersatz wird zum aktiven Master, und wenn dieser nicht mehr funktioniert, wird der zweite Ersatz aktiv.
Ein solches Schema ist zuverlÀssiger als ein universeller Algorithmus, da zur Aktivierung eines neuen Masters lediglich der Ausfall des alten festgestellt werden muss.
Wie können die Kunden wissen, welcher der Master gerade arbeitet? In 50 ms ist es unmöglich, Informationen an Tausende von Kunden zu senden. Es kann vorkommen, dass ein Kunde eine Anfrage zur Eröffnung einer Transaktion sendet, ohne zu wissen, dass dieser Master bereits nicht mehr funktioniert, und die Anfrage somit im Timeout stecken bleibt. Um dies zu vermeiden, senden die Kunden spekulativ eine Anfrage zur Eröffnung einer Transaktion gleichzeitig an den Gruppenmaster und beide seine Reserven, aber nur der Master, der gerade aktiv ist, wird auf diese Anfrage antworten. Alle weiteren Kommunikationen im Rahmen der Transaktion erfolgen nur mit dem aktiven Master.
Die Reserve-Master platzieren erhaltene Anfragen fĂŒr nicht eigene Transaktionen in eine Warteschlange der nicht erfĂŒllten Transaktionen, wo sie eine gewisse Zeit gespeichert werden. Wenn der aktive Master ausfĂ€llt, bearbeitet ein neuer Master die Anfragen zur Eröffnung von Transaktionen aus seiner Warteschlange und antwortet dem Kunden. Wenn der Kunde bereits eine Transaktion mit dem alten Master eröffnet hat, wird die zweite Antwort ignoriert (und diese Transaktion wird offensichtlich nicht abgeschlossen und muss vom Kunden erneut durchgefĂŒhrt werden).
Wie funktioniert eine Transaktion
Angenommen, ein Kunde sendet dem Koordinator eine Anfrage zur Eröffnung einer Transaktion fĂŒr eine bestimmte EntitĂ€t mit einem bestimmten PrimĂ€rschlĂŒssel. Der Koordinator sperrt diese EntitĂ€t und platziert sie in einer Speichertabelle fĂŒr Sperren. Wenn nötig, liest der Koordinator diese EntitĂ€t aus dem Speicher und speichert die erhaltenen Daten im Transaktionszustand im Speicher des Koordinators.

Wenn der Kunde die Daten in der Transaktion Ă€ndern möchte, sendet er dem Koordinator eine Anfrage zur Modifikation der EntitĂ€t, und dieser platziert die neuen Daten in der Tabelle fĂŒr TransaktionszustĂ€nde im Speicher. Damit ist die Speicherung abgeschlossen â es erfolgt keine Aufnahme in den Speicher.

Wenn der Kunde wÀhrend einer aktiven Transaktion seine eigenen geÀnderten Daten anfragt, handelt der Koordinator wie folgt:
- Wenn die ID bereits in der Transaktion vorhanden ist, werden die Daten aus dem Speicher entnommen;
- Falls die ID im Speicher nicht vorhanden ist, werden die fehlenden Daten aus den Node-Speichern gelesen, mit den bereits im Speicher vorhandenen kombiniert und das Ergebnis dem Kunden ĂŒbergeben.
Auf diese Weise kann der Kunde seine eigenen Ănderungen lesen, wĂ€hrend andere Kunden diese Ănderungen nicht sehen, da sie nur im Speicher des Koordinators gehalten werden und in den Cassandra-Nodes noch nicht existieren.

Wenn ein Kunde einen Commit sendet, wird der Zustand, der im Dienst gespeichert war, vom Koordinator in einem logged batch gesichert und als logged batch an die Cassandra-Speicher gesendet. Die Speicher fĂŒhren alles Notwendige aus, damit dieses Paket atomar (vollstĂ€ndig) angewendet wird und geben dem Koordinator eine RĂŒckmeldung, der daraufhin die Sperren aufhebt und den Erfolg der Transaktion dem Kunden bestĂ€tigt.

FĂŒr die RĂŒcksetzung muss der Koordinator lediglich den Speicher freigeben, der fĂŒr den Zustand der Transaktion verwendet wurde.
Durch die oben beschriebenen Anpassungen haben wir die ACID-Prinzipien implementiert:
- AtomaritĂ€t. Dies ist eine Garantie dafĂŒr, dass keine Transaktion teilweise im System erfasst wird; es werden entweder alle ihre Unteroperationen ausgefĂŒhrt oder keine. In unserem Fall wird dieses Prinzip durch den logged batch in Cassandra gewĂ€hrleistet.
- Konsistenz. Jede erfolgreiche Transaktion erfasst definitionsgemÀà nur zulĂ€ssige Ergebnisse. Wenn nach dem Ăffnen einer Transaktion und der AusfĂŒhrung einiger Operationen festgestellt wird, dass das Ergebnis nicht zulĂ€ssig ist, wird ein Rollback durchgefĂŒhrt.
- Isolierung. Bei der AusfĂŒhrung einer Transaktion sollten parallele Transaktionen das Ergebnis nicht beeinflussen. WettbewerbsfĂ€hige Transaktionen sind durch pessimistische Sperren auf dem Koordinator isoliert. FĂŒr LesevorgĂ€nge auĂerhalb der Transaktion gilt der Isolationsgrundsatz auf der Ebene Read Committed.
- WiderstandsfĂ€higkeit. UnabhĂ€ngig von Problemen auf unteren Ebenen â Stromausfall, Hardwarefehler â mĂŒssen die Ănderungen, die von einer erfolgreich abgeschlossenen Transaktion vorgenommen wurden, nach dem Wiederanlauf des Systems erhalten bleiben.
Lesen ĂŒber Indizes
Nehmen wir eine einfache Tabelle:
CREATE TABLE photos (
id bigint primary key,
owner bigint,
modified timestamp,
âŠ)Diese Tabelle hat eine ID (PrimĂ€rschlĂŒssel), einen Besitzer und ein Ănderungsdatum. Wir mĂŒssen eine sehr einfache Abfrage durchfĂŒhren â die Daten nach dem Besitzer mit dem Ănderungsdatum âin den letzten 24 Stundenâ abfragen.
SELECT *
WHERE owner=?
AND modified>?Damit eine solche Abfrage schnell ausgefĂŒhrt wird, muss in einer klassischen SQL-Datenbank ein Index ĂŒber die Spalten (owner, modified) erstellt werden. Das können wir ziemlich einfach umsetzen, da wir nun ACID-Garantien haben!
Indizes in C*One
Es gibt eine Ursprungstabelle mit Fotos, in der die ID des Datensatzes der PrimĂ€rschlĂŒssel ist.

FĂŒr den Index C*One wird eine neue Tabelle erstellt, die eine Kopie der Ausgangstabelle darstellt. Der SchlĂŒssel entspricht dem Indexausdruck und umfasst zudem den PrimĂ€rschlĂŒssel des Datensatzes aus der Ausgangstabelle:

Nun kann die Abfrage nach «Besitzer in den letzten 24 Stunden» wie folgt umformuliert werden: select aus einer anderen Tabelle:
SELECT * FROM i1_test
WHERE owner=?
AND modified>?Die Konsistenz der Ausgangstabelle photos und des Index i1 wird automatisch vom Koordinator aufrechterhalten. Basierend auf dem Schema der Daten generiert der Koordinator bei Ănderungen nicht nur das Haupttabelle-Update, sondern merkt sich auch die Ănderungen bei Kopien. Es sind keine zusĂ€tzlichen Aktionen mit der Indextabelle erforderlich, Protokolle werden nicht gelesen, Sperren werden nicht verwendet. Das HinzufĂŒgen von Indizes verbraucht also fast keine Ressourcen und hat praktisch keinen Einfluss auf die Geschwindigkeit der Anwendung von Modifikationen.
Mit ACID haben wir Indizes wie in SQL implementiert. Sie bieten Konsistenz, Skalierbarkeit, hohe Geschwindigkeiten, können zusammengesetzt werden und sind in die CQL-Abfragesprache integriert. FĂŒr die UnterstĂŒtzung der Indizes sind keine Ănderungen am Anwendungscode erforderlich. Alles ist so einfach wie in SQL. Und das Wichtigste ist, dass Indizes die AusfĂŒhrungsgeschwindigkeit der Modifikationen der ursprĂŒnglichen Transaktionstabelle nicht beeinflussen.
Was entstanden ist
Wir haben C*One vor drei Jahren entwickelt und in den produktiven Einsatz gebracht.
Was haben wir letztendlich erreicht? Lassen Sie uns das am Beispiel des Systems zur Verarbeitung und Speicherung von Fotos betrachten, einem der wichtigsten Datentypen in sozialen Netzwerken. Dabei geht es nicht um die Fotos selbst, sondern um die vielfÀltigen Metainformationen. Momentan hat "Odnoklassniki" etwa 20 Milliarden solcher EintrÀge, das System verarbeitet 80.000 Leseanforderungen pro Sekunde und bis zu 8.000 ACID-Transaktionen pro Sekunde, die mit der Datenmodifikation verbunden sind.
Als wir SQL mit einem Replikationsfaktor von 1 verwendeten (aber in RAID 10), wurde die Metainformation der Fotos in einem hochverfĂŒgbaren Cluster aus 32 Maschinen mit Microsoft SQL Server (plus 11 Sicherungsservern) gespeichert. ZusĂ€tzlich wurden 10 Server fĂŒr die Sicherung von Backups bereitgestellt. Insgesamt handelt es sich um 50 kostspielige Maschinen. Dabei arbeitete das System mit einer nominalen Last, ohne Spielraum.
Nach der Migration auf das neue System haben wir einen Replikationsfaktor von 3 erreicht â eine Kopie in jedem Rechenzentrum. Das System besteht aus 63 Knoten des Cassandra-Speichers und 6 Steuerungsmaschinen, insgesamt 69 Server. Diese Maschinen sind jedoch deutlich gĂŒnstiger, ihre Gesamtkosten betragen etwa 30 % der Kosten des SQL-Systems. Die Last liegt konstant bei 30 %.
Mit der EinfĂŒhrung von C*One haben sich auch die Latenzen verringert: Bei SQL benötigte ein Schreibvorgang etwa 4,5 ms. Bei C*One liegt die Zeit bei ungefĂ€hr 1,6 ms. Die Dauer einer Transaktion liegt im Durchschnitt unter 40 ms, der Commit erfolgt in 2 ms, die Lese- und Schreibdauer betrĂ€gt im Durchschnitt 2 ms. Der 99. Perzentil liegt bei gerade einmal 3-3,1 ms, die Anzahl der Timeouts ist um das 100-fache gesenkt worden â alles dank der breiten Anwendung von Spekulationen.
Bis jetzt wurde der GroĂteil der SQL Server-Nodes stillgelegt, neue Produkte werden ausschlieĂlich mit C*One entwickelt. Wir haben C*One fĂŒr den Betrieb in unserer Cloud optimiert. , was die Bereitstellung neuer Cluster beschleunigt, die Konfiguration vereinfacht und den Betrieb automatisiert. Ohne den Quellcode wĂ€re dies deutlich komplizierter und fehleranfĂ€lliger gewesen.
Aktuell arbeiten wir daran, andere Speicher in die Cloud zu migrieren â aber das ist eine ganz andere Geschichte.
Quelle: habr.com
