![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Opbygning af et pålideligt, meget tilgængeligt, skalerbart distribueret system kræver overholdelse af specifikke teknikker, principper og mønstre. Designet af sådanne systemer indebærer at tage fat på et utal af udfordringer. Blandt de mest udbredte og grundlæggende problemer er det dobbelte skriveproblem .
Det "dobbelte skriveproblem" er en udfordring, der opstår i distribuerede systemer, hovedsageligt når man håndterer flere datakilder eller databaser, der skal holdes synkroniseret. Det henviser til vanskeligheden ved at sikre, at dataændringer konsekvent skrives til forskellige datalagre, såsom databaser eller caches, uden at introducere problemer som datainkonsistens, konflikter eller ydeevneflaskehalse.
Mikroservicearkitekturen og mønsterdatabasen pr. tjeneste giver dig mange fordele, såsom uafhængig implementering og skalering, isolerede fejl og et potentielt løft af udviklingshastigheden. Operationer kræver dog ændringer blandt flere mikrotjenester, hvilket tvinger dig til at tænke på en pålidelig løsning til at tackle dette problem.
Lad os overveje et scenarie, hvor vores domæne involverer at acceptere låneansøgninger, vurdere dem og derefter sende notifikationer til kunder.
I ånden bag princippet om enkelt ansvar, Conways lov og domænedrevne designtilgang, blev hele domænet efter adskillige hændelsesstormsessioner opdelt i tre underdomæner med definerede afgrænsede kontekster med klare grænser, domænemodeller og allestedsnærværende sprog.
Den første har til opgave at onboarde og udarbejde nye låneansøgninger. Det andet system evaluerer disse ansøgninger og træffer beslutninger baseret på de leverede data. Denne vurderingsproces, herunder KYC/KYB, antibedrageri og kreditrisikotjek, kan være tidskrævende, hvilket nødvendiggør evnen til at håndtere tusindvis af ansøgninger samtidigt. Som følge heraf er denne funktionalitet blevet delegeret til en dedikeret mikrotjeneste med sin egen database, hvilket muliggør uafhængig skalering.
Ydermere administreres disse undersystemer af to forskellige teams, hver med deres egne udgivelsescyklusser, serviceniveauaftaler (SLA) og skalerbarhedskrav.
Endelig er en specialiseret underretningstjeneste på plads til at sende advarsler til kunder.
Her er en raffineret beskrivelse af systemets primære brugssituation:
Det er et ret simpelt og primitivt system ved første øjekast, men lad os dykke ned i, hvordan låneansøgningstjenesten behandler kommandoen indsend låneansøgning.
Vi kan overveje to tilgange til serviceinteraktioner:
First-Local-Commit-Then-Publish: I denne tilgang opdaterer tjenesten sin lokale database (commits) og udgiver derefter en begivenhed eller besked til andre tjenester.
First-Publish-Then-Local-Commit: Omvendt involverer denne metode at publicere en begivenhed eller besked, før ændringerne til den lokale database.
Begge metoder har deres ulemper og er kun delvist fejlsikre til kommunikation i distribuerede systemer.
Dette er et sekvensdiagram over anvendelse af den første tilgang.
I dette scenarie anvender låneansøgningstjenesten First-Local-Commit-Then-Publish- tilgangen, hvor den først forpligter en transaktion og derefter forsøger at sende en meddelelse til et andet system. Denne proces er dog modtagelig for fejl, hvis der for eksempel er netværksproblemer, vurderingstjenesten er utilgængelig, eller låneansøgningstjenesten støder på en fejl i hukommelsen (OOM) og går ned. I sådanne tilfælde ville meddelelsen gå tabt, og vurderingen forlades uden varsel om den nye låneansøgning, medmindre yderligere foranstaltninger implementeres.
Og den anden.
I First-Publish-Then-Then-Local-Commit- scenariet står låneansøgningstjenesten over for større risici. Det kan informere Assessment Service om et nyt program, men undlader at gemme denne opdatering lokalt på grund af problemer som databaseproblemer, hukommelsesfejl eller kodefejl. Denne tilgang kan føre til betydelige uoverensstemmelser i data, som kan forårsage alvorlige problemer, afhængigt af hvordan Låneanmeldelsestjenesten håndterer indkommende ansøgninger.
Derfor skal vi identificere en løsning, der tilbyder en robust mekanisme til at publicere begivenheder til eksterne forbrugere. Men før vi dykker ned i potentielle løsninger, bør vi først afklare, hvilke typer meddelelsesleveringsgarantier, der kan opnås i distribuerede systemer.
Der er fire typer garantier, vi kan opnå.
Ingen garantier
Der er ingen garanti for, at beskeden bliver leveret til destinationen. Tilgangen First-Local-Commit-Then-Publish handler netop om dette. Forbrugere kan modtage beskeder én gang, flere gange eller aldrig overhovedet.
Højst én gang levering
Højst én gang levering betyder, at beskeden højst bliver leveret til destinationen 1 gang. Tilgangen First-Local-Commit-Then-Publish kan også implementeres på denne måde med genforsøgspolitikken for forsøg med værdi et.
Mindst én gang levering\Forbrugere vil modtage og behandle hver besked, men kan modtage den samme besked mere end én gang.
Præcis én gang levering\ Præcis én gang levering betyder, at forbrugeren modtager beskeden effektivt én gang.
Teknisk set er det muligt at opnå med Kafka transaktioner og specifik idempotent implementering af producent og forbruger.
I de fleste tilfælde løser leveringsgarantier "mindst én gang" mange problemer ved at sikre, at beskeder leveres mindst én gang, men forbrugerne skal være idempotente. Men i betragtning af de uundgåelige netværksfejl skal al forbrugerlogik være idempotent for at undgå at behandle duplikerede meddelelser, uanset producentens garantier. Derfor er dette krav ikke så meget en ulempe, som det afspejler virkeligheden.
Der er masser af løsninger på dette problem, som har deres fordele og ulemper.
Ifølge Wikipedia er Two-Phase Commit (2PC) en distribueret transaktionsprotokol, der bruges i datalogi og databasestyringssystemer for at sikre konsistensen og pålideligheden af distribuerede transaktioner. Det er designet til situationer, hvor flere ressourcer (f.eks. databaser) skal deltage i en enkelt transaktion, og det sikrer, at enten alle udfører transaktionen eller alle afbryder den, og derved opretholder datakonsistensen. Det lyder præcis, hvad vi har brug for, men Two-Phase Commit har flere ulemper:
Den mest åbenlyse løsning til mikroservicearkitektur er at anvende et mønster (eller endda nogle gange anti-mønster) - en delt database. Denne tilgang er meget intuitiv, hvis du har brug for transaktionskonsistens på tværs af flere tabeller i forskellige databaser, brug blot én delt database til disse mikrotjenester.
Ulemperne ved denne tilgang omfatter indførelse af et enkelt fejlpunkt, hæmning af uafhængig databaseskalering og begrænsning af muligheden for at bruge forskellige databaseløsninger, der er bedst egnede til specifikke krav og brugssager. Derudover ville modifikationer af mikrotjenesternes kodebaser være nødvendige for at understøtte en sådan form for distribueret transaktion.
Den ' transaktionelle udbakke ' er et designmønster, der bruges i distribuerede systemer for at sikre pålidelig meddelelsesudbredelse, selv i lyset af upålidelige meddelelsessystemer. Det involverer lagring af hændelser i en udpeget 'OutboxEvents'-tabel inden for samme transaktion som selve operationen. Denne tilgang stemmer godt overens med ACID-egenskaber for relationelle databaser. I modsætning hertil understøtter mange No-SQL-databaser ikke fuldt ud ACID-egenskaber, idet de i stedet vælger principperne i CAP-sætningen og BASE-filosofien, som prioriterer tilgængelighed og eventuel konsistens frem for streng konsistens.
En transaktionsudbakke giver mindst én gang garanti og kan implementeres med flere tilgange:
Transaktionslog tailing
Afstemningsudgiver
Transaktionslog-tailing- tilgang indebærer brug af databasespecifikke løsninger som CDC (Change Data Capture). De vigtigste ulemper ved denne tilgang er:
Databasespecifikke løsninger
Øget latens på grund af specifikationer for CDC-implementeringer
En anden metode er Polling Publisher , som letter udbakkeaflæsning ved at polle udbakketabellen. Den primære ulempe ved denne tilgang er potentialet for øget databasebelastning, hvilket kan føre til højere omkostninger. Desuden understøtter ikke alle No-SQL-databaser effektiv forespørgsel for specifikke dokumentsegmenter. Udtrækning af hele dokumenter kan derfor resultere i ydeevneforringelse.
Her er et lille sekvensdiagram, der forklarer, hvordan det fungerer.
Den primære udfordring med Transactional Outbox-mønsteret ligger i dets afhængighed af databasens ACID-egenskaber. Det kan være ligetil i typiske OLTP-databaser, men udgør udfordringer i NoSQL-området. For at løse dette er en potentiel løsning at udnytte tilføjelsesloggen (for eksempel Kafka) lige fra start af anmodningsbehandling.
I stedet for direkte at behandle kommandoen 'send låneansøgning' sender vi den straks til et internt Kafka-emne og returnerer derefter et 'accepteret' resultat til kunden. Men da det er meget sandsynligt, at kommandoen stadig skal behandles, kan vi ikke umiddelbart informere kunden om resultatet. For at styre denne eventuelle konsistens kan vi anvende teknikker såsom lang polling, klientinitieret polling, optimistiske UI-opdateringer eller brug af WebSockets eller Server-Sent Events til meddelelser. Dette er dog et særskilt emne helt, så lad os vende tilbage til vores indledende emne.
Vi sendte beskeden om et internt Kafka-emne. Låneansøgningstjenesten bruger derefter denne besked - den samme kommando, som den modtog fra klienten - og begynder behandlingen. For det første udfører den en vis forretningslogik; først efter at denne logik er udført med succes, og resultaterne er fastholdt, udgiver den nye meddelelser om et offentligt Kafka-emne.
Lad os tage et kig på lidt pseudo-kode.
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(); }
Hvad hvis behandlingen af forretningslogikken fejler? Ingen bekymringer, da forskydningen endnu ikke er begået, vil meddelelsen blive forsøgt igen.
Hvad hvis det mislykkes at sende nye begivenheder til Kafka? Ingen bekymringer, da forretningslogikken er idempotent, vil den ikke skabe en dublet låneansøgning. I stedet vil den forsøge at sende beskeder til det offentlige Kafka-emne igen.
Hvad hvis meddelelser sendes til Kafka, men offset-commit mislykkes? Ingen bekymringer, da forretningslogikken er idempotent, vil den ikke skabe en dublet låneansøgning. I stedet vil den sende beskeder til det offentlige Kafka-emne igen og håbe, at offset-forpligtelsen lykkes denne gang.
De største ulemper ved denne tilgang omfatter den ekstra kompleksitet, der er forbundet med en ny programmeringsstil, eventuel konsistens (da klienten ikke umiddelbart kender resultatet) og kravet om, at al forretningslogik skal være idempotent.
Hvad er event sourcing, og hvordan kan det anvendes her? Event sourcing er et softwarearkitektonisk mønster, der bruges til at modellere et systems tilstand ved at fange alle ændringer i dets data som en række uforanderlige hændelser. Disse begivenheder repræsenterer fakta eller tilstandsovergange og tjener som den eneste kilde til sandhed for systemets nuværende tilstand. Så teknisk set, ved at implementere et event-sourcing-system, har vi allerede alle begivenheder i EventStore, og denne EventStore kan bruges af forbrugerne som en enkelt kilde til sandhed om, hvad der skete. Der er ikke behov for en specifik databaseløsning til at spore alle ændringer eller bekymringer om bestilling, det eneste problem er at sidde på læsesiden, da det er nødvendigt at genafspille alle begivenheder for at kunne få den faktiske tilstand af enheden.
I denne artikel har vi gennemgået flere tilgange til at opbygge pålidelige beskeder i distribuerede systemer. Der er flere anbefalinger, vi kan overveje, når vi bygger systemer med disse egenskaber
Næste gang vil vi se på et mere praktisk eksempel på implementering af en transaktionsudbakke. Se
du!