Hallo iedereen. Dit is Vladislav Rodin. Momenteel geef ik cursussen op het OTUS-platform over software-architectuur en software-architectuur die onder hoge belasting staat. In de aanloop naar de start van de nieuwe ronde van de cursus heb ik besloten om een klein origineel stuk te schrijven dat ik graag met jullie wil delen.

Inleiding
Omdat een HDD slechts ongeveer 400-700 bewerkingen per seconde kan uitvoeren (wat niet te vergelijken is met de typische rps'en die op een hoogbelaste systeem komen), is een traditionele relationele database een bottleneck in de architectuur. Daarom moet er speciale aandacht worden besteed aan de schalingpatronen van deze opslag.
Momenteel zijn er 2 schalingpatronen voor databases: replicatie en sharding. Sharding maakt het mogelijk om de schrijfbewerking te schalen en, als gevolg daarvan, de rps op schrijven die op ƩƩn server van uw cluster komt te verminderen. Replicatie maakt hetzelfde mogelijk, maar dan voor leesbewerkingen. Dit patroon is waar dit artikel aan gewijd is.
Replicatie
Als we naar replicatie kijken vanuit een heel hoog niveau, is het een simpel concept: je had ƩƩn server waarop de gegevens waren opgeslagen, en toen kon die server de leeslast van deze gegevens niet meer aan. Je voegt nog een paar servers toe, synchroniseert de gegevens op alle servers, en de gebruiker kan van elke server van uw cluster lezen.
Ondanks de schijnbare eenvoud zijn er verschillende manieren om de verschillende implementaties van dit schema te classificeren:
- Op rollen in de cluster (master-master of master-slave)
- Op verzonden objecten (row-based, statement-based of mixed)
- Volgens het synchronisatiemechanisme van de nodes
Vandaag zullen we ons richten op punt 3.
Hoe de transactie wordt gecommiteerd
Dit onderwerp is niet direct gerelateerd aan replicatie, hier kan een apart artikel over worden geschreven, maar aangezien het begrijpen van het mechanisme van de transactietoewijzing verder lezen nutteloos maakt, laat ik mezelf de belangrijkste zaken herinneren. De commit van een transactie gebeurt in 3 stappen:
- Het schrijven van de transactie naar het databasejournal.
- Het toepassen van de transactie in de database-engine.
- Het terugsturen van de bevestiging aan de cliƫnt dat de transactie succesvol is toegepast.
In verschillende databases kunnen er nuances optreden in dit algoritme: bijvoorbeeld, in de InnoDB-engine van MySQL zijn er 2 logs: ƩƩn voor replicatie (binary log) en de andere voor het behouden van ACID (undo/redolog), terwijl PostgreSQL ƩƩn log heeft die beide functies vervult (write ahead log = WAL). Maar hierboven is de algemene conceptie gepresenteerd die dergelijke nuances niet in overweging neemt.
Synchronisatie (sync) replicatie
Laten we de logica voor het repliceren van de ontvangen wijzigingen toevoegen aan het commit-algoritme van de transactie:
- Het schrijven van de transactie naar het databasejournal.
- Het toepassen van de transactie in de database-engine.
- Gegevens verzenden naar alle replicas.
- Bevestiging ontvangen van alle replicas over de uitvoering van de transactie.
- Het terugsturen van de bevestiging aan de cliƫnt dat de transactie succesvol is toegepast.
Bij deze aanpak ondervinden we een aantal nadelen:
- de client wacht op de toepassing van wijzigingen op alle replicas.
- met de toename van het aantal knooppunten in het cluster verminderen we de kans dat de schrijfoperatie succesvol is.
Als het eerste punt meer of minder duidelijk is, dan is het de moeite waard om de redenen voor het tweede punt toe te lichten. Als we bij synchronisatie replicatie geen antwoord van ten minste ƩƩn knooppunt ontvangen, annulleren we de transactie. Hierdoor vergroot u, door het aantal knooppunten in het cluster te verhogen, de kans dat de schrijfoperatie mislukt.
Kunnen we bevestiging verwachten van slechts een deel van de knooppunten, bijvoorbeeld van 51% (quorum)? Ja, dat kan, maar in de klassieke variant is bevestiging van alle knooppunten nodig, want zo kunnen we volledige consistentie van de gegevens in het cluster waarborgen, wat een onmiskenbaar voordeel van dit soort replicatie is.
Asynchrone (async) replicatie
Laten we het vorige algoritme aanpassen. We zullen de gegevens naar de replicas verzenden 'op een later moment', en 'op een later moment' zullen de wijzigingen op de replicas worden toegepast:
- Het schrijven van de transactie naar het databasejournal.
- Het toepassen van de transactie in de database-engine.
- Het terugsturen van de bevestiging aan de cliƫnt dat de transactie succesvol is toegepast.
- Gegevens verzenden naar de replicas en het toepassen van wijzigingen door hen.
Deze aanpak zorgt ervoor dat het cluster snel werkt, omdat we de client niet laten wachten totdat de gegevens naar de replicas zijn verzonden en bovendien zijn gecommit.
Maar de voorwaarde om de gegevens naar de replicas 'op een later moment' te verzenden kan leiden tot verlies van transactie, en dit betreft het verlies van een bevestigde transactie aan de gebruiker, want als de gegevens niet tijdig zijn gerepliceerd, wordt de client bevestiging gestuurd over het succesvol uitvoeren van de operatie, terwijl de knoop waar de wijzigingen zijn aangekomen, een HDD-fout heeft, verliezen we de transactie, wat zeer onaangename gevolgen kan hebben.
Halve synchronisatie (semisync) replicatie
Eindelijk zijn we aangekomen bij half-synchrone replicatie. Dit type replicatie is niet erg bekend en niet veelvoorkomend, maar het is heel interessant omdat het de voordelen van zowel synchrone als asynchrone replicatie kan combineren.
Laten we proberen de twee eerdere benaderingen te combineren. We zullen de cliƫnt niet lang vasthouden, maar we eisen dat de gegevens gerepliceerd worden:
- Het schrijven van de transactie naar het databasejournal.
- Het toepassen van de transactie in de database-engine.
- Verzending van gegevens naar de replica's.
- Ontvangstbevestiging van de replica over het ontvangen van wijzigingen (of ze zullen 'een keer later' toegepast worden).
- Het terugsturen van de bevestiging aan de cliƫnt dat de transactie succesvol is toegepast.
Let op dat bij dit algoritme het verlies van een transactie slechts optreedt in het geval van een storing van zowel de node die de wijzigingen aanneemt als de replica-node. De kans op zo'n storing wordt als klein beschouwd en deze risico's worden aanvaard.
Bij deze benadering bestaat het risico van phantom reads. Stel je het volgende scenario voor: in stap 4 hebben we van geen enkele replica een bevestiging ontvangen. We moeten deze transactie terugdraaien en de cliƫnt geen bevestiging geven. Aangezien de gegevens in stap 2 zijn toegepast, ontstaat er een tijdsperiode tussen het einde van stap 2 en het terugdraaien van de transactie, waarin parallelle transacties die wijzigingen kunnen zien die niet in de database zouden moeten zijn.
Lose-less semisync replicatie
Als we even nadenken, kunnen we simpelweg de stappen van het algoritme omwisselen om het probleem van phantom reads in dit scenario op te lossen:
- Het schrijven van de transactie naar het databasejournal.
- Verzending van gegevens naar de replica.
- Ontvangstbevestiging van de replica over het ontvangen van wijzigingen (of ze zullen 'een keer later' toegepast worden).
- Het toepassen van de transactie in de database-engine.
- Het terugsturen van de bevestiging aan de cliƫnt dat de transactie succesvol is toegepast.
Nu committeren we de wijzigingen alleen als ze gerepliceerd zijn.
Uitslag
Zoals altijd zijn er geen ideale oplossingen, maar een verzameling oplossingen, elk met zijn eigen voor- en nadelen en geschikt voor verschillende klassen van problemen. Dit is ook zeker waar voor het kiezen van een mechanisme voor gegevenssynchronisatie in een gerepliceerde database. De voordelen van half-synchrone replicatie zijn vrij aanzienlijk en interessant genoeg om het aandacht waard te maken, ondanks de geringe verspreiding.
Dit is het. Tot ziens bij !
Bron: habr.com
