paint-brush
Betrouwbare berichten in gedistribueerde systemendoor@fairday
37,190 lezingen
37,190 lezingen

Betrouwbare berichten in gedistribueerde systemen

door Aleksei8m2024/03/18
Read on Terminal Reader
Read this story w/o Javascript

Te lang; Lezen

Voor het bouwen van een betrouwbaar, zeer beschikbaar en schaalbaar gedistribueerd systeem is het nodig om specifieke technieken, principes en patronen te hanteren.
featured image - Betrouwbare berichten in gedistribueerde systemen
Aleksei HackerNoon profile picture

Dubbel schrijfprobleem

Het bouwen van een betrouwbaar, zeer beschikbaar, schaalbaar gedistribueerd systeem vereist naleving van specifieke technieken, principes en patronen. Het ontwerp van dergelijke systemen omvat het aanpakken van een veelvoud aan uitdagingen. Een van de meest voorkomende en fundamentele problemen is het dual write-probleem .


Het "dual write problem" is een uitdaging die zich voordoet in gedistribueerde systemen, voornamelijk bij het omgaan met meerdere gegevensbronnen of databases die gesynchroniseerd moeten worden. Het verwijst naar de moeilijkheid om ervoor te zorgen dat gegevenswijzigingen consistent worden weggeschreven naar verschillende gegevensopslagplaatsen, zoals databases of caches, zonder problemen te introduceren zoals gegevensinconsistenties, conflicten of prestatieknelpunten.


De microservicesarchitectuur en patroondatabase per service bieden u veel voordelen, zoals onafhankelijke implementatie en schaling, geïsoleerde storingen en een potentiële boost van de ontwikkelingssnelheid. Echter, bewerkingen vereisen wijzigingen tussen meerdere microservices, waardoor u gedwongen wordt om na te denken over een betrouwbare oplossing om dit probleem aan te pakken.

Bijna een echt voorbeeld

Laten we eens een scenario bekijken waarin ons domein bestaat uit het accepteren van kredietaanvragen, het beoordelen ervan en het versturen van meldingen naar klanten.


In de geest van het principe van de individuele verantwoordelijkheid, de wet van Conway en de domeingestuurde ontwerpbenadering werd het hele domein na verschillende eventstorming-sessies opgesplitst in drie subdomeinen met gedefinieerde, begrensde contexten met duidelijke grenzen, domeinmodellen en alomtegenwoordige taal.


De eerste is belast met het onboarden en samenstellen van nieuwe leningaanvragen. Het tweede systeem evalueert deze aanvragen en neemt beslissingen op basis van de verstrekte gegevens. Dit beoordelingsproces, inclusief KYC/KYB, antifraude en kredietrisicocontroles, kan tijdrovend zijn, waardoor het nodig is om duizenden aanvragen tegelijkertijd te verwerken. Daarom is deze functionaliteit gedelegeerd aan een speciale microservice met een eigen database, wat onafhankelijke schaalbaarheid mogelijk maakt.

Bovendien worden deze subsystemen beheerd door twee verschillende teams, elk met zijn eigen releasecycli, service level agreements (SLA) en schaalbaarheidsvereisten.


Ten slotte is er een gespecialiseerde meldingsservice om klanten te waarschuwen.



Hier is een verfijnde beschrijving van het primaire gebruiksscenario van het systeem:

  1. Een klant dient een kredietaanvraag in.
  2. De Kredietaanvraagdienst registreert de nieuwe aanvraag met de status 'In behandeling' en start het beoordelingsproces door de aanvraag door te sturen naar de Beoordelingsdienst.
  3. De Beoordelingsdienst beoordeelt de binnenkomende kredietaanvraag en informeert de Kredietaanvraagdienst vervolgens over het besluit.
  4. Zodra de Kredietaanvraagservice de beslissing heeft ontvangen, past deze de status van de kredietaanvraag aan en activeert de Meldingsservice om de klant op de hoogte te stellen van de uitkomst.
  5. De Meldingsservice verwerkt dit verzoek en verstuurt meldingen naar de klant via e-mail, sms of andere gewenste communicatiemethoden, afhankelijk van de instellingen van de klant.


Op het eerste gezicht lijkt het een vrij eenvoudig en primitief systeem, maar laten we eens kijken hoe de kredietaanvraagservice de opdracht tot het indienen van een kredietaanvraag verwerkt.


We kunnen twee benaderingen voor service-interacties overwegen:

  1. Eerst-lokale-commit-dan-publiceren: bij deze aanpak werkt de service zijn lokale database bij (commits) en publiceert vervolgens een gebeurtenis of bericht naar andere services.

  2. Eerst publiceren en dan lokaal vastleggen: Bij deze methode wordt een gebeurtenis of bericht gepubliceerd voordat de wijzigingen in de lokale database worden vastgelegd.


Beide methoden hebben hun nadelen en zijn slechts gedeeltelijk veilig voor communicatie in gedistribueerde systemen.


Dit is een sequentiediagram van de toepassing van de eerste aanpak.


Eerst lokaal committen en dan publiceren


In dit scenario gebruikt de Loan Application Service de First-Local-Commit-Then-Publish- benadering, waarbij eerst een transactie wordt vastgelegd en vervolgens wordt geprobeerd een melding naar een ander systeem te sturen. Dit proces is echter vatbaar voor fouten als er bijvoorbeeld netwerkproblemen zijn, de Assessment Service niet beschikbaar is of de Loan Application Service een Out of Memory (OOM)-fout tegenkomt en vastloopt. In dergelijke gevallen zou het bericht verloren gaan, waardoor de Assessment geen melding krijgt van de nieuwe leningaanvraag, tenzij er aanvullende maatregelen worden geïmplementeerd.


En de tweede.

Eerst publiceren en dan lokaal committen
In het First-Publish-Then-Local-Commit- scenario loopt de Loan Application Service grotere risico's. Het kan de Assessment Service informeren over een nieuwe aanvraag, maar deze update lokaal niet opslaan vanwege problemen zoals databaseproblemen, geheugenfouten of codebugs. Deze aanpak kan leiden tot aanzienlijke inconsistenties in gegevens, wat ernstige problemen kan veroorzaken, afhankelijk van hoe de Loan Review Service inkomende aanvragen verwerkt.


Daarom moeten we een oplossing identificeren die een robuust mechanisme biedt voor het publiceren van gebeurtenissen naar externe consumenten. Maar voordat we ingaan op mogelijke oplossingen, moeten we eerst de typen berichtleveringsgaranties verduidelijken die haalbaar zijn in gedistribueerde systemen.

Garanties voor berichtlevering

Er zijn vier soorten garanties die we kunnen verkrijgen.

  1. Geen garanties
    Er is geen garantie dat het bericht op de bestemming wordt afgeleverd. De aanpak First-Local-Commit-Then-Publish gaat hier juist over. Consumenten kunnen berichten één keer, meerdere keren of helemaal niet ontvangen.

  2. Hoogstens één keer levering
    At most once delivery betekent dat het bericht maximaal 1 keer op de bestemming wordt afgeleverd. De aanpak First-Local-Commit-Then-Publish kan op deze manier ook worden geïmplementeerd met het retry-beleid van pogingen met waarde één.

  3. Minimaal één keer bezorgen\Consumenten ontvangen en verwerken elk bericht, maar kunnen hetzelfde bericht meerdere keren ontvangen.

  4. Exact éénmalige levering\Exact éénmalige levering betekent dat de consument het bericht effectief één keer ontvangt.
    Technisch gezien is het mogelijk om dit te bereiken met Kafka-transacties en een specifieke idempotente implementatie van producent en consument.


In de meeste gevallen lossen 'ten minste één keer'- leveringsgaranties veel problemen op door ervoor te zorgen dat berichten ten minste één keer worden afgeleverd, maar consumenten moeten idempotent zijn. Echter, gezien de onvermijdelijke netwerkstoringen, moet alle consumentenlogica idempotent zijn om te voorkomen dat dubbele berichten worden verwerkt, ongeacht de garanties van de producent. Daarom is deze vereiste niet zozeer een nadeel als wel een weerspiegeling van de realiteit.

Oplossingen

Er zijn talloze oplossingen voor dit probleem, die elk hun eigen voor- en nadelen hebben.

Twee-fase commit

Volgens Wikipedia is de Two-Phase Commit (2PC) een gedistribueerd transactieprotocol dat wordt gebruikt in computerwetenschappen en databasebeheersystemen om de consistentie en betrouwbaarheid van gedistribueerde transacties te garanderen. Het is ontworpen voor situaties waarin meerdere bronnen (bijv. databases) moeten deelnemen aan één enkele transactie, en het zorgt ervoor dat ze allemaal de transactie committen of allemaal afbreken, waardoor de consistentie van de gegevens behouden blijft. Het klinkt precies wat we nodig hebben, maar Two-Phase Commit heeft verschillende nadelen:

  • Als een deelnemende resource niet meer reageert of een storing ervaart, kan het hele proces worden geblokkeerd totdat het probleem is opgelost. Dit kan leiden tot potentiële prestatie- en beschikbaarheidsproblemen.
  • Two-Phase Commit biedt geen ingebouwde fouttolerantiemechanismen. Het vertrouwt op externe mechanismen of handmatige interventie om fouten te verwerken.
  • Niet alle moderne databases ondersteunen Two-Phase Commit.

Gedeelde database

De meest voor de hand liggende oplossing voor microservices-architectuur is om een patroon (of soms zelfs anti-patroon) toe te passen — een gedeelde database. Deze aanpak is erg intuïtief als u transactionele consistentie nodig hebt over meerdere tabellen in verschillende databases, gebruik dan gewoon één gedeelde database voor deze microservices.


De nadelen van deze aanpak zijn onder andere het introduceren van een single point of failure, het verhinderen van onafhankelijke database-schaling en het beperken van de mogelijkheid om verschillende database-oplossingen te gebruiken die het beste geschikt zijn voor specifieke vereisten en use cases. Bovendien zouden er aanpassingen aan de microservices-codebases nodig zijn om een dergelijke vorm van gedistribueerde transactie te ondersteunen.

Transactie-outbox

De ' transactionele outbox ' is een ontwerppatroon dat in gedistribueerde systemen wordt gebruikt om betrouwbare berichtverspreiding te garanderen, zelfs in het geval van onbetrouwbare berichtensystemen. Het omvat het opslaan van gebeurtenissen in een aangewezen 'OutboxEvents'-tabel binnen dezelfde transactie als de bewerking zelf. Deze aanpak sluit goed aan bij ACID-eigenschappen van relationele databases. Daarentegen ondersteunen veel No-SQL-databases ACID-eigenschappen niet volledig en kiezen ze in plaats daarvan voor de principes van de CAP-stelling en BASE-filosofie, die beschikbaarheid en uiteindelijke consistentie boven strikte consistentie stellen.


Een transactionele outbox biedt ten minste één garantie en kan op verschillende manieren worden geïmplementeerd:

  1. Transactielogboek bijhouden

  2. Poll-uitgever


De Transaction Log Tailing -benadering impliceert het gebruik van databasespecifieke oplossingen zoals CDC (Change Data Capture). De belangrijkste nadelen van die benadering zijn:

  • Databasespecifieke oplossingen

  • Verhoogde latentie vanwege specifieke CDC-implementaties


Een andere methode is de Polling Publisher , die outbox-offloading faciliteert door de outbox-tabel te pollen. Het belangrijkste nadeel van deze aanpak is de potentie voor een verhoogde databasebelasting, wat kan leiden tot hogere kosten. Bovendien ondersteunen niet alle No-SQL-databases efficiënte query's voor specifieke documentsegmenten. Het extraheren van hele documenten kan daarom leiden tot prestatieverslechtering.


Hier is een klein sequentiediagram waarin wordt uitgelegd hoe het werkt.


Luister naar jezelf

De grootste uitdaging met het Transactional Outbox-patroon ligt in de afhankelijkheid van database ACID-eigenschappen. Het is misschien eenvoudig in typische OLTP-databases, maar het levert uitdagingen op in het NoSQL-domein. Om dit aan te pakken, is een mogelijke oplossing om het append-logboek (bijvoorbeeld Kafka) te gebruiken vanaf het initiëren van de verwerking van verzoeken.


In plaats van de opdracht 'indien leningaanvraag' direct te verwerken, sturen we deze direct naar een intern Kafka-onderwerp en retourneren we vervolgens een 'geaccepteerd' resultaat naar de klant. Omdat het echter zeer waarschijnlijk is dat de opdracht nog moet worden verwerkt, kunnen we de klant niet direct op de hoogte stellen van het resultaat. Om deze uiteindelijke consistentie te beheren, kunnen we technieken gebruiken zoals long polling, client-initiated polling, optimistische UI-updates of het gebruik van WebSockets of Server-Sent Events voor meldingen. Dit is echter een heel ander onderwerp, dus laten we terugkeren naar ons oorspronkelijke onderwerp.


We hebben het bericht op een intern Kafka-onderwerp verzonden. De Loan Application Service gebruikt dit bericht vervolgens — dezelfde opdracht die het van de client ontving — en begint met verwerken. Eerst voert het wat bedrijfslogica uit; pas nadat deze logica succesvol is uitgevoerd en de resultaten zijn opgeslagen, publiceert het nieuwe berichten op een openbaar Kafka-onderwerp.


Laten we eens naar een stukje pseudocode kijken.


 public async Task HandleAsync(SubmitLoanApplicationCommand command, ...) { //First, process business logic var loanApplication = await _loanApplicationService.HandleCommandAsync(command, ...); //Then, send new events to public Kafka topic producer.Send(new LoanApplicationSubmittedEvent(loanApplication.Id)); //Then, commit offset consumer.Commit(); }


Wat als de verwerking van de business logica mislukt? Geen zorgen, aangezien de offset nog niet is vastgelegd, wordt het bericht opnieuw geprobeerd.


Wat als het verzenden van nieuwe gebeurtenissen naar Kafka mislukt? Geen zorgen, aangezien de businesslogica idempotent is, zal het geen dubbele leningaanvraag creëren. In plaats daarvan zal het proberen om berichten opnieuw te verzenden naar het openbare Kafka-onderwerp.


Wat als berichten naar Kafka worden gestuurd, maar de offset commit mislukt? Geen zorgen, aangezien de business logica idempotent is, zal het geen dubbele leningaanvraag creëren. In plaats daarvan zal het berichten opnieuw naar het openbare Kafka-onderwerp sturen en hopen dat de offset commit deze keer wel slaagt.


De belangrijkste nadelen van deze aanpak zijn onder meer de extra complexiteit die gepaard gaat met een nieuwe programmeerstijl, de uiteindelijke consistentie (aangezien de klant het resultaat niet meteen weet) en de vereiste dat alle bedrijfslogica idempotent moet zijn.

Evenementen sourcing

Wat is event sourcing en hoe kan het hier worden toegepast? Event sourcing is een software-architectuurpatroon dat wordt gebruikt om de status van een systeem te modelleren door alle wijzigingen in de gegevens vast te leggen als een reeks onveranderlijke gebeurtenissen. Deze gebeurtenissen vertegenwoordigen feiten of statusovergangen en dienen als de enige bron van waarheid voor de huidige status van het systeem. Dus technisch gezien hebben we door de implementatie van een event-sourcingsysteem al alle gebeurtenissen in EventStore en kan deze EventStore door consumenten worden gebruikt als een enkele bron van waarheid over wat er is gebeurd. Er is geen behoefte aan een specifieke databaseoplossing voor het bijhouden van alle wijzigingen of zorgen over de volgorde, het enige probleem is dat het aan de leeszijde zit, omdat om de werkelijke status van de entiteit te kunnen krijgen, alle gebeurtenissen opnieuw moeten worden afgespeeld.

Conclusie

In dit artikel hebben we verschillende benaderingen besproken voor het bouwen van betrouwbare berichten in gedistribueerde systemen. Er zijn verschillende aanbevelingen die we kunnen overwegen bij het bouwen van systemen met deze kenmerken

  1. Zorg altijd voor idempotente consumenten, aangezien netwerkstoringen onvermijdelijk zijn.
  2. Gebruik de First-Local-Commit-Then-Publish-methode zorgvuldig en zorg ervoor dat u de garantievereisten goed begrijpt.
  3. Gebruik nooit de aanpak Eerst publiceren, dan lokaal vastleggen, omdat dit kan leiden tot ernstige inconsistenties in de gegevens in uw systeem.
  4. Als de keuze van de bestaande database zeer waarschijnlijk kan veranderen of als de technische strategie vereist dat de beste opslagoplossing voor het probleem wordt geselecteerd, bouw dan geen gedeelde bibliotheken door te binden aan databaseoplossingen zoals CDC .
  5. Gebruik de Transactional Outbox- aanpak als standaardoplossing om minimaal één keer garanties te behalen.
  6. Overweeg de Luister naar uzelf- aanpak te gebruiken wanneer u No-SQL-databases gebruikt.


De volgende keer zullen we kijken naar een praktischer voorbeeld van het implementeren van een Transactional Outbox. Zie

Jij!