![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Construirea unui sistem distribuit de încredere, foarte disponibil și scalabil necesită aderarea la tehnici, principii și modele specifice. Proiectarea unor astfel de sisteme implică abordarea unei multitudini de provocări. Printre cele mai răspândite și fundamentale probleme se numără problema scrierii duale .
„Problema de scriere duală” este o provocare care apare în sistemele distribuite, în principal atunci când aveți de-a face cu mai multe surse de date sau baze de date care trebuie menținute sincronizate. Se referă la dificultatea de a vă asigura că modificările datelor sunt scrise în mod consecvent în diferite depozite de date, cum ar fi baze de date sau cache, fără a introduce probleme precum inconsecvențele datelor, conflictele sau blocajele de performanță.
Arhitectura de microservicii și baza de date cu modele per serviciu vă oferă multe beneficii, cum ar fi implementare și scalare independentă, eșecuri izolate și o creștere potențială a vitezei de dezvoltare. Cu toate acestea, operațiunile necesită modificări între mai multe microservicii, forțându-vă să vă gândiți la o soluție fiabilă pentru a rezolva această problemă.
Să luăm în considerare un scenariu în care domeniul nostru implică acceptarea cererilor de împrumut, evaluarea acestora și apoi trimiterea de alerte de notificare către clienți.
În spiritul principiului responsabilității unice, al legii lui Conway și al abordării de proiectare bazată pe domenii, după mai multe sesiuni de asumare a evenimentelor, întregul domeniu a fost împărțit în trei subdomenii cu contexte delimitate definite, având granițe clare, modele de domenii și limbaj omniprezent.
Primul are sarcina de a integra și de a compila noi cereri de împrumut. Al doilea sistem evaluează aceste aplicații și ia decizii pe baza datelor furnizate. Acest proces de evaluare, inclusiv verificări KYC/KYB, antifraudă și riscul de credit, poate consuma mult timp, necesitând capacitatea de a gestiona mii de aplicații simultan. În consecință, această funcționalitate a fost delegată unui microserviciu dedicat cu propria bază de date, permițând scalarea independentă.
În plus, aceste subsisteme sunt gestionate de două echipe diferite, fiecare cu propriile cicluri de lansare, acorduri de nivel de servicii (SLA) și cerințe de scalabilitate.
În cele din urmă , există un serviciu specializat de notificare pentru a trimite alerte clienților.
Iată o descriere rafinată a cazului de utilizare principal al sistemului:
Este un sistem destul de simplu și primitiv la prima vedere, dar haideți să vedem cum procesează serviciul de cerere de împrumut comanda de trimitere a cererii de împrumut.
Putem lua în considerare două abordări pentru interacțiunile cu serviciile:
First-Local-Commit-Then-Publish: în această abordare, serviciul își actualizează baza de date locală (commit) și apoi publică un eveniment sau un mesaj către alte servicii.
First-Publish-Then-Local-Commit: Dimpotrivă, această metodă implică publicarea unui eveniment sau mesaj înainte de a efectua modificările în baza de date locală.
Ambele metode au dezavantajele lor și sunt doar parțial sigure pentru comunicații în sistemele distribuite.
Aceasta este o diagramă de secvență a aplicării primei abordări.
În acest scenariu, Serviciul de aplicare a împrumuturilor folosește abordarea First-Local-Commit-Then-Publish , în care mai întâi comite o tranzacție și apoi încearcă să trimită o notificare către alt sistem. Cu toate acestea, acest proces este susceptibil de eșec dacă, de exemplu, există probleme de rețea, Serviciul de evaluare este indisponibil sau Serviciul de cerere de împrumut întâlnește o eroare de memorie lipsită (OOM) și se blochează. În astfel de cazuri, mesajul s-ar pierde, părăsind Evaluarea fără notificarea noii cereri de împrumut, cu excepția cazului în care sunt implementate măsuri suplimentare.
Și al doilea.
În scenariul First-Publish-Then-Local-Commit , Serviciul de solicitare de împrumut se confruntă cu riscuri mai semnificative. Este posibil să informeze Serviciul de evaluare despre o nouă aplicație, dar să nu salveze această actualizare la nivel local din cauza unor probleme precum probleme de bază de date, erori de memorie sau erori de cod. Această abordare poate duce la inconsecvențe semnificative în date, care ar putea cauza probleme serioase, în funcție de modul în care Serviciul de evaluare a împrumuturilor gestionează cererile primite.
Prin urmare, trebuie să identificăm o soluție care să ofere un mecanism robust de publicare a evenimentelor consumatorilor externi. Dar, înainte de a explora soluțiile potențiale, ar trebui mai întâi să clarificăm tipurile de garanții de livrare a mesajelor realizabile în sistemele distribuite.
Există patru tipuri de garanții pe care le-am putea obține.
Fără garanții
Nu există nicio garanție că mesajul va fi livrat la destinație. Abordarea First-Local-Commit-Then-Publish este tocmai despre asta. Consumatorii pot primi mesaje o dată, de mai multe ori sau deloc.
Cel mult o dată livrare
Livrarea de cel mult o dată înseamnă că mesajul va fi livrat la destinație de cel mult o dată. Abordarea First-Local-Commit-Then-Publish poate fi implementată în acest fel și cu politica de reîncercare a încercărilor cu valoarea unu.
Cel puțin o dată livrare\Consumatorii vor primi și procesa fiecare mesaj, dar pot primi același mesaj de mai multe ori.
Livrare exactă o dată\Livrare exactă o dată înseamnă că consumatorul va primi mesajul efectiv o dată.
Din punct de vedere tehnic, este posibil să se realizeze cu Kafka tranzacții și implementare idempotent specifică a producătorului și consumatorului.
În cele mai multe cazuri, garanțiile de livrare „cel puțin o dată” abordează multe probleme, asigurându-se că mesajele sunt livrate cel puțin o dată, dar consumatorii trebuie să fie idepotenți. Totuși, având în vedere defecțiunile inevitabile ale rețelei, toată logica consumatorului trebuie să fie idempotentă pentru a evita procesarea mesajelor duplicate, indiferent de garanțiile producătorului. Prin urmare, această cerință nu este atât de mult un dezavantaj, ci reflectă realitatea.
Există o mulțime de soluții la această problemă, care au avantajele și dezavantajele lor.
Potrivit Wikipedia, Two-Phase Commit (2PC) este un protocol de tranzacție distribuit utilizat în informatică și sisteme de management al bazelor de date pentru a asigura consistența și fiabilitatea tranzacțiilor distribuite. Este conceput pentru situațiile în care mai multe resurse (de exemplu, baze de date) trebuie să participe la o singură tranzacție și se asigură că fie toate commit tranzacția, fie că toate o anulează, menținând astfel coerența datelor. Sună exact ceea ce avem nevoie, dar Comitarea în două faze are mai multe dezavantaje:
Cea mai evidentă soluție pentru arhitectura de microservicii este aplicarea unui model (sau chiar uneori anti-model) - o bază de date partajată. Această abordare este foarte intuitivă dacă aveți nevoie de consistență tranzacțională în mai multe tabele din baze de date diferite, utilizați doar o bază de date partajată pentru aceste microservicii.
Dezavantajele acestei abordări includ introducerea unui singur punct de eșec, inhibarea scalării independente a bazei de date și limitarea capacității de a utiliza diferite soluții de baze de date, cele mai potrivite pentru cerințe și cazuri de utilizare specifice. În plus, ar fi necesare modificări ale bazelor de cod pentru microservicii pentru a sprijini o astfel de formă de tranzacție distribuită.
„ Casa de ieșire tranzacțională ” este un model de design utilizat în sistemele distribuite pentru a asigura propagarea fiabilă a mesajelor, chiar și în fața sistemelor de mesagerie nesigure. Aceasta implică stocarea evenimentelor într-un tabel desemnat „OutboxEvents” în cadrul aceleiași tranzacții ca și operațiunea în sine. Această abordare se aliniază bine cu proprietățile ACID ale bazelor de date relaționale. În schimb, multe baze de date No-SQL nu acceptă pe deplin proprietățile ACID, optând în schimb pentru principiile teoremei CAP și ale filozofiei BASE, care prioritizează disponibilitatea și eventuala consistență față de consistența strictă.
O căsuță de ieșire tranzacțională oferă cel puțin o garanție și poate fi implementată cu mai multe abordări:
Înregistrarea jurnalului de tranzacții
Editor de sondaje
Abordarea jurnalului de tranzacții implică utilizarea unor soluții specifice bazei de date, cum ar fi CDC (Change Data Capture). Principalele dezavantaje ale acestei abordări sunt:
Soluții specifice bazelor de date
Latență crescută datorită specificului implementărilor CDC
O altă metodă este Polling Publisher , care facilitează descărcarea căsuței de ieșire prin sondajul tabelului de trimiteri. Principalul dezavantaj al acestei abordări este potențialul de creștere a încărcării bazei de date, care poate duce la costuri mai mari. În plus, nu toate bazele de date No-SQL acceptă interogări eficiente pentru anumite segmente de document. Prin urmare, extragerea documentelor întregi poate duce la degradarea performanței.
Iată o mică diagramă de secvență care explică cum funcționează.
Principala provocare a modelului Tranzacțional Outbox constă în dependența acestuia de proprietățile ACID ale bazei de date. Ar putea fi simplu în bazele de date OLTP tipice, dar prezintă provocări în domeniul NoSQL. Pentru a rezolva acest lucru, o soluție potențială este să folosiți jurnalul de atașare (de exemplu, Kafka) chiar de la inițierea procesării cererii.
În loc să procesăm direct comanda „trimite cererea de împrumut”, o trimitem imediat la un subiect intern Kafka și apoi returnăm clientului un rezultat „acceptat”. Cu toate acestea, deoarece este foarte probabil ca comanda să fie încă procesată, nu putem informa imediat clientul despre rezultat. Pentru a gestiona această eventuală consistență, putem folosi tehnici precum sondajul lung, sondajul inițiat de client, actualizările optimiste ale interfeței de utilizare sau utilizarea WebSockets sau Evenimentele trimise de server pentru notificări. Cu toate acestea, acesta este un subiect cu totul distinct, așa că să revenim la subiectul nostru inițial.
Am trimis mesajul pe un subiect intern Kafka. Serviciul de cerere de împrumut consumă apoi acest mesaj - aceeași comandă pe care a primit-o de la client - și începe procesarea. În primul rând, execută o anumită logică de afaceri; numai după ce această logică este executată cu succes și rezultatele sunt persistente, publică mesaje noi pe un subiect public Kafka.
Să aruncăm o privire la un pic de pseudo-cod.
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(); }
Ce se întâmplă dacă procesarea logicii de afaceri eșuează? Nu vă faceți griji, deoarece compensarea nu a fost încă efectuată, mesajul va fi reîncercat.
Ce se întâmplă dacă trimiterea de noi evenimente către Kafka eșuează? Nu vă faceți griji, deoarece logica de afaceri este idempotentă, nu va crea o cerere de împrumut duplicat. În schimb, va încerca să retrimită mesaje către subiectul public Kafka.
Ce se întâmplă dacă mesajele sunt trimise către Kafka, dar comiterea offset eșuează? Nu vă faceți griji, deoarece logica de afaceri este idempotentă, nu va crea o cerere de împrumut duplicat. În schimb, va retrimite mesaje către subiectul public Kafka și va spera că comiterea de compensare va reuși de data aceasta.
Principalele dezavantaje ale acestei abordări includ complexitatea adăugată asociată cu un nou stil de programare, eventuala consistență (din moment ce clientul nu va ști imediat rezultatul) și cerința ca toată logica de afaceri să fie idempotentă.
Ce este aprovizionarea cu evenimente și cum ar putea fi aplicată aici? Event sourcing este un model arhitectural software folosit pentru a modela starea unui sistem prin captarea tuturor modificărilor aduse datelor sale ca o serie de evenimente imuabile. Aceste evenimente reprezintă fapte sau tranziții de stare și servesc ca sursă unică de adevăr pentru starea curentă a sistemului. Deci, din punct de vedere tehnic, prin implementarea unui sistem de aprovizionare cu evenimente, avem deja toate evenimentele în EventStore, iar acest EventStore poate fi folosit de consumatori ca o singură sursă de adevăr despre ceea ce sa întâmplat. Nu este nevoie de o soluție de bază de date specifică pentru urmărirea tuturor modificărilor sau preocupărilor legate de comandă, singura problemă este să stai pe partea de citire, deoarece pentru a putea obține starea reală a entității este necesară reluarea tuturor evenimentelor.
În acest articol, am analizat mai multe abordări pentru construirea de mesagerie fiabile în sistemele distribuite. Există câteva recomandări pe care le-am putea lua în considerare atunci când construim sisteme cu aceste caracteristici
Data viitoare, ne vom uita la un exemplu mai practic de implementare a unei căsuțe de ieșire tranzacționale. Vedea
tu!