![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
La construcció d'un sistema distribuït fiable, d'alta disponibilitat i escalable requereix l'adhesió a tècniques, principis i patrons específics. El disseny d'aquests sistemes implica abordar una infinitat de reptes. Entre els problemes més prevalents i fonamentals hi ha el problema d'escriptura dual .
El "problema d'escriptura dual" és un repte que sorgeix en sistemes distribuïts, principalment quan es tracta de múltiples fonts de dades o bases de dades que s'han de mantenir sincronitzades. Es refereix a la dificultat d'assegurar que els canvis de dades s'escriuen de manera coherent a diversos magatzems de dades, com ara bases de dades o memòria cau, sense introduir problemes com ara inconsistències de dades, conflictes o colls d'ampolla de rendiment.
L'arquitectura de microserveis i la base de dades de patrons per servei us ofereixen molts avantatges, com ara desplegament i escalat independents, errors aïllats i un augment potencial de la velocitat de desenvolupament. Tanmateix, les operacions requereixen canvis entre diversos microserveis, cosa que us obliga a pensar en una solució fiable per fer front a aquest problema.
Considerem un escenari en què el nostre domini implica acceptar sol·licituds de préstec, avaluar-les i després enviar alertes de notificació als clients.
En l'esperit del principi de responsabilitat única, la llei de Conway i l'enfocament de disseny basat en dominis, després de diverses sessions d'assalt d'esdeveniments, tot el domini es va dividir en tres subdominis amb contextos delimitats definits amb límits clars, models de domini i llenguatge omnipresent.
El primer s'encarrega d'incorporar i compilar noves sol·licituds de préstec. El segon sistema avalua aquestes aplicacions i pren decisions a partir de les dades proporcionades. Aquest procés d'avaluació, que inclou KYC/KYB, antifrau i comprovacions de risc de crèdit, pot consumir molt de temps, i requereix la capacitat de gestionar milers d'aplicacions simultàniament. En conseqüència, aquesta funcionalitat s'ha delegat a un microservei dedicat amb la seva pròpia base de dades, que permet un escalat independent.
A més, aquests subsistemes són gestionats per dos equips diferents, cadascun amb els seus propis cicles de llançament, acords de nivell de servei (SLA) i requisits d'escalabilitat.
Finalment , hi ha un servei de notificació especialitzat per enviar alertes als clients.
Aquí hi ha una descripció refinada del cas d'ús principal del sistema:
És un sistema bastant senzill i primitiu a primera vista, però analitzem com el servei de sol·licitud de préstec processa l'ordre d'enviament de sol·licitud de préstec.
Podem considerar dos enfocaments per a les interaccions de serveis:
First-Local-Commit-Then-Publish: En aquest enfocament, el servei actualitza la seva base de dades local (commits) i després publica un esdeveniment o missatge a altres serveis.
First-Publish-Then-Local-Commit: Per contra, aquest mètode implica la publicació d'un esdeveniment o missatge abans de confirmar els canvis a la base de dades local.
Tots dos mètodes tenen els seus inconvenients i només són parcialment segurs per a la comunicació en sistemes distribuïts.
Aquest és un diagrama de seqüència d'aplicació del primer enfocament.
En aquest escenari, el servei de sol·licitud de préstec utilitza l'enfocament First-Local-Commit-Then-Publish , on primer confirma una transacció i després intenta enviar una notificació a un altre sistema. Tanmateix, aquest procés és susceptible de fallar si, per exemple, hi ha problemes de xarxa, el servei d'avaluació no està disponible o el servei de sol·licitud de préstec troba un error de memòria sense memòria (OOM) i falla. En aquests casos, el missatge es perdria, deixant l'Avaluació sense avís de la nova sol·licitud de préstec, llevat que s'implementin mesures addicionals.
I el segon.
En l'escenari First-Publish-Then-Local-Commit , el servei de sol·licitud de préstec s'enfronta a riscos més importants. Pot ser que informi el Servei d'Avaluació sobre una aplicació nova, però no desa aquesta actualització localment a causa de problemes com ara problemes de base de dades, errors de memòria o errors de codi. Aquest enfocament pot comportar inconsistències importants en les dades, que podrien causar problemes greus, depenent de com el Servei de revisió de préstecs gestioni les sol·licituds entrants.
Per tant, hem d'identificar una solució que ofereixi un mecanisme robust per publicar esdeveniments a consumidors externs. Però, abans d'aprofundir en possibles solucions, primer hauríem d'aclarir els tipus de garanties de lliurament de missatges que es poden aconseguir en sistemes distribuïts.
Hi ha quatre tipus de garanties que podríem aconseguir.
Sense garanties
No hi ha cap garantia que el missatge s'entregui a la destinació. L'enfocament First-Local-Commit-Then-Publish tracta precisament d'això. Els consumidors poden rebre missatges una vegada, diverses vegades o mai.
Com a màxim una vegada lliurament
Com a màxim un lliurament significa que el missatge s'entregarà a la destinació com a màxim 1 vegada. L'enfocament First-Local-Commit-Then-Publish es pot implementar d'aquesta manera també amb la política de reintents d'intents amb el valor 1.
Almenys un lliurament\Els consumidors rebran i processaran tots els missatges, però poden rebre el mateix missatge més d'una vegada.
Un lliurament exacte\Una entrega exacta significa que el consumidor rebrà el missatge de manera efectiva una vegada.
Tècnicament, és possible aconseguir amb Kafka transaccions i implementació idempotent específica de productor i consumidor.
En la majoria dels casos, les garanties de lliurament "almenys una vegada" resolen molts problemes assegurant-se que els missatges s'entreguen almenys una vegada, però els consumidors han de ser idempotents. No obstant això, ateses les fallades inevitables de la xarxa, tota la lògica del consumidor ha de ser idempotent per evitar processar missatges duplicats, independentment de les garanties del productor. Per tant, aquest requisit no és tant un inconvenient sinó que reflecteix la realitat.
Hi ha moltes solucions a aquest problema, que tenen els seus avantatges i desavantatges.
Segons la Viquipèdia, el Two-Phase Commit (2PC) és un protocol de transacció distribuït utilitzat en sistemes de gestió de bases de dades i informàtica per garantir la coherència i la fiabilitat de les transaccions distribuïdes. Està dissenyat per a situacions en què diversos recursos (p. ex., bases de dades) han de participar en una única transacció i assegura que tots ells cometen la transacció o que tots l'avortin, mantenint així la coherència de les dades. Sembla exactament el que necessitem, però el compromís en dues fases té diversos inconvenients:
La solució més aparent per a l'arquitectura de microserveis és aplicar un patró (o fins i tot de vegades anti-patró): una base de dades compartida. Aquest enfocament és molt intuïtiu si necessiteu coherència transaccional en diverses taules de diferents bases de dades, només feu servir una base de dades compartida per a aquests microserveis.
Els inconvenients d'aquest enfocament inclouen la introducció d'un únic punt de fallada, la inhibició de l'escala de la base de dades independent i la limitació de la capacitat d'utilitzar diferents solucions de bases de dades més adequades per a requisits i casos d'ús específics. A més, serien necessàries modificacions a les bases de codi dels microserveis per donar suport a aquesta forma de transacció distribuïda.
La " safata de sortida transaccional " és un patró de disseny utilitzat en sistemes distribuïts per garantir una propagació fiable dels missatges, fins i tot davant de sistemes de missatgeria poc fiables. Implica emmagatzemar esdeveniments en una taula "OutboxEvents" designada dins de la mateixa transacció que la pròpia operació. Aquest enfocament s'alinea bé amb les propietats ACID de les bases de dades relacionals. En canvi, moltes bases de dades No-SQL no admeten completament les propietats ACID, optant en canvi pels principis del teorema CAP i la filosofia BASE, que prioritzen la disponibilitat i la consistència eventual per sobre de la coherència estricta.
Una bústia de sortida transaccional ofereix almenys una garantia i es pot implementar amb diversos enfocaments:
Tailing del registre de transaccions
Editor d'enquestes
L'enfocament del registre de transaccions implica l'ús de solucions específiques de bases de dades com CDC (Change Data Capture). Els principals inconvenients d'aquest enfocament són:
Solucions específiques de bases de dades
Augment de la latència a causa de les especificitats de les implementacions de CDC
Un altre mètode és el Polling Publisher , que facilita la descàrrega de la safata de sortida consultant la taula de la safata de sortida. El principal inconvenient d'aquest enfocament és la possibilitat d'augmentar la càrrega de la base de dades, que pot comportar costos més elevats. A més, no totes les bases de dades No-SQL admeten consultes eficients per a segments de documents específics. Per tant, extreure documents sencers pot provocar una degradació del rendiment.
Aquí teniu un petit diagrama de seqüència que explica com funciona.
El repte principal amb el patró de la bústia de sortida transaccional rau en la seva dependència de les propietats ACID de la base de dades. Pot ser senzill a les bases de dades OLTP típiques, però planteja reptes a l'àmbit NoSQL. Per solucionar-ho, una solució potencial és aprofitar el registre d'afegir (per exemple, Kafka) des d'iniciar el processament de la sol·licitud.
En lloc de processar directament l'ordre "envia la sol·licitud de préstec", l'enviem immediatament a un tema intern de Kafka i després retornem un resultat "acceptat" al client. Tanmateix, com que és molt probable que l'ordre encara s'hagi de processar, no podem informar immediatament el resultat del client. Per gestionar aquesta eventual coherència, podem utilitzar tècniques com ara sondejos llargs, sondejos iniciats pel client, actualitzacions optimistes de la interfície d'usuari o utilitzar WebSockets o esdeveniments enviats pel servidor per a les notificacions. Tanmateix, aquest és un tema completament diferent, així que tornem al nostre tema inicial.
Hem enviat el missatge sobre un tema intern de Kafka. Aleshores, el servei de sol·licitud de préstec consumeix aquest missatge, la mateixa ordre que va rebre del client, i comença a processar-se. En primer lloc, executa una mica de lògica de negoci; només després que aquesta lògica s'executi correctament i els resultats es persisteixen, publica nous missatges sobre un tema públic de Kafka.
Fem una ullada a una mica de pseudocodi.
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(); }
Què passa si falla el processament de la lògica de negoci? No us preocupeu, com que la compensació encara no s'ha compromès, el missatge es tornarà a provar.
Què passa si falla l'enviament de nous esdeveniments a Kafka? No et preocupis, ja que la lògica empresarial és idempotent, no crearà una sol·licitud de préstec duplicada. En canvi, intentarà tornar a enviar missatges al tema públic de Kafka.
Què passa si s'envien missatges a Kafka, però la confirmació de compensació falla? No us preocupeu, ja que la lògica empresarial és idempotent, no crearà una sol·licitud de préstec duplicada. En lloc d'això, tornarà a enviar missatges al tema públic de Kafka i espera que aquesta vegada el compromís de compensació tingui èxit.
Els principals inconvenients d'aquest enfocament inclouen la complexitat afegida associada a un nou estil de programació, la coherència eventual (ja que el client no coneixerà immediatament el resultat) i l'exigència que tota la lògica empresarial sigui idempotent.
Què és l'abastament d'esdeveniments i com es podria aplicar aquí? L'abastament d'esdeveniments és un patró arquitectònic de programari utilitzat per modelar l'estat d'un sistema capturant tots els canvis a les seves dades com una sèrie d'esdeveniments immutables. Aquests esdeveniments representen fets o transicions d'estat i serveixen com a font única de veritat per a l'estat actual del sistema. Per tant, tècnicament, mitjançant la implementació d'un sistema d'abastament d'esdeveniments, ja tenim tots els esdeveniments a EventStore, i els consumidors poden utilitzar aquesta EventStore com a font única de veritat sobre el que va passar. No hi ha necessitat d'una solució de base de dades específica per fer el seguiment de tots els canvis o preocupacions sobre la comanda, l'únic problema és estar al costat de la lectura, ja que per poder obtenir l'estat real de l'entitat cal reproduir tots els esdeveniments.
En aquest article, hem revisat diversos enfocaments per crear missatgeria fiable en sistemes distribuïts. Hi ha diverses recomanacions que podem tenir en compte a l'hora de construir sistemes amb aquestes característiques
La propera vegada, veurem un exemple més pràctic d'implementació d'una bústia de sortida transaccional. Vegeu
tu!