![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Creare un sistema distribuito affidabile, altamente disponibile e scalabile richiede l'aderenza a tecniche, principi e modelli specifici. La progettazione di tali sistemi implica l'affrontare una miriade di sfide. Tra i problemi più diffusi e fondamentali c'è il problema della scrittura duale .
Il "dual write problem" è una sfida che si presenta nei sistemi distribuiti, principalmente quando si ha a che fare con più fonti di dati o database che devono essere mantenuti sincronizzati. Si riferisce alla difficoltà di garantire che le modifiche dei dati vengano scritte in modo coerente su vari archivi dati, come database o cache, senza introdurre problemi come incongruenze nei dati, conflitti o colli di bottiglia nelle prestazioni.
L'architettura dei microservizi e il database di pattern per servizio ti offrono molti vantaggi, come distribuzione e scalabilità indipendenti, guasti isolati e un potenziale aumento della velocità di sviluppo. Tuttavia, le operazioni richiedono modifiche tra più microservizi, costringendoti a pensare a una soluzione affidabile per affrontare questo problema.
Consideriamo uno scenario in cui il nostro dominio comporta l'accettazione di richieste di prestito, la loro valutazione e l'invio di avvisi di notifica ai clienti.
Nello spirito del principio di responsabilità unica, della legge di Conway e dell'approccio di progettazione basato sul dominio, dopo diverse sessioni di event storming, l'intero dominio è stato suddiviso in tre sottodomini con contesti delimitati definiti, con confini chiari, modelli di dominio e linguaggio onnipresente.
Il primo è incaricato di onboarding e compilazione di nuove domande di prestito. Il secondo sistema valuta queste domande e prende decisioni in base ai dati forniti. Questo processo di valutazione, inclusi i controlli KYC/KYB, antifrode e di rischio di credito, può richiedere molto tempo, rendendo necessaria la capacità di gestire migliaia di domande contemporaneamente. Di conseguenza, questa funzionalità è stata delegata a un microservizio dedicato con il proprio database, consentendo un ridimensionamento indipendente.
Inoltre, questi sottosistemi sono gestiti da due team diversi, ciascuno con i propri cicli di rilascio, accordi sul livello di servizio (SLA) e requisiti di scalabilità.
Infine , è attivo un servizio di notifica specializzato per inviare avvisi ai clienti.
Ecco una descrizione dettagliata del caso d'uso principale del sistema:
A prima vista sembra un sistema piuttosto semplice e primitivo, ma analizziamo nel dettaglio come il servizio di richiesta di prestito elabora il comando di invio della richiesta di prestito.
Possiamo considerare due approcci per le interazioni di servizio:
First-Local-Commit-Then-Publish: con questo approccio, il servizio aggiorna il suo database locale (esegue commit) e poi pubblica un evento o un messaggio su altri servizi.
First-Publish-Then-Local-Commit: al contrario, questo metodo prevede la pubblicazione di un evento o di un messaggio prima di confermare le modifiche nel database locale.
Entrambi i metodi presentano degli svantaggi e sono solo parzialmente sicuri per la comunicazione nei sistemi distribuiti.
Questo è uno schema sequenziale dell'applicazione del primo approccio.
In questo scenario, il Loan Application Service impiega l'approccio First-Local-Commit-Then-Publish , in cui prima esegue una transazione e poi tenta di inviare una notifica a un altro sistema. Tuttavia, questo processo è suscettibile di fallimento se, ad esempio, ci sono problemi di rete, l'Assessment Service non è disponibile o il Loan Application Service incontra un errore Out of Memory (OOM) e si blocca. In tali casi, il messaggio andrebbe perso, lasciando l'Assessment senza notifica della nuova richiesta di prestito, a meno che non vengano implementate misure aggiuntive.
E il secondo.
Nello scenario First-Publish-Then-Local-Commit , il Loan Application Service affronta rischi più significativi. Potrebbe informare l'Assessment Service di una nuova domanda ma non riuscire a salvare questo aggiornamento localmente a causa di problemi come problemi di database, errori di memoria o bug del codice. Questo approccio può portare a significative incongruenze nei dati, che potrebbero causare seri problemi, a seconda di come il Loan Review Service gestisce le domande in arrivo.
Pertanto, dobbiamo identificare una soluzione che offra un meccanismo robusto per pubblicare eventi su consumatori esterni. Ma, prima di addentrarci in possibili soluzioni, dovremmo prima chiarire i tipi di garanzie di recapito dei messaggi ottenibili nei sistemi distribuiti.
Esistono quattro tipi di garanzie che potremmo ottenere.
Nessuna garanzia
Non c'è alcuna garanzia che il messaggio verrà recapitato a destinazione. L'approccio First-Local-Commit-Then-Publish riguarda proprio questo. I consumatori possono ricevere messaggi una volta, più volte o mai.
Al massimo una consegna
La consegna al massimo una volta significa che il messaggio verrà consegnato alla destinazione al massimo 1 volta. L'approccio First-Local-Commit-Then-Publish può essere implementato anche in questo modo con la politica di ripetizione dei tentativi con valore uno.
Almeno una volta consegnato\I consumatori riceveranno ed elaboreranno ogni messaggio, ma potrebbero ricevere lo stesso messaggio più di una volta.
Consegna esattamente una volta\La consegna esattamente una volta significa che il consumatore riceverà il messaggio una sola volta.
Tecnicamente, è possibile ottenerlo con transazioni Kafka e con l'implementazione idempotente specifica di produttore e consumatore.
Nella maggior parte dei casi, le garanzie di consegna "almeno una volta" affrontano molti problemi assicurando che i messaggi vengano recapitati almeno una volta, ma i consumatori devono essere idempotenti. Tuttavia, dati gli inevitabili guasti di rete, tutta la logica del consumatore deve essere idempotente per evitare di elaborare messaggi duplicati, indipendentemente dalle garanzie del produttore. Pertanto, questo requisito non è tanto uno svantaggio quanto riflette la realtà.
Esistono numerose soluzioni a questo problema, ciascuna con i suoi vantaggi e svantaggi.
Secondo Wikipedia, il Two-Phase Commit (2PC) è un protocollo di transazione distribuito utilizzato in informatica e nei sistemi di gestione di database per garantire la coerenza e l'affidabilità delle transazioni distribuite. È progettato per situazioni in cui più risorse (ad esempio, database) devono partecipare a una singola transazione e garantisce che tutte eseguano il commit della transazione o che tutte eseguano l'interruzione, mantenendo così la coerenza dei dati. Sembra esattamente ciò di cui abbiamo bisogno, ma il Two-Phase Commit presenta diversi svantaggi:
La soluzione più evidente per l'architettura dei microservizi è quella di applicare un pattern (o talvolta anche un anti-pattern) — un database condiviso. Questo approccio è molto intuitivo se hai bisogno di coerenza transazionale su più tabelle in database diversi, usa semplicemente un database condiviso per questi microservizi.
Gli svantaggi di questo approccio includono l'introduzione di un singolo punto di errore, l'inibizione della scalabilità indipendente del database e la limitazione della capacità di utilizzare diverse soluzioni di database più adatte a requisiti e casi d'uso specifici. Inoltre, modifiche alle basi di codice dei microservizi sarebbero necessarie per supportare tale forma di transazione distribuita.
Il " transactional outbox " è un modello di progettazione utilizzato nei sistemi distribuiti per garantire una propagazione affidabile dei messaggi, anche di fronte a sistemi di messaggistica inaffidabili. Comporta l'archiviazione degli eventi in una tabella "OutboxEvents" designata all'interno della stessa transazione dell'operazione stessa. Questo approccio si allinea bene con le proprietà ACID dei database relazionali. Al contrario, molti database No-SQL non supportano completamente le proprietà ACID, optando invece per i principi del teorema CAP e della filosofia BASE, che danno priorità alla disponibilità e alla coerenza finale rispetto alla coerenza rigorosa.
Una casella di posta in uscita transazionale fornisce almeno una garanzia e può essere implementata con diversi approcci:
Registrazione del registro delle transazioni
Editore di sondaggi
L'approccio di tailing del registro delle transazioni implica l'utilizzo di soluzioni specifiche del database come CDC (Change Data Capture). I principali svantaggi di tale approccio sono:
Soluzioni specifiche per database
Aumento della latenza dovuto alle specificità delle implementazioni CDC
Un altro metodo è Polling Publisher , che facilita l'offload della posta in uscita interrogando la tabella della posta in uscita. Lo svantaggio principale di questo approccio è il potenziale aumento del carico del database, che può portare a costi più elevati. Inoltre, non tutti i database No-SQL supportano query efficienti per segmenti di documenti specifici. L'estrazione di interi documenti può, quindi, causare un degrado delle prestazioni.
Ecco un piccolo diagramma sequenziale che spiega come funziona.
La sfida principale con il modello Transactional Outbox risiede nella sua dipendenza dalle proprietà ACID del database. Potrebbe essere semplice nei tipici database OLTP, ma pone delle sfide nel regno NoSQL. Per risolvere questo problema, una possibile soluzione è sfruttare il log di aggiunta (ad esempio, Kafka) fin dall'avvio dell'elaborazione della richiesta.
Invece di elaborare direttamente il comando "submit loan application", lo inviamo immediatamente a un argomento Kafka interno e quindi restituiamo un risultato "accepted" al client. Tuttavia, poiché è altamente probabile che il comando debba ancora essere elaborato, non possiamo informare immediatamente il cliente del risultato. Per gestire questa eventuale coerenza, possiamo impiegare tecniche come il long polling, il client-initiated polling, gli aggiornamenti UI ottimistici o l'utilizzo di WebSocket o Server-Sent Events per le notifiche. Tuttavia, questo è un argomento completamente diverso, quindi torniamo al nostro argomento iniziale.
Abbiamo inviato il messaggio su un argomento Kafka interno. Il Loan Application Service quindi consuma questo messaggio, lo stesso comando ricevuto dal client, e inizia l'elaborazione. Innanzitutto, esegue una logica aziendale; solo dopo che questa logica è stata eseguita correttamente e i risultati sono stati resi persistenti, pubblica nuovi messaggi su un argomento Kafka pubblico.
Diamo un'occhiata a un po' di pseudo-codice.
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(); }
Cosa succede se l'elaborazione della logica aziendale fallisce? Nessun problema, poiché l'offset non è ancora stato eseguito, il messaggio verrà ritentato.
Cosa succede se l'invio di nuovi eventi a Kafka fallisce? Nessun problema, poiché la logica aziendale è idempotente, non creerà una richiesta di prestito duplicata. Invece, tenterà di inviare nuovamente i messaggi all'argomento pubblico di Kafka.
Cosa succede se i messaggi vengono inviati a Kafka, ma il commit offset fallisce? Nessun problema, poiché la logica aziendale è idempotente, non creerà una richiesta di prestito duplicata. Invece, invierà nuovamente i messaggi al topic pubblico di Kafka e spererà che il commit offset questa volta vada a buon fine.
Gli svantaggi principali di questo approccio includono la maggiore complessità associata a un nuovo stile di programmazione, la coerenza finale (poiché il cliente non conoscerà immediatamente il risultato) e il requisito che tutta la logica aziendale sia idempotente.
Cos'è l'event sourcing e come potrebbe essere applicato qui? L'event sourcing è un modello di architettura software utilizzato per modellare lo stato di un sistema catturando tutte le modifiche ai suoi dati come una serie di eventi immutabili. Questi eventi rappresentano fatti o transizioni di stato e servono come unica fonte di verità per lo stato attuale del sistema. Quindi, tecnicamente, implementando un sistema di event sourcing, abbiamo già tutti gli eventi in EventStore e questo EventStore può essere utilizzato dai consumatori come unica fonte di verità su ciò che è accaduto. Non c'è bisogno di una soluzione di database specifica per tracciare tutte le modifiche o preoccupazioni sull'ordinamento, l'unico problema è sedersi sul lato di lettura poiché per essere in grado di ottenere lo stato effettivo dell'entità è necessario riprodurre tutti gli eventi.
In questo articolo, abbiamo esaminato diversi approcci per la creazione di messaggistica affidabile nei sistemi distribuiti. Ci sono diverse raccomandazioni che potremmo prendere in considerazione durante la creazione di sistemi con queste caratteristiche
La prossima volta esamineremo un esempio più pratico di implementazione di una posta in uscita transazionale. Vedere
Voi!