NewSQL = NoSQL+ACID

NewSQL = NoSQL+ACID
Bis vor kurzem wurden in Odnoklassniki etwa 50 TB in Echtzeit verarbeitete Daten in SQL Server gespeichert. Bei einem solchen Volumen ist es nahezu unmöglich, mithilfe eines SQL-DBMS einen schnellen und zuverlässigen und sogar fehlertoleranten Zugriff auf das Rechenzentrum bereitzustellen. Normalerweise wird in solchen Fällen einer der NoSQL-Speicher verwendet, aber nicht alles kann auf NoSQL übertragen werden: Einige Entitäten erfordern ACID-Transaktionsgarantien.

Dies führte uns zum Einsatz von NewSQL-Speicher, also einem DBMS, das Fehlertoleranz, Skalierbarkeit und Leistung von NoSQL-Systemen bietet, gleichzeitig aber die von klassischen Systemen bekannten ACID-Garantien beibehält. Es gibt nur wenige funktionierende Industriesysteme dieser neuen Klasse, daher haben wir selbst ein solches System implementiert und in den kommerziellen Betrieb genommen.

Wie es funktioniert und was passiert ist – lesen Sie unter dem Schnitt.

Heute umfasst das monatliche Publikum von Odnoklassniki mehr als 70 Millionen einzelne Besucher. Wir Kommen Sie in die Top XNUMX Es ist das größte soziale Netzwerk der Welt und gehört zu den zwanzig Websites, auf denen Benutzer die meiste Zeit verbringen. Die „OK“-Infrastruktur bewältigt sehr hohe Lasten: über eine Million HTTP-Anfragen/Sek. pro Front. Teile des Serverparks in Höhe von mehr als 8000 Stück liegen nahe beieinander – in vier Moskauer Rechenzentren, was eine Netzwerkverzögerung von weniger als 1 ms zwischen ihnen ermöglicht.

Wir nutzen Cassandra seit 2010, beginnend mit Version 0.6. Heute sind mehrere Dutzend Cluster in Betrieb. Der schnellste Cluster verarbeitet über 4 Millionen Vorgänge pro Sekunde, während der größte 260 TB speichert.

Dies sind jedoch alles normale NoSQL-Cluster, die zum Speichern verwendet werden schwach koordiniert Daten. Wir wollten auch den wichtigsten konsistenten Speicher, Microsoft SQL Server, ersetzen, der seit der Gründung von Odnoklassniki verwendet wird. Der Speicher bestand aus mehr als 300 SQL Server Standard Edition-Maschinen, die 50 TB Daten – Geschäftseinheiten – enthielten. Diese Daten werden im Rahmen von ACID-Transaktionen geändert und erfordern hohe Konsistenz.

Um Daten auf SQL Server-Knoten zu verteilen, haben wir sowohl vertikale als auch horizontale Methoden verwendet Partitionierung (Scherben). In der Vergangenheit haben wir ein einfaches Daten-Sharding-Schema verwendet: Jede Entität wurde mit einem Token verknüpft – einer Funktion der Entitäts-ID. Entitäten mit demselben Token wurden auf demselben SQL-Server platziert. Die Master-Detail-Beziehung wurde so umgesetzt, dass die Tokens der Master- und Child-Datensätze immer übereinstimmen und auf demselben Server liegen. In einem sozialen Netzwerk werden fast alle Datensätze im Namen des Benutzers generiert, was bedeutet, dass alle Benutzerdaten innerhalb eines funktionalen Subsystems auf einem Server gespeichert werden. Das heißt, an einer Geschäftstransaktion waren fast immer Tabellen eines SQL-Servers beteiligt, was es ermöglichte, die Datenkonsistenz mithilfe lokaler ACID-Transaktionen sicherzustellen, ohne dass diese verwendet werden mussten langsam und unzuverlässig verteilte ACID-Transaktionen.

Dank Sharding und zur Beschleunigung von SQL:

  • Wir verwenden keine Fremdschlüsseleinschränkungen, da sich die Entitäts-ID beim Sharding auf einem anderen Server befinden kann.
  • Aufgrund der zusätzlichen Belastung der DBMS-CPU verwenden wir keine gespeicherten Prozeduren und Trigger.
  • Aus all den oben genannten Gründen und wegen der vielen zufälligen Lesevorgänge von der Festplatte verwenden wir keine JOINs.
  • Außerhalb einer Transaktion verwenden wir die Isolationsstufe „Read Uncommitted“, um Deadlocks zu reduzieren.
  • Wir führen nur kurze Transaktionen aus (durchschnittlich weniger als 100 ms).
  • Aufgrund der großen Anzahl von Deadlocks verwenden wir kein mehrzeiliges UPDATE und DELETE – wir aktualisieren jeweils nur einen Datensatz.
  • Abfragen werden immer nur durch Indizes ausgeführt – eine Abfrage mit einem vollständigen Tabellenscanplan bedeutet für uns eine Überlastung der Datenbank und deren Ausfall.

Diese Schritte ermöglichten es uns, nahezu die maximale Leistung aus SQL-Servern herauszuholen. Allerdings wurden die Probleme immer größer. Werfen wir einen Blick auf sie.

Probleme mit SQL

  • Da wir selbstgeschriebenes Sharding verwendeten, wurde das Hinzufügen neuer Shards manuell von Administratoren durchgeführt. In dieser ganzen Zeit haben skalierbare Datenreplikate keine Anfragen bedient.
  • Wenn die Anzahl der Datensätze in der Tabelle zunimmt, nimmt die Geschwindigkeit des Einfügens und Änderns ab. Beim Hinzufügen von Indizes zu einer vorhandenen Tabelle sinkt die Geschwindigkeit um ein Vielfaches, und die Erstellung und Neuerstellung von Indizes erfordert eine Ausfallzeit.
  • Eine kleine Menge Windows für SQL Server in der Produktion zu haben, erschwert die Infrastrukturverwaltung

Aber das Hauptproblem ist

Fehlertoleranz

Der klassische SQL Server weist eine schlechte Fehlertoleranz auf. Nehmen wir an, Sie haben nur einen Datenbankserver und dieser fällt alle drei Jahre aus. Zu diesem Zeitpunkt ist die Website 20 Minuten lang nicht verfügbar. Dies ist akzeptabel. Wenn Sie 64 Server haben, fällt die Site alle drei Wochen einmal aus. Und wenn Sie 200 Server haben, funktioniert die Site nicht jede Woche. Das ist ein Problem.

Was kann getan werden, um die Fehlertoleranz des SQL-Servers zu verbessern? Wikipedia lädt uns zum Bauen ein hochverfügbarer Cluster: Hier gibt es im Falle eines Ausfalls einer der Komponenten ein Backup.

Dies erfordert eine Flotte teurer Geräte: zahlreiche Duplikate, Glasfaser, gemeinsam genutzter Speicher, und die Einbeziehung der Reserve funktioniert nicht zuverlässig: Etwa 10 % der Einbeziehungen enden im Ausfall des Backup-Knotens mit einem Zug hinter dem Hauptknoten.

Der Hauptnachteil eines solchen hochverfügbaren Clusters besteht jedoch darin, dass er bei einem Ausfall des Rechenzentrums, in dem er sich befindet, nicht verfügbar ist. Odnoklassniki verfügt über vier Rechenzentren, und wir müssen die Arbeit in einem davon im Falle eines Komplettausfalls sicherstellen.

Hierfür könnte man sich bewerben Multi-Master Replikation in SQL Server integriert. Diese Lösung ist aufgrund der Softwarekosten viel teurer und weist bekannte Replikationsprobleme auf – unvorhersehbare Transaktionsverzögerungen bei synchroner Replikation und Verzögerungen bei der Anwendung der Replikation (und infolgedessen verlorene Änderungen) bei asynchroner Replikation. impliziert manuelle Konfliktlösung macht diese Option für uns völlig unanwendbar.

Alle diese Probleme erforderten eine grundlegende Lösung und wir begannen mit der detaillierten Analyse. Hier müssen wir uns mit den grundsätzlichen Funktionen von SQL Server vertraut machen – Transaktionen.

Einfache Transaktion

Betrachten Sie die einfachste Transaktion aus der Sicht eines angewandten SQL-Programmierers: das Hinzufügen eines Fotos zu einem Album. Alben und Fotos werden auf verschiedenen Platten gespeichert. Das Album verfügt über einen öffentlichen Fotoschalter. Dann gliedert sich eine solche Transaktion in folgende Schritte:

  1. Wir blockieren das Album nach Schlüssel.
  2. Erstellen Sie einen Eintrag in der Fototabelle.
  3. Wenn das Foto einen öffentlichen Status hat, schließen wir den Zähler der öffentlichen Fotos im Album, aktualisieren den Datensatz und führen die Transaktion durch.

Oder im Pseudocode:

TX.start("Albums", 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 für eine Geschäftstransaktion darin besteht, Daten aus der Datenbank in den Speicher des Anwendungsservers zu lesen, etwas zu ändern und die neuen Werte wieder in der Datenbank zu speichern. Normalerweise aktualisieren wir bei einer solchen Transaktion mehrere Entitäten, mehrere Tabellen.

Wenn eine Transaktion ausgeführt wird, kann es gleichzeitig zu einer Änderung derselben Daten aus einem anderen System kommen. Antispam kann beispielsweise entscheiden, dass der Benutzer irgendwie verdächtig ist und daher alle Fotos des Benutzers nicht mehr öffentlich sein sollten. Sie müssen zur Moderation gesendet werden, was bedeutet, dass photo.status auf einen anderen Wert geändert und die entsprechenden Zähler deaktiviert werden. Offensichtlich, wenn dieser Vorgang ohne Garantien für die Atomizität der Anwendung und die Isolierung konkurrierender Modifikationen durchgeführt wird, wie in ACID, dann ist das Ergebnis nicht das, was Sie brauchen – entweder zeigt der Fotozähler den falschen Wert an oder es werden nicht alle Fotos zur Moderation gesendet.

Im Laufe der Existenz von Odnoklassniki wurde eine Menge solcher Codes geschrieben, die verschiedene Geschäftseinheiten innerhalb einer einzigen Transaktion manipulieren. Nach den Erfahrungen von Migrationen zu NoSQL mit Eventuelle Konsistenz Wir wissen, dass die größte (und zeitaufwändigste) Herausforderung die Notwendigkeit ist, Code zu entwickeln, um die Datenkonsistenz aufrechtzuerhalten. Daher betrachteten wir die Bereitstellung der Anwendungslogik für echte ACID-Transaktionen als Hauptanforderung für den neuen Speicher.

Weitere ebenso wichtige Anforderungen waren:

  • Bei einem Ausfall des Rechenzentrums muss sowohl Lese- als auch Schreibzugriff auf den neuen Speicher möglich sein.
  • Beibehaltung der aktuellen Entwicklungsgeschwindigkeit. Das heißt, wenn Sie mit einem neuen Repository arbeiten, sollte die Codemenge ungefähr gleich sein, es sollte keine Notwendigkeit bestehen, etwas zum Repository hinzuzufügen, Algorithmen zur Lösung von Konflikten, zur Pflege sekundärer Indizes usw. zu entwickeln.
  • Die Geschwindigkeit des neuen Speichers sollte hoch genug sein, sowohl beim Lesen von Daten als auch bei der Verarbeitung von Transaktionen, was effektiv bedeutete, dass akademisch strenge, universelle, aber langsame Lösungen, wie z Zwei-Phasen-Commits.
  • Automatische Skalierung im laufenden Betrieb.
  • Verwenden Sie gewöhnliche, billige Server, ohne dass Sie exotische Eisenstücke kaufen müssen.
  • Möglichkeit der Speicherentwicklung durch die Entwickler des Unternehmens. Mit anderen Worten: Der Schwerpunkt lag auf proprietären oder Open-Source-Lösungen, vorzugsweise in Java.

Entscheidungen Entscheidungen

Bei der Analyse möglicher Lösungen kamen wir zu zwei möglichen Architekturoptionen:

Die erste besteht darin, einen beliebigen SQL-Server zu nehmen und die erforderliche Fehlertoleranz, den Skalierungsmechanismus, das Failover-Clustering, die Konfliktlösung sowie verteilte, zuverlässige und schnelle ACID-Transaktionen zu implementieren. Wir beurteilten diese Option als äußerst nicht trivial und zeitaufwändig.

Die zweite Möglichkeit besteht darin, einen vorgefertigten NoSQL-Speicher mit implementierter Skalierung, Failover-Clustering und Konfliktlösung zu verwenden und Transaktionen und SQL selbst zu implementieren. Auf den ersten Blick sieht selbst die Aufgabe, SQL zu implementieren, ganz zu schweigen von ACID-Transaktionen, nach einer jahrelangen Aufgabe aus. Aber dann wurde uns klar, dass die SQL-Funktionen, die wir in der Praxis verwenden, so weit von ANSI SQL entfernt sind Kassandra CQL weit entfernt von ANSI SQL. Als wir uns CQL genauer ansahen, stellten wir fest, dass es nah genug an unseren Anforderungen liegt.

Cassandra und CQL

Was ist also das Interessante an Cassandra, welche Funktionen hat es?

Erstens können Sie hier Tabellen mit Unterstützung für verschiedene Datentypen erstellen. Sie können SELECT oder UPDATE nach Primärschlüssel durchführen.

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

Um die Konsistenz der Replikatdaten sicherzustellen, verwendet Cassandra Quorum-Ansatz. Im einfachsten Fall bedeutet dies, dass bei der Platzierung von drei Replikaten derselben Zeile auf verschiedenen Knoten des Clusters der Schreibvorgang dann als erfolgreich gilt, wenn die Mehrheit der Knoten (also zwei von drei) den Erfolg dieses Schreibvorgangs bestätigt haben. Die Daten einer Reihe gelten als konsistent, wenn beim Lesen die meisten Knoten abgefragt und bestätigt wurden. Wenn also drei Replikate vorhanden sind, ist die vollständige und sofortige Datenkonsistenz gewährleistet, wenn ein Knoten ausfällt. Mit diesem Ansatz konnten wir ein noch zuverlässigeres Schema implementieren: Anfragen immer an alle drei Replikate senden und auf eine Antwort der beiden schnellsten warten. Die verspätete Antwort des dritten Replikats wird dann verworfen. Gleichzeitig kann ein Knoten, der mit einer Antwort zu spät kommt, schwerwiegende Probleme haben – Bremsen, Speicherbereinigung in der JVM, direkte Speicherrückgewinnung im Linux-Kernel, Hardwarefehler, Trennung vom Netzwerk. Der Betrieb und die Daten des Kunden werden jedoch in keiner Weise beeinträchtigt.

Der Ansatz, bei dem wir auf drei Knoten zugreifen und von zwei eine Antwort erhalten, wird aufgerufen Spekulation: Eine Anfrage für zusätzliche Replikate wird gesendet, bevor sie „abfällt“.

Ein weiterer Vorteil von Cassandra ist Batchlog, ein Mechanismus, der sicherstellt, dass von Ihnen vorgenommene Änderungen entweder vollständig oder nicht vollständig auf das Paket angewendet werden. Dies ermöglicht es uns, A in ACID zu lösen – Atomizität sofort einsatzbereit.

Den Transaktionen in Cassandra am nächsten kommt das sogenannte „leichte Transaktionen". Aber sie sind weit entfernt von „echten“ ACID-Transaktionen: Tatsächlich handelt es sich hier um eine Gelegenheit CAS auf den Daten nur eines Datensatzes unter Verwendung des Paxos-Schwergewichtsprotokollkonsenses. Daher ist die Geschwindigkeit solcher Transaktionen gering.

Was wir in Cassandra vermisst haben

Wir mussten also echte ACID-Transaktionen in Cassandra implementieren. Mit dessen Hilfe könnten wir problemlos zwei weitere praktische Funktionen des klassischen DBMS implementieren: konsistente schnelle Indizes, die es uns ermöglichen würden, Daten nicht nur anhand des Primärschlüssels auszuwählen, und den üblichen Generator monotoner automatisch inkrementierender IDs.

Kegel

So wurde das neue DBMS geboren Kegel, bestehend aus drei Arten von Serverknoten:

  • Speicher sind (fast) Standard-Cassandra-Server, die für die Speicherung von Daten auf lokalen Laufwerken verantwortlich sind. Wenn die Last und das Datenvolumen wachsen, kann ihre Anzahl problemlos auf Dutzende und Hunderte erhöht werden.
  • Transaktionskoordinatoren – stellen die Ausführung von Transaktionen sicher.
  • Clients sind Anwendungsserver, die Geschäftsvorgänge implementieren und Transaktionen initiieren. Es kann Tausende solcher Kunden geben.

NewSQL = NoSQL+ACID

Server aller Art befinden sich in einem gemeinsamen Cluster und nutzen das interne Nachrichtenprotokoll von Cassandra, um miteinander zu kommunizieren Klatsch für den Austausch von Clusterinformationen. Mithilfe von Heartbeat lernen Server gegenseitige Ausfälle kennen und behalten ein einziges Datenschema bei – Tabellen, deren Struktur und Replikation; Partitionierungsschema, Clustertopologie usw.

Kundschaft

NewSQL = NoSQL+ACID

Anstelle von Standardtreibern wird der Fat-Client-Modus verwendet. Ein solcher Knoten speichert keine Daten, kann aber als Koordinator für die Ausführung von Anforderungen fungieren, d. h. der Client selbst fungiert als Koordinator seiner Anforderungen: Er fragt Speicherreplikate ab und löst Konflikte. Dies ist nicht nur zuverlässiger und schneller als der Standardtreiber, der eine Kommunikation mit einem Remote-Koordinator erfordert, sondern ermöglicht Ihnen auch die Steuerung der Übertragung von Anfragen. Außerhalb einer auf dem Client geöffneten Transaktion werden Anforderungen an Speicher gesendet. Wenn der Kunde eine Transaktion eröffnet hat, werden alle Anfragen innerhalb der Transaktion an den Transaktionskoordinator gesendet.
NewSQL = NoSQL+ACID

C*One-Transaktionskoordinator

Der Koordinator ist das, was wir für C*One von Grund auf implementiert haben. Es ist für die Verwaltung von Transaktionen, Sperren und der Reihenfolge, in der Transaktionen angewendet werden, verantwortlich.

Für jede bediente Transaktion generiert der Koordinator einen Zeitstempel: Jeder nachfolgende ist größer als der der vorherigen Transaktion. Da das Konfliktlösungssystem in Cassandra auf Zeitstempeln basiert (von zwei widersprüchlichen Datensätzen wird der neueste Zeitstempel als relevant angesehen), wird der Konflikt immer zugunsten der nachfolgenden Transaktion gelöst. So haben wir es umgesetzt Lamport-Uhr ist eine kostengünstige Möglichkeit, Konflikte in einem verteilten System zu lösen.

Schlösser

Um die Isolation zu gewährleisten, haben wir uns für den einfachsten Weg entschieden – pessimistische Sperren für den Primärschlüssel des Datensatzes. Mit anderen Worten: Bei einer Transaktion muss ein Datensatz zunächst gesperrt, dann erst gelesen, geändert und gespeichert werden. Erst nach einem erfolgreichen Commit kann ein Datensatz entsperrt werden, sodass konkurrierende Transaktionen ihn verwenden können.

Die Implementierung einer solchen Sperre ist in einer nicht verteilten Umgebung einfach. In einem verteilten System gibt es im Wesentlichen zwei Möglichkeiten: entweder eine verteilte Sperrung auf einem Cluster implementieren oder Transaktionen so verteilen, dass Transaktionen, die denselben Datensatz betreffen, immer von demselben Koordinator bedient werden.

Da in unserem Fall die Daten in SQL bereits auf Gruppen lokaler Transaktionen verteilt sind, wurde beschlossen, den Koordinatoren Gruppen lokaler Transaktionen zuzuweisen: Ein Koordinator führt alle Transaktionen mit einem Token von 0 bis 9 aus, der zweite – mit einem Token von 10 bis 19 und so weiter. Dadurch wird jede Instanz des Koordinators zum Master der Transaktionsgruppe.

Dann können Sperren als banale HashMap im Speicher des Koordinators implementiert werden.

Ablehnungen von Koordinatoren

Da ein Koordinator ausschließlich eine Gruppe von Transaktionen bedient, ist es sehr wichtig, die Tatsache seines Scheiterns schnell festzustellen, damit der wiederholte Versuch, die Transaktion auszuführen, innerhalb des Timeouts liegt. Um dies schnell und zuverlässig zu machen, haben wir ein vollständig vermaschtes Quorum-Hearbeat-Protokoll verwendet:

Jedes Rechenzentrum beherbergt mindestens zwei Koordinatorknoten. In regelmäßigen Abständen sendet jeder Koordinator eine Heartbeat-Nachricht an die anderen Koordinatoren und informiert sie über seine Funktionsweise sowie darüber, wann er zuletzt Heartbeat-Nachrichten von welchen Koordinatoren im Cluster erhalten hat.

NewSQL = NoSQL+ACID

Jeder Koordinator erhält im Rahmen seiner Heartbeat-Nachrichten ähnliche Informationen von den anderen und entscheidet selbst, welche Knoten des Clusters funktionieren und welche nicht. Dabei orientiert er sich am Quorum-Prinzip: Wenn Knoten X Informationen von der Mehrheit der Knoten im Cluster erhalten hat der normale Empfang von Nachrichten vom Knoten Y, dann funktioniert Y. Umgekehrt gilt: Sobald die Mehrheit fehlende Nachrichten von Knoten Y meldet, ist Y ausgefallen. Wenn das Quorum dem Knoten X mitteilt, dass er keine Nachrichten mehr von ihm empfängt, dann betrachtet sich Knoten

Heartbeat-Nachrichten werden mit hoher Frequenz, etwa 20 Mal pro Sekunde, mit einer Periode von 50 ms gesendet. In Java ist es aufgrund vergleichbarer Pausenzeiten, die durch den Garbage Collector verursacht werden, schwierig, eine Anwendungsantwort innerhalb von 50 ms zu garantieren. Diese Reaktionszeit konnten wir mithilfe des G1 Garbage Collectors erreichen, der es ermöglicht, ein Ziel für die Dauer von GC-Pausen festzulegen. Manchmal, aber recht selten, überschreiten die Kollektorpausen mehr als 50 ms, was zu einer falschen Fehlererkennung führen kann. Um dies zu vermeiden, meldet der Koordinator den Ausfall des Remote-Knotens nicht, wenn die erste Heartbeat-Nachricht von ihm verloren geht, sondern nur dann, wenn mehrere in einer Reihe fehlen. So ist es uns gelungen, den Ausfall des Koordinatorknotens innerhalb von 200 ms zu erkennen.

Es reicht jedoch nicht aus, schnell zu verstehen, welcher Knoten nicht mehr funktioniert. Es muss etwas dagegen getan werden.

Reservierung

Das klassische Schema geht davon aus, dass im Falle eines Scheiterns des Masters die Wahl eines neuen mithilfe eines der Master-Systeme gestartet werden kann modisch universell Algorithmen. Allerdings haben solche Algorithmen bekannte Probleme mit der zeitlichen Konvergenz und der Dauer des Wahlprozesses selbst. Solche zusätzlichen Verzögerungen konnten wir mithilfe des Koordinator-Austauschschemas in einem vollständig verbundenen Netzwerk vermeiden:

NewSQL = NoSQL+ACID

Nehmen wir an, wir möchten eine Transaktion in Gruppe 50 ausführen. Definieren wir im Voraus ein Ersatzschema, das heißt, welche Knoten Transaktionen der Gruppe 50 ausführen, falls der Hauptkoordinator ausfällt. Unser Ziel ist es, das System auch bei einem Ausfall des Rechenzentrums betriebsbereit zu halten. Definieren wir, dass die erste Reserve ein Knoten aus einem anderen Rechenzentrum und die zweite Reserve ein Knoten aus dem dritten sein wird. Dieses Schema wird einmal ausgewählt und ändert sich erst, wenn sich die Topologie des Clusters ändert, d. h. bis neue Knoten in ihn eintreten (was sehr selten vorkommt). Die Reihenfolge der Auswahl eines neuen aktiven Masters im Falle eines Ausfalls des alten wird immer wie folgt sein: Die erste Reserve wird zum aktiven Master, und wenn sie nicht mehr funktioniert, wird die zweite Reserve.

Ein solches Schema ist zuverlässiger als ein universeller Algorithmus, da es zur Aktivierung eines neuen Masters ausreicht, den Ausfall des alten Masters festzustellen.

Doch wie sollen Kunden verstehen, welcher der Master gerade arbeitet? Es ist unmöglich, Informationen innerhalb von 50 ms an Tausende von Clients zu senden. Es ist möglich, dass ein Client eine Anfrage zum Öffnen einer Transaktion sendet, ohne zu wissen, dass dieser Master nicht mehr funktioniert, und die Anfrage aufgrund einer Zeitüberschreitung hängen bleibt. Um dies zu verhindern, senden Kunden spekulativ eine Anfrage zur Eröffnung einer Transaktion gleichzeitig an den Master der Gruppe und beide ihrer Reserven, aber nur derjenige, der derzeit der aktive Master ist, wird auf diese Anfrage antworten. Die gesamte nachfolgende Kommunikation innerhalb der Transaktion wird vom Client nur mit dem aktiven Master durchgeführt.

Die Standby-Master stellen die empfangenen Anfragen für Transaktionen, die nicht ihre eigenen sind, in die Warteschlange für ungeborene Transaktionen, wo sie für einige Zeit gespeichert werden. Wenn der aktive Master stirbt, verarbeitet der neue Master Anfragen zum Öffnen von Transaktionen aus seiner Warteschlange und antwortet dem Client. Wenn es dem Client bereits gelungen ist, eine Transaktion mit dem alten Master zu eröffnen, wird die zweite Antwort ignoriert (und natürlich wird eine solche Transaktion nicht abgeschlossen und vom Client wiederholt).

So funktioniert eine Transaktion

Angenommen, der Client hat eine Anfrage an den Koordinator gesendet, um eine Transaktion für diese oder jene Entität mit diesem und jenem Primärschlüssel zu eröffnen. Der Koordinator sperrt diese Entität und platziert sie in der Sperrtabelle im Speicher. Bei Bedarf liest der Koordinator diese Entität aus dem Speicher und speichert die empfangenen Daten in einem Transaktionszustand im Speicher des Koordinators.

NewSQL = NoSQL+ACID

Wenn ein Client die Daten in einer Transaktion ändern möchte, sendet er eine Anfrage an den Koordinator, um die Entität zu ändern, und der Koordinator platziert die neuen Daten in der Transaktionsstatustabelle im Speicher. Damit ist die Aufzeichnung abgeschlossen – der Speicher wird nicht beschrieben.

NewSQL = NoSQL+ACID

Wenn ein Client im Rahmen einer aktiven Transaktion seine eigenen geänderten Daten anfordert, verhält sich der Koordinator folgendermaßen:

  • Wenn die ID bereits in der Transaktion enthalten ist, werden die Daten aus dem Speicher entnommen.
  • Wenn keine ID im Speicher vorhanden ist, werden die fehlenden Daten aus den Speicherknoten gelesen, mit denen bereits im Speicher kombiniert und das Ergebnis an den Client zurückgegeben.

Somit kann der Client seine eigenen Änderungen lesen, andere Clients sehen diese Änderungen jedoch nicht, da sie nur im Speicher des Koordinators gespeichert sind und sich noch nicht in den Cassandra-Knoten befinden.

NewSQL = NoSQL+ACID

Wenn der Client einen Commit sendet, wird der Status, den der Dienst im Speicher hatte, vom Koordinator in einem protokollierten Batch gespeichert und als protokollierter Batch an die Cassandra-Speicher gesendet. Die Repositorys unternehmen alles Notwendige, damit dieses Paket atomar (vollständig) angewendet wird, und senden eine Antwort an den Koordinator zurück, der die Sperren aufhebt und dem Client den Erfolg der Transaktion bestätigt.

NewSQL = NoSQL+ACID

Und für ein Rollback muss der Koordinator nur den Speicher freigeben, der durch den Status der Transaktion belegt ist.

Als Ergebnis der oben beschriebenen Verbesserungen haben wir die Prinzipien von ACID umgesetzt:

  • Atomarität. Dies ist eine Garantie dafür, dass keine Transaktion teilweise im System fixiert wird, entweder alle ihre Unteroperationen ausgeführt werden oder keine davon ausgeführt wird. In unserem Fall wird dieses Prinzip aufgrund der protokollierten Charge in Cassandra eingehalten.
  • Konsistenz. Jede erfolgreiche Transaktion übermittelt per Definition nur gültige Ergebnisse. Wenn nach dem Öffnen einer Transaktion und der Durchführung einiger Vorgänge festgestellt wird, dass das Ergebnis ungültig ist, wird ein Rollback durchgeführt.
  • Isolierung. Wenn eine Transaktion ausgeführt wird, sollten parallele Transaktionen keinen Einfluss auf das Ergebnis haben. Gleichzeitige Transaktionen werden durch pessimistische Sperren auf dem Koordinator isoliert. Bei Lesevorgängen außerhalb einer Transaktion wird das Prinzip der Isolation auf der Read Committed-Ebene beachtet.
  • Stabilität. Unabhängig von Problemen auf den unteren Ebenen – einem Systemausfall, einem Hardwarefehler – sollten die durch eine erfolgreich abgeschlossene Transaktion vorgenommenen Änderungen auch nach der Wiederaufnahme der Funktion gespeichert bleiben.

Lesen nach Indizes

Nehmen wir eine einfache Tabelle:

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

Es verfügt über eine ID (Primärschlüssel), einen Besitzer und ein Änderungsdatum. Sie müssen eine ganz einfache Anfrage stellen: Wählen Sie die Daten des Eigentümers mit dem Änderungsdatum „für den letzten Tag“ aus.

SELECT *
WHERE owner=?
AND modified>?

Um eine solche Abfrage schnell verarbeiten zu können, müssen Sie in einem klassischen SQL-DBMS einen Index für die Spalten (Eigentümer, geändert) erstellen. Wir können dies ganz einfach tun, da wir jetzt ACID-Garantien haben!

Indizes in C*One

Es gibt eine anfängliche Tabelle mit Fotos, in denen die Datensatz-ID ein Primärschlüssel ist.

NewSQL = NoSQL+ACID

Für einen Index erstellt C*One eine neue Tabelle, die eine Kopie der Originaltabelle ist. Der Schlüssel ist derselbe wie der Indexausdruck, enthält jedoch zusätzlich den Primärschlüssel des Datensatzes aus der Quelltabelle:

NewSQL = NoSQL+ACID

Jetzt kann die Abfrage nach „Besitzer in den letzten XNUMX Stunden“ als Auswahl aus einer anderen Tabelle umgeschrieben werden:

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

Die Datenkonsistenz zwischen den Fotos der Quelltabelle und dem Index i1 wird vom Koordinator automatisch aufrechterhalten. Allein auf der Grundlage des Datenschemas generiert und speichert der Koordinator beim Empfang einer Änderung nicht nur die Änderung der Haupttabelle, sondern auch die Änderungen der Kopien. Es werden keine weiteren Aktionen mit der Indextabelle durchgeführt, Protokolle werden nicht gelesen, Sperren werden nicht verwendet. Das heißt, das Hinzufügen von Indizes verbraucht nahezu keine Ressourcen und hat praktisch keinen Einfluss auf die Geschwindigkeit der Änderungsanwendung.

Mit Hilfe von ACID ist es uns gelungen, Indizes „wie in SQL“ zu implementieren. Sie sind konsistent, skalierbar, schnell, zusammensetzbar und in die CQL-Abfragesprache integriert. Für die Indexunterstützung sind keine Änderungen am Anwendungscode erforderlich. Alles ist einfach, wie in SQL. Und was am wichtigsten ist: Indizes haben keinen Einfluss auf die Ausführungsgeschwindigkeit von Änderungen an der ursprünglichen Transaktionstabelle.

Was ist passiert

Wir haben C*One vor drei Jahren entwickelt und in den kommerziellen Betrieb genommen.

Was haben wir am Ende herausgefunden? Bewerten wir dies am Beispiel eines Subsystems zur Verarbeitung und Speicherung von Fotos, einer der wichtigsten Datenarten in einem sozialen Netzwerk. Dabei geht es nicht um die Körper der Fotografien selbst, sondern um alle Arten von Metainformationen. Mittlerweile gibt es in Odnoklassniki etwa 20 Milliarden solcher Datensätze, das System verarbeitet 80 Leseanfragen pro Sekunde, bis zu 8 ACID-Transaktionen pro Sekunde im Zusammenhang mit Datenänderungen.

Als wir SQL mit Replikationsfaktor = 1 (jedoch in RAID 10) verwendeten, wurden die Foto-Metainformationen auf einem hochverfügbaren Cluster von 32 Microsoft SQL Server-Maschinen (plus 11 Ersatzmaschinen) gespeichert. Außerdem wurden 10 Server zum Speichern von Backups zugewiesen. Insgesamt 50 teure Autos. Gleichzeitig arbeitete das System ohne Spielraum unter Nennlast.

Nach der Migration auf ein neues System erhielten wir einen Replikationsfaktor = 3 – eine Kopie in jedem Rechenzentrum. Das System besteht aus 63 Cassandra-Speicherknoten und 6 Koordinatormaschinen, also insgesamt 69 Servern. Diese Maschinen sind jedoch viel günstiger und kosten insgesamt etwa 30 % der Kosten eines SQL-Systems. In diesem Fall wird die Last auf dem Niveau von 30 % gehalten.

Mit der Einführung von C*One verringerte sich auch die Latenz: In SQL dauerte ein Schreibvorgang etwa 4,5 ms. In C * One - etwa 1,6 ms. Die Dauer einer Transaktion beträgt durchschnittlich weniger als 40 ms, der Commit ist in 2 ms abgeschlossen, die Lese- und Schreibdauer beträgt durchschnittlich 2 ms. Das 99. Perzentil beträgt nur 3–3,1 ms, die Anzahl der Timeouts hat sich um das Hundertfache verringert – alles aufgrund der weit verbreiteten Verwendung von Spekulationen.

Bisher wurden die meisten SQL Server-Knoten außer Betrieb genommen, neue Produkte werden nur noch mit C*One entwickelt. Wir haben C*One so angepasst, dass es in unserer Cloud funktioniert eine Wolke, was es ermöglichte, die Bereitstellung neuer Cluster zu beschleunigen, die Konfiguration zu vereinfachen und den Betrieb zu automatisieren. Ohne den Quellcode wäre dies viel schwieriger und abgedroschen.

Jetzt arbeiten wir daran, unsere anderen Speicher in die Cloud zu verlagern – aber das ist eine ganz andere Geschichte.

Source: habr.com

Kommentar hinzufügen