Hallo zusammen. Hier spricht Владислав Родин. Momentan unterrichte ich auf der Plattform OTUS Kurse zur Softwarearchitektur und zu hochbelastbaren Softwarearchitekturen. Im Vorfeld des Starts des neuen Kurszyklus habe ich beschlossen, ein kurzes eigenes Material zu schreiben, das ich mit Ihnen teilen möchte.

Einführung
Da auf HDDs nur etwa 400-700 Operationen pro Sekunde ausgeführt werden können (was im Vergleich zu den typischen rps, die auf ein hochbelastetes System entfallen, unzureichend ist), stellt die klassische Festplattendatenbank einen Engpass in der Architektur dar. Daher ist es notwendig, besondere Aufmerksamkeit den Skalierungsmustern dieses Speichers zu widmen.
Derzeit gibt es 2 Muster zur Skalierung der Datenbank: Replikation und Sharding. Sharding ermöglicht die Skalierung von Schreiboperationen und reduziert folglich die rps pro Schreiboperation, die auf einen Server Ihres Clusters entfallen. Die Replikation ermöglicht dasselbe für Leseoperationen. Genau diesem Muster widmet sich dieser Artikel.
Replikation
Wenn man die Replikation auf einer ganz grundlegenden Ebene betrachtet, ist es eine einfache Angelegenheit: Sie hatten einen Server, auf dem Daten gespeichert waren, und dann konnte dieser Server die Leseanforderungen nicht mehr bewältigen. Sie fügen ein paar weitere Server hinzu, synchronisieren die Daten auf allen Servern, und der Benutzer kann von jedem Server Ihres Clusters lesen.
Trotz der scheinbaren Einfachheit gibt es mehrere Möglichkeiten, verschiedene Implementierungen dieses Schemas zu klassifizieren:
- Nach Rollen im Cluster (Master-Master oder Master-Slave)
- Nach übermittelten Objekten (zeilenbasiert, anweisungsbasiert oder gemischt)
- Nach dem Mechanismus der Knoten-Synchronisation
Heute werden wir uns besonders mit dem 3. Punkt befassen.
Wie der Transaktions-Commit abläuft
Dieses Thema gehört nicht direkt zur Replikation, könnte aber einen eigenen Artikel füllen. Da jedoch das Verständnis des Mechanismus des Transaktionscommits für das weitere Lesen unerlässlich ist, erlaube ich mir, die grundlegendsten Dinge zu wiederholen. Der Commit einer Transaktion erfolgt in drei Schritten:
- Aufzeichnung der Transaktion im Datenbank-Log.
- Anwendung der Transaktion im Datenbank-Engine.
- Rückmeldung an den Kunden über den erfolgreichen Abschluss der Transaktion.
In verschiedenen Datenbanken können in diesem Algorithmus Nuancen auftreten: Beispielsweise gibt es im InnoDB-Engine von MySQL zwei Journale: eines für die Replikation (binary log) und ein weiteres zur Aufrechterhaltung von ACID (undo/redolog), während PostgreSQL ein einziges Journal hat, das beide Funktionen erfüllt (write-ahead-log = WAL). Die obige Darstellung ist jedoch die allgemeine Konzeption, die solche Nuancen unberücksichtigt lässt.
Synchronisierte (sync) Replikation
Fügen wir dem Commit-Algorithmus der Transaktion die Logik zur Replikation der empfangenen Änderungen hinzu:
- Aufzeichnung der Transaktion im Datenbank-Log.
- Anwendung der Transaktion im Datenbank-Engine.
- Daten an alle Replikate senden.
- Bestätigung von allen Replikaten über die Ausführung der Transaktion erhalten.
- Rückmeldung an den Kunden über den erfolgreichen Abschluss der Transaktion.
Bei diesem Ansatz haben wir einige Nachteile:
- Der Kunde wartet darauf, dass die Änderungen auf allen Replikaten angewendet werden.
- Mit zunehmender Anzahl von Knoten im Cluster verringern wir die Wahrscheinlichkeit, dass die Schreiboperation erfolgreich ist.
Während bei Punkt 1 alles mehr oder weniger klar ist, sollten die Gründe für Punkt 2 erläutert werden. Wenn wir bei der synchronisierten Replikation keine Antwort von mindestens einem Knoten erhalten, führen wir einen Rollback der Transaktion durch. Dadurch erhöht sich mit einer steigenden Anzahl an Knoten im Cluster die Wahrscheinlichkeit, dass die Schreiboperation fehlschlägt.
Können wir eine Bestätigung nur von einem Teil der Knoten erwarten, zum Beispiel von 51% (Quorum)? Ja, das können wir, jedoch erfordert die klassische Variante die Bestätigung von allen Knoten, da wir nur so die vollständige Konsistenz der Daten im Cluster gewährleisten können, was zweifellos ein Vorteil dieser Art der Replication darstellt.
Asynchrone (async) Replikation
Lassen Sie uns den vorherigen Algorithmus anpassen. Die Daten werden an die Replikate »irgendwann später« gesendet, und die Änderungen werden »irgendwann später« in den Replikaten angewendet:
- Aufzeichnung der Transaktion im Datenbank-Log.
- Anwendung der Transaktion im Datenbank-Engine.
- Rückmeldung an den Kunden über den erfolgreichen Abschluss der Transaktion.
- Datenübertragung an Replikate und deren Anwendung der Änderungen.
Dieser Ansatz sorgt dafür, dass der Cluster schnell arbeitet, denn wir halten den Kunden nicht auf, während die Daten zu den Replikaten gelangen und dort auch noch festgeschrieben werden.
Aber die Bedingung, die Daten »irgendwann später« an die Replikate zu übertragen, kann zu einem Verlust von Transaktionen führen, und das betrifft die bestätigte Transaktion des Nutzers, denn wenn die Daten nicht rechtzeitig repliziert werden, und die Bestätigung an den Kunden über den erfolgreichen Abschluss der Operation gesendet wird, und die HDD bei dem Knoten, an den die Änderungen gesendet wurden, ausfällt, verlieren wir die Transaktion, was zu sehr unangenehmen Folgen führen kann.
Halb-synchrone (Semisync) Replikation
Endlich sind wir bei der halb-synchronen Replikation angekommen. Diese Art der Replikation ist eher unbekannt und nicht weit verbreitet, bietet jedoch großes Interesse, da sie die Vorteile sowohl der synchronen als auch der asynchronen Replikation vereinen kann.
Versuchen wir, die beiden vorherigen Ansätze zu kombinieren. Wir halten den Client nicht lange auf, fordern jedoch, dass die Daten repliziert werden:
- Aufzeichnung der Transaktion im Datenbank-Log.
- Anwendung der Transaktion im Datenbank-Engine.
- Daten an die Replikate senden.
- Bestätigung von der Replik für den Empfang der Änderungen erhalten (die Änderungen werden "irgendwann später" angewendet).
- Rückmeldung an den Kunden über den erfolgreichen Abschluss der Transaktion.
Bitte beachten Sie, dass bei diesem Algorithmus der Verlust einer Transaktion nur dann auftritt, wenn sowohl der Knoten, der die Änderungen vornimmt, als auch der Knoten der Replik ausfallen. Die Wahrscheinlichkeit eines solchen Ausfalls wird als gering angesehen, und die damit verbundenen Risiken werden akzeptiert.
Bei diesem Ansatz besteht jedoch das Risiko von Phantomlesungen. Stellen Sie sich folgendes Szenario vor: In Schritt 4 haben wir von keiner der Replikate eine Bestätigung erhalten. Wir müssen diese Transaktion zurückrollen, und dem Kunden keine Bestätigung zurückgeben. Da die Daten in Schritt 2 angewendet wurden, entsteht zwischen dem Ende von Schritt 2 und dem Zurückrollen der Transaktion eine zeitliche Lücke, in der parallele Transaktionen die Änderungen sehen können, die nicht in der Datenbank vorhanden sein sollten.
Verlustfreie semisynchrone Replikation
Wenn man ein wenig darüber nachdenkt, kann man durch einfaches Vertauschen der Schritte im Algorithmus das Problem der Phantomlesungen in diesem Szenario beheben:
- Aufzeichnung der Transaktion im Datenbank-Log.
- Datenübertragung der Replikate.
- Bestätigung von der Replik für den Empfang der Änderungen erhalten (die Änderungen werden "irgendwann später" angewendet).
- Anwendung der Transaktion im Datenbank-Engine.
- Rückmeldung an den Kunden über den erfolgreichen Abschluss der Transaktion.
Jetzt committen wir die Änderungen nur, wenn sie repliziert wurden.
Fazit
Wie immer gibt es keine perfekten Lösungen, sondern eine Reihe von Lösungen, von denen jede ihre eigenen Vor- und Nachteile hat und sich für verschiedene Aufgabenklassen eignet. Das gilt auch für die Wahl des Mechanismus zur Datensynchronisation einer replizierten Datenbank. Die Vorteile der halb-synchronen Replikation sind ziemlich solide und interessant, sodass sie trotz ihrer geringen Verbreitung Beachtung finden sollte.
Das war's. Bis zum nächsten Mal bei !
Quelle: habr.com
