paint-brush
Mesaje de încredere în sistemele distribuitede@fairday
37,190 lecturi
37,190 lecturi

Mesaje de încredere în sistemele distribuite

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

Prea lung; A citi

Construirea unui sistem distribuit de încredere, foarte disponibil și scalabil necesită aderarea la tehnici, principii și modele specifice.
featured image - Mesaje de încredere în sistemele distribuite
Aleksei HackerNoon profile picture

Problemă de scriere duală

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ă.

Aproape un exemplu real

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:

  1. Un client depune o cerere de împrumut.
  2. Serviciul de Cerere de Împrumut înregistrează noua cerere cu statutul „În așteptare” și inițiază procesul de evaluare prin transmiterea cererii către Serviciul de Evaluare.
  3. Serviciul de evaluare evaluează cererea de împrumut primită și ulterior informează Serviciul de solicitare de împrumut asupra deciziei.
  4. La primirea deciziei, Serviciul de solicitare de împrumut actualizează în consecință starea cererii de împrumut și declanșează Serviciul de notificări pentru a informa clientul despre rezultat.
  5. Serviciul de Notificări procesează această solicitare și trimite notificări către client prin e-mail, SMS sau alte metode de comunicare preferate, conform setărilor clientului.


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:

  1. 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.

  2. 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.


Prima-Local-Commit-Apoi-Publicare


Î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.

Prima-Publicare-Apoi-Angajare-Local
Î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.

Garanții de livrare a mesajelor

Există patru tipuri de garanții pe care le-am putea obține.

  1. 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.

  2. 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.

  3. Cel puțin o dată livrare\Consumatorii vor primi și procesa fiecare mesaj, dar pot primi același mesaj de mai multe ori.

  4. 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.

Soluții

Există o mulțime de soluții la această problemă, care au avantajele și dezavantajele lor.

Commit în două faze

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:

  • Dacă o resursă participantă nu răspunde sau întâmpină un eșec, întregul proces poate fi blocat până când problema este rezolvată. Acest lucru poate duce la potențiale probleme de performanță și disponibilitate.
  • Two-Phase Commit nu oferă mecanisme de toleranță la erori încorporate. Se bazează pe mecanisme externe sau pe intervenția manuală pentru a gestiona defecțiunile.
  • Nu toate bazele de date moderne acceptă Two-Phase Commit.

Baza de date partajată

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ă.

Căsuță de trimitere tranzacțională

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:

  1. Înregistrarea jurnalului de tranzacții

  2. 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ă.


Ascultă-te pe tine

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ă.

Aprovizionare pentru evenimente

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.

Concluzie

Î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

  1. Dezvoltați întotdeauna consumatori idempotenți, deoarece defecțiunea rețelei este inevitabil.
  2. Utilizați cu atenție First-Local-Commit-Then-Publish cu o înțelegere clară a cerințelor de garanție.
  3. Nu utilizați niciodată abordarea First-Publish-Then-Local-Commit, deoarece aceasta poate duce la o inconsecvență gravă a datelor în sistemul dumneavoastră.
  4. Dacă este foarte probabil ca decizia de alegere a bazei de date existente să se schimbe sau strategia tehnică implică selectarea celei mai bune soluții de stocare pentru problemă, nu construiți biblioteci partajate legându-vă la soluții de baze de date precum CDC .
  5. Utilizați abordarea Tranzacțional Outbox ca soluție standard pentru a obține cel puțin o dată garanții.
  6. Luați în considerare utilizarea abordării Ascultați-vă pe dvs. atunci când sunt utilizate bazele de date No-SQL.


Data viitoare, ne vom uita la un exemplu mai practic de implementare a unei căsuțe de ieșire tranzacționale. Vedea

tu!