![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Att bygga ett pålitligt, högst tillgängligt, skalbart distribuerat system kräver att man följer specifika tekniker, principer och mönster. Utformningen av sådana system innebär att ta itu med en myriad av utmaningar. Bland de vanligaste och grundläggande frågorna är problemet med dubbelskrivning .
Det "dubbla skrivproblemet" är en utmaning som uppstår i distribuerade system, främst när man hanterar flera datakällor eller databaser som måste hållas synkroniserade. Det hänvisar till svårigheten att säkerställa att dataändringar konsekvent skrivs till olika datalager, såsom databaser eller cachar, utan att introducera problem som datainkonsekvenser, konflikter eller prestandaflaskhalsar.
Mikrotjänsters arkitektur och mönsterdatabas per tjänst ger dig många fördelar, såsom oberoende distribution och skalning, isolerade fel och en potentiell ökning av utvecklingshastigheten. Operationer kräver dock förändringar bland flera mikrotjänster, vilket tvingar dig att tänka på en pålitlig lösning för att ta itu med detta problem.
Låt oss överväga ett scenario där vår domän involverar att acceptera låneansökningar, bedöma dem och sedan skicka aviseringsvarningar till kunder.
I andan av principen om ett enda ansvar, Conways lag och domändrivna designmetoder, efter flera händelsestormingssessioner, delades hela domänen upp i tre underdomäner med definierade avgränsade sammanhang med tydliga gränser, domänmodeller och allmänt förekommande språk.
Den första har till uppgift att onboarda och sammanställa nya låneansökningar. Det andra systemet utvärderar dessa ansökningar och fattar beslut baserat på de uppgifter som tillhandahålls. Denna bedömningsprocess, inklusive KYC/KYB, antibedrägeri och kreditriskkontroller, kan vara tidskrävande, vilket kräver förmågan att hantera tusentals ansökningar samtidigt. Följaktligen har denna funktionalitet delegerats till en dedikerad mikrotjänst med en egen databas, vilket möjliggör oberoende skalning.
Dessutom hanteras dessa delsystem av två olika team, var och en med sina egna releasecykler, servicenivåavtal (SLA) och krav på skalbarhet.
Slutligen finns en specialiserad aviseringstjänst på plats för att skicka varningar till kunder.
Här är en förfinad beskrivning av systemets primära användningsfall:
Det är ett ganska enkelt och primitivt system vid första anblicken, men låt oss dyka in i hur låneansökningstjänsten behandlar kommandot skicka in låneansökan.
Vi kan överväga två tillvägagångssätt för serviceinteraktioner:
First-Local-Commit-Then-Publish: I detta tillvägagångssätt uppdaterar tjänsten sin lokala databas (commits) och publicerar sedan en händelse eller ett meddelande till andra tjänster.
First-Publish-Then-Local-Commit: Omvänt innebär den här metoden att en händelse eller ett meddelande publiceras innan ändringarna görs i den lokala databasen.
Båda metoderna har sina nackdelar och är endast delvis felsäkra för kommunikation i distribuerade system.
Detta är ett sekvensdiagram för att tillämpa den första metoden.
I det här scenariot använder låneansökningstjänsten First-Local-Commit-Then-Publish- metoden, där den först utför en transaktion och sedan försöker skicka ett meddelande till ett annat system. Den här processen kan dock misslyckas om det till exempel finns nätverksproblem, bedömningstjänsten är otillgänglig eller om låneansökningstjänsten stöter på ett minneslöst (OOM)-fel och kraschar. I sådana fall skulle meddelandet gå förlorat, vilket lämnar Bedömningen utan meddelande om den nya låneansökan, om inte ytterligare åtgärder vidtas.
Och den andra.
I scenariot First-Publish-Then-Local-Commit står låneansökningstjänsten inför mer betydande risker. Det kan informera Assessment Service om ett nytt program men misslyckas med att spara den här uppdateringen lokalt på grund av problem som databasproblem, minnesfel eller kodbuggar. Detta tillvägagångssätt kan leda till betydande inkonsekvenser i data, vilket kan orsaka allvarliga problem, beroende på hur Lånegranskningstjänsten hanterar inkommande ansökningar.
Därför måste vi hitta en lösning som erbjuder en robust mekanism för att publicera evenemang till externa konsumenter. Men innan vi fördjupar oss i potentiella lösningar bör vi först klargöra vilka typer av meddelandeleveransgarantier som kan uppnås i distribuerade system.
Det finns fyra typer av garantier vi skulle kunna uppnå.
Inga garantier
Det finns ingen garanti för att meddelandet kommer att levereras till destinationen. Tillvägagångssättet First-Local-Commit-Then-Publish handlar just om detta. Konsumenter kan få meddelanden en gång, flera gånger eller aldrig alls.
Högst en gång leverans
Högst en gång leverans innebär att meddelandet kommer att levereras till destinationen högst 1 gång. Tillvägagångssättet First-Local-Commit-Then-Publish kan också implementeras på detta sätt med policyn för att försöka igen med försök med värde ett.
Minst en gång leverans\Konsumenter kommer att ta emot och bearbeta varje meddelande men kan få samma meddelande mer än en gång.
Exakt en gång leverans\Exakt en gång leverans betyder att konsumenten får meddelandet effektivt en gång.
Tekniskt sett är det möjligt att uppnå med Kafka transaktioner och specifik idempotent implementering av producent och konsument.
I de flesta fall löser leveransgarantier "minst en gång" många problem genom att säkerställa att meddelanden levereras minst en gång, men konsumenterna måste vara idempotenta. Med tanke på de oundvikliga nätverksfelen måste dock all konsumentlogik vara idempotent för att undvika att behandla dubbletter av meddelanden, oavsett tillverkarens garantier. Därför är detta krav inte så mycket en nackdel som det speglar verkligheten.
Det finns gott om lösningar på detta problem, som har sina fördelar och nackdelar.
Enligt Wikipedia är Two-Phase Commit (2PC) ett distribuerat transaktionsprotokoll som används i datavetenskap och databashanteringssystem för att säkerställa konsistensen och tillförlitligheten hos distribuerade transaktioner. Den är designad för situationer där flera resurser (t.ex. databaser) behöver delta i en enda transaktion, och det säkerställer att antingen alla genomför transaktionen eller alla avbryter den, och därigenom bibehåller datakonsistensen. Det låter precis vad vi behöver, men Two-Phase Commit har flera nackdelar:
Den mest uppenbara lösningen för mikrotjänsters arkitektur är att tillämpa ett mönster (eller till och med ibland antimönster) - en delad databas. Detta tillvägagångssätt är mycket intuitivt om du behöver transaktionskonsistens över flera tabeller i olika databaser, använd bara en delad databas för dessa mikrotjänster.
Nackdelarna med detta tillvägagångssätt inkluderar införandet av en enda felpunkt, förhindrande av oberoende databasskalning och begränsning av möjligheten att använda olika databaslösningar som är bäst lämpade för specifika krav och användningsfall. Dessutom skulle modifieringar av mikrotjänsters kodbaser vara nödvändiga för att stödja en sådan form av distribuerad transaktion.
" Transaktionsutkorgen " är ett designmönster som används i distribuerade system för att säkerställa tillförlitlig meddelandeförmedling, även inför opålitliga meddelandesystem. Det handlar om att lagra händelser i en utpekad 'OutboxEvents'-tabell inom samma transaktion som själva operationen. Detta tillvägagångssätt stämmer väl överens med ACID-egenskaper hos relationsdatabaser. Däremot stöder många No-SQL-databaser inte fullt ut ACID-egenskaper, utan väljer istället principerna för CAP-teoremet och BASE-filosofin, som prioriterar tillgänglighet och eventuell konsistens framför strikt konsistens.
En transaktionsutkorg ger minst en gång garanti och kan implementeras med flera metoder:
Transaktionsloggavskärning
Omröstningsförlag
Transaktionslogganvändning innebär att man använder databasspecifika lösningar som CDC (Change Data Capture). De främsta nackdelarna med det tillvägagångssättet är:
Databasspecifika lösningar
Ökad latens på grund av specifikationerna för CDC-implementeringar
En annan metod är Polling Publisher , som underlättar avlastning av utkorgen genom att polla utkorgstabellen. Den primära nackdelen med detta tillvägagångssätt är potentialen för ökad databasbelastning, vilket kan leda till högre kostnader. Dessutom stöder inte alla No-SQL-databaser effektiv sökning för specifika dokumentsegment. Att extrahera hela dokument kan därför resultera i prestandaförsämring.
Här är ett litet sekvensdiagram som förklarar hur det fungerar.
Den primära utmaningen med Transactional Outbox-mönstret ligger i dess beroende av databasens ACID-egenskaper. Det kan vara okomplicerat i typiska OLTP-databaser men innebär utmaningar i NoSQL-sfären. För att komma till rätta med detta är en potentiell lösning att utnyttja bifogad logg (till exempel Kafka) direkt från att initiera förfrågningsbehandling.
Istället för att direkt bearbeta kommandot 'skicka in låneansökan' skickar vi det omedelbart till ett internt Kafka-ämne och returnerar sedan ett 'godkänt' resultat till kunden. Men eftersom det är mycket troligt att kommandot fortfarande behöver bearbetas kan vi inte omedelbart informera kunden om resultatet. För att hantera denna eventuella konsekvens kan vi använda tekniker som lång polling, klientinitierad polling, optimistiska UI-uppdateringar eller använda WebSockets eller Server-Sent Events för aviseringar. Detta är dock ett distinkt ämne helt och hållet, så låt oss återgå till vårt ursprungliga ämne.
Vi skickade meddelandet om ett internt Kafka-ämne. Låneansökningstjänsten förbrukar sedan detta meddelande - samma kommando som det fick från klienten - och börjar bearbeta. För det första kör den en del affärslogik; först efter att denna logik har exekveras framgångsrikt och resultaten kvarstår, publicerar den nya meddelanden om ett offentligt Kafka-ämne.
Låt oss ta en titt på lite pseudokod.
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(); }
Vad händer om bearbetningen av affärslogiken misslyckas? Inga bekymmer, eftersom förskjutningen ännu inte har begåtts kommer meddelandet att försökas igen.
Vad händer om det misslyckas att skicka nya händelser till Kafka? Inga bekymmer, eftersom affärslogiken är idempotent, kommer den inte att skapa en duplicerad låneansökan. Istället kommer den att försöka skicka meddelanden till det offentliga Kafka-ämnet igen.
Vad händer om meddelanden skickas till Kafka, men offset-commit misslyckas? Inga bekymmer, eftersom affärslogiken är idempotent kommer den inte att skapa en duplicerad låneansökan. Istället kommer det att skicka meddelanden till det offentliga Kafka-ämnet och hoppas att offset-åtagandet lyckas den här gången.
De främsta nackdelarna med detta tillvägagångssätt inkluderar den extra komplexiteten som är förknippad med en ny programmeringsstil, eventuell konsekvens (eftersom kunden inte omedelbart vet resultatet) och kravet på att all affärslogik ska vara idempotent.
Vad är event sourcing och hur skulle det kunna tillämpas här? Event sourcing är ett mjukvaruarkitektoniskt mönster som används för att modellera ett systems tillstånd genom att fånga alla ändringar av dess data som en serie oföränderliga händelser. Dessa händelser representerar fakta eller tillståndsövergångar och fungerar som den enda källan till sanning för systemets nuvarande tillstånd. Så, tekniskt sett, genom att implementera ett event-sourcing-system har vi redan alla event i EventStore, och denna EventStore kan användas av konsumenter som en enda källa till sanning om vad som hände. Det finns inget behov av en specifik databaslösning för att spåra alla ändringar eller bekymmer om beställning, det enda problemet är att sitta på lässidan eftersom det krävs att alla händelser spelas upp för att kunna få det faktiska tillståndet.
I den här artikeln har vi granskat flera tillvägagångssätt för att bygga tillförlitliga meddelanden i distribuerade system. Det finns flera rekommendationer som vi kan överväga när vi bygger system med dessa egenskaper
Nästa gång kommer vi att titta på ett mer praktiskt exempel på implementering av en transaktionsutkorg. Se
du!