Qual è la strategia di transazione più accettata per i microservizi


80

Uno dei principali problemi che ho riscontrato in un sistema con microservizi è il modo in cui le transazioni funzionano quando si estendono su servizi diversi. All'interno della nostra architettura, abbiamo utilizzato le transazioni distribuite per risolvere questo problema, ma presentano problemi propri. Soprattutto i deadlock sono stati un dolore finora.

Un'altra opzione sembra essere una sorta di gestore delle transazioni su misura, che conosce i flussi all'interno del tuo sistema e si occuperà dei rollback per te come un processo in background che si estende su tutto il sistema (quindi dirà agli altri servizi di rollback e se sono inattivi, avvisali più tardi).

C'è un'altra opzione accettata? Entrambi sembrano avere i loro svantaggi. Il primo potrebbe causare deadlock e una serie di altri problemi, il secondo potrebbe comportare un'incoerenza dei dati. Ci sono opzioni migliori?


Giusto per essere sicuri, quei microservizi sono utilizzati da molti client contemporaneamente o solo uno alla volta?
Marcel,

2
Stavo cercando di porre questa domanda agnostica a quello, Marcel. Ma supponiamo che stiamo usando un sistema abbastanza grande per fare entrambe le cose, e vogliamo avere un'architettura che supporti entrambi.
Kristof,

4
Questa è una domanda mal formulata, ma molto importante. Quando la maggior parte delle persone pensa alle "transazioni", pensa quasi esclusivamente alla coerenza e non agli aspetti di atomicità, isolamento o durata delle transazioni. La domanda dovrebbe in realtà essere formulata "Come si crea un sistema compatibile ACID con un'architettura a microservizi. Solo implementare la coerenza e non il resto di ACID non è davvero utile. A meno che non vi piacciano i dati errati.
Jeff Fischer

10
Puoi sempre provare a modificare la mia domanda per renderla meno "mal formulata", Jeff Fischer.
Kristof

Questa domanda e discussione sono strettamente correlate a quest'altra domanda sulla SO .
Paulo Merson,

Risposte:


42

L'approccio abituale è quello di isolare il più possibile quei microservizi: trattarli come singole unità. Quindi le transazioni possono essere sviluppate nel contesto del servizio nel suo insieme (cioè non parte delle normali transazioni DB, anche se è ancora possibile avere transazioni DB interne al servizio).

Pensa a come avvengono le transazioni e che tipo hanno senso per i tuoi servizi, quindi puoi implementare un meccanismo di rollback che annulla l'operazione originale o un sistema di commit in 2 fasi che riserva l'operazione originale fino a quando non viene detto di impegnarsi per davvero. Naturalmente entrambi questi sistemi significano che stai implementando i tuoi, ma poi stai già implementando i tuoi microservizi.

I servizi finanziari fanno sempre questo genere di cose: se voglio trasferire denaro dalla mia banca alla tua banca, non esiste un'unica transazione come quella che avresti in un DB. Non sai quali sistemi sono in esecuzione su entrambi i banchi, quindi devi trattarli efficacemente come i tuoi microservizi. In questo caso, la mia banca sposterebbe i miei soldi dal mio conto a un conto di deposito e quindi direbbe alla tua banca che avevano dei soldi, se l'invio fallisce, la mia banca rimborserà il mio conto con i soldi che hanno provato a inviare.


5
Stai presupponendo che il rimborso non possa mai fallire.
Sanjeev Kumar Dangi,

9
@SanjeevKumarDangi è meno probabile e anche se fallisce, può essere facilmente gestito dal momento che il conto di deposito e il conto reale sono sotto il controllo della banca.
Gldraphael,

30

Penso che la saggezza standard sia quella di non avere mai transazioni che superino i confini del microservizio. Se un dato set di dati deve davvero essere atomicamente coerente con un altro, queste due cose si uniscono.

Questo è uno dei motivi per cui è molto difficile dividere un sistema in servizi fino a quando non è stato progettato completamente. Che nel mondo moderno probabilmente significa averlo scritto ...


37
Tale approccio potrebbe benissimo portare alla fusione di tutti i microservizi in un'unica applicazione monolitica alla fine.
Slava Fomin II,

4
Questo è l'approccio corretto. In realtà è semplice: se hai bisogno di una transazione tra servizi, i tuoi servizi sono sbagliati: riprogettali! @SlavaFominII quello che dici è vero solo se non sai come progettare un sistema di microservizi. Se ti ritrovi a combattere l'approccio dei microservizi, non farlo, il tuo monolito sarà migliore di un cattivo design dei microservizi. Solo quando trovi i giusti limiti di servizio è quando dovresti dividere il monolito nei servizi. Altrimenti l'uso dei microservizi non è la scelta architettonica giusta, sta solo seguendo l'hype.
Francesc Castells,

@FrancescCastells Che cosa succede se i nostri servizi richiedono realmente transazioni tra altri servizi, vuoi dire che dovremmo ignorare i contesti limitati e modellare i nostri servizi in modo che finiscano come un'unica transazione? Sono un principiante nei microservizi, sto ancora leggendo quindi scusate la mia domanda se sembra ingenuo ....: D: D
Bilbo Baggins

@BilboBaggins Intendo il contrario di ignorare contesti limitati. Per definizione, le transazioni avvengono in un contesto limitato e non attraverso diverse di esse. Pertanto, se si progettano correttamente i microservizi insieme ai contesti limitati, le transazioni non dovrebbero comprendere più servizi. Si noti che, a volte, ciò che è necessario non sono le transazioni, ma una migliore gestione dell'eventuale coerenza e azioni di compensazione adeguate quando le cose non vanno correttamente.
Francesc Castells,

Non capisco il tuo punto, c'è una possibilità che possiamo discuterne in una chat?
Bilbo Baggins,

17

Penso che se la coerenza è un requisito fondamentale nella tua applicazione, dovresti chiederti se i microservizi sono l'approccio migliore. Come dice Martin Fowler :

I microservizi introducono eventuali problemi di coerenza a causa della loro lodevole insistenza sulla gestione decentralizzata dei dati. Con un monolite, puoi aggiornare un sacco di cose insieme in una singola transazione. I microservizi richiedono più risorse per l'aggiornamento e le transazioni distribuite sono disapprovate (per una buona ragione). Quindi ora gli sviluppatori devono essere consapevoli dei problemi di coerenza e capire come rilevare quando le cose non sono sincronizzate prima di fare qualcosa di cui il codice si pentirà.

Ma forse nel tuo caso, puoi sacrificare la coerenza rispetto alla disponibilità

I processi aziendali sono spesso più tolleranti rispetto alle incongruenze di quanto si pensi perché le aziende spesso premiano maggiormente la disponibilità.

Tuttavia, mi chiedo anche se esiste una strategia per le transazioni distribuite nei microservizi, ma forse il costo è troppo elevato. Volevo darti i miei due centesimi con l'articolo sempre eccellente di Martin Fowler e il teorema della PAC .


1
Nelle transazioni commerciali distribuite, la coerenza viene talvolta risolta aggiungendo un livello di orchestrazione come un motore del flusso di lavoro o una coda accuratamente progettata. Tale livello gestisce tutte e due le fasi di commit e rollback e consente ai microservizi di concentrarsi su una logica aziendale specifica. Ma torniamo alla PAC, investire in disponibilità e coerenza rende le prestazioni la vittima. Come si confrontano i microservizi con molte classi disaccoppiate che comprendono la tua attività in OOP?
Greg,

È possibile utilizzare RAFT tramite microservizi per indirizzare la coerenza FWIW
f0ster

1
+1. Spesso mi chiedo perché in un contesto di microservizi le transazioni siano desiderate, se tutte le viste 'pull' dei dati possono essere implementate come viste materializzate. Ad esempio, se un microservizio implementa debiti da un conto e altri crediti verso un altro conto, le viste dei saldi considererebbero solo coppie di crediti e debiti, in cui i crediti e i debiti senza pari sarebbero ancora nei buffer della vista materializzata.
Sentinella

16

Come suggerito in almeno una delle risposte qui, ma anche in altre parti del Web, è possibile progettare un microservizio che persista le entità insieme all'interno di una normale transazione se è necessaria la coerenza tra le due entità.

Ma allo stesso tempo, potresti avere la situazione in cui le entità in realtà non appartengono allo stesso microservizio, ad esempio i registri delle vendite e i registri degli ordini (quando ordini qualcosa per eseguire la vendita). In tali casi, potrebbe essere necessario un modo per garantire la coerenza tra i due microservizi.

Sono state utilizzate le transazioni tradizionalmente distribuite e, nella mia esperienza, funzionano bene fino a quando non si adattano a dimensioni in cui il blocco diventa un problema. Puoi rilassare il blocco in modo tale che solo le risorse rilevanti (ad esempio l'oggetto in vendita) siano "bloccate" usando un cambio di stato, ma è qui che inizia a diventare complicato perché stai entrando nel territorio in cui devi costruire tutto il logica per fare questo, tu stesso, piuttosto che avere, dire un database gestirlo per te.

Ho lavorato con aziende che hanno intrapreso la strada della creazione di un proprio framework di transazioni per gestire questo problema complesso, ma non lo consiglio perché è costoso e richiede tempo per maturare.

Ci sono prodotti là fuori che possono essere fissati al tuo sistema che si prendono cura della coerenza. Un motore di processo aziendale è un buon esempio e in genere gestisce la coerenza alla fine e utilizzando la compensazione. Altri prodotti funzionano in modo simile. Di solito si finisce con uno strato di software vicino al / i cliente / i, che si occupa di coerenza e transazioni e chiama (micro) servizi per eseguire la vera elaborazione aziendale . Uno di questi prodotti è un connettore JCA generico che può essere utilizzato con soluzioni Java EE (per trasparenza: sono l'autore). Vedi http://blog.maxant.co.uk/pebble/2015/08/04/1438716480000.html per maggiori dettagli e una discussione più approfondita delle questioni sollevate qui.

Un altro modo di gestire le transazioni e la coerenza è avvolgere una chiamata a un microservizio in una chiamata a qualcosa di transazionale come una coda di messaggi. Prendi l'esempio del record di vendita / record di ordine dall'alto: potresti semplicemente consentire al microservizio di vendita di inviare un messaggio al sistema degli ordini, che è impegnato nella stessa transazione che scrive la vendita nel database. Il risultato è una soluzione asincrona che si adatta molto bene. Utilizzando tecnologie come i socket Web, è anche possibile aggirare il problema del blocco, che è spesso correlato al ridimensionamento delle soluzioni asincrone. Per ulteriori idee su modelli come questo, vedi un altro dei miei articoli: http://blog.maxant.co.uk/pebble/2015/08/11/1439322480000.html .

Qualunque sia la soluzione che sceglierai, è importante riconoscere che solo una piccola parte del tuo sistema scriverà cose che devono essere coerenti - la maggior parte degli accessi è probabilmente di sola lettura. Per tale motivo, incorporare la gestione delle transazioni solo nelle parti pertinenti del sistema, in modo che possa comunque ridimensionarsi bene.


Nel frattempo, direi che si dovrebbe seriamente considerare di trasformare il processo in un processo asincrono, in cui ogni fase del processo è completamente transazionale. Vedi qui per i dettagli: blog.maxant.co.uk/pebble/2018/02/18/1518974314273.html
Ant Kutschera

"Crea il record di vendita / esempio di record di ordine dall'alto: potresti semplicemente consentire al microservizio di vendita di inviare un messaggio al sistema di ordini, che è impegnato nella stessa transazione che scrive la vendita nel database." Non sei sicuro che ciò che stai suggerendo sia fondamentalmente una transazione distribuita, che ha detto, come gestiresti lo scenario di rollback in questo caso? Ad esempio, il messaggio si impegna nella coda dei messaggi ma è stato eseguito il rollback sul lato DB.
Sactiw

@sactiw non è sicuro di aver avuto in mente un commit in due fasi, ma eviterei che ora e invece scriva i miei dati aziendali e il fatto che un messaggio debba essere aggiunto alla coda, in una transazione al mio microservice DB . Il "fatto", noto anche come "comando", viene quindi elaborato in modo asincrono dopo il commit della transazione, utilizzando un meccanismo di tentativo automatico. Vedi l'articolo del blog del 10-03-2018 per un esempio. Evitare il rollback o la compensazione a favore di una strategia di forward, se possibile, poiché è più facile da implementare.
Ant Kutschera,


1

Esistono molte soluzioni che compromettono più di quanto mi trovo a mio agio. Certo, se il tuo caso d'uso è complesso, come spostare denaro tra banche diverse, alternative più piacevoli potrebbero essere impossibili. Ma diamo un'occhiata a cosa possiamo fare nello scenario comune, in cui l'uso dei microservizi interferisce con le nostre potenziali transazioni di database.

Opzione 1: evitare la necessità di transazioni, se possibile

Ovvio e menzionato prima, ma ideale se riusciamo a gestirlo. I componenti appartenevano effettivamente allo stesso microservizio? Oppure possiamo ridisegnare i sistemi in modo tale che la transazione diventi superflua? Forse accettare la non transazione è il sacrificio più conveniente.

Opzione 2: utilizzare una coda

Se c'è abbastanza certezza che l'altro servizio riuscirà in qualunque modo vogliamo, possiamo chiamarlo tramite una qualche forma di coda. L'articolo in coda non verrà ritirato fino a dopo, ma possiamo assicurarci che l'articolo sia in coda .

Ad esempio, supponiamo che vogliamo inserire un'entità e inviare un'e-mail, come un'unica transazione. Invece di chiamare il server di posta, accodiamo l'e-mail in una tabella.

Begin transaction
Insert entity
Insert e-mail
Commit transaction

Un chiaro svantaggio è che più microservizi dovranno accedere alla stessa tabella.

Opzione 3: eseguire il lavoro esterno per ultimo, appena prima di completare la transazione

Questo approccio si basa sul presupposto che è improbabile che fallisca la commissione.

Begin transaction
Insert entity
Insert another entity
Make external call
Commit transaction

Se le query falliscono, la chiamata esterna non ha ancora avuto luogo. Se la chiamata esterna non riesce, la transazione non viene mai impegnata.

Questo approccio ha i limiti che possiamo fare solo una chiamata esterna, e deve essere fatto per ultimo (cioè non possiamo usare il suo risultato nelle nostre query).

Opzione 4: creare oggetti in uno stato in sospeso

Come pubblicato qui , possiamo far sì che più microservizi creino componenti diversi, ciascuno in uno stato in sospeso, non a livello transazionale.

Viene eseguita qualsiasi convalida, ma nulla viene creato in uno stato definitivo. Dopo che tutto è stato creato con successo, ogni componente viene attivato. Di solito, questa operazione è così semplice e le probabilità che qualcosa vada storto sono così ridotte, che potremmo addirittura preferire fare l'attivazione in modo non transazionale.

Il più grande svantaggio è probabilmente che dobbiamo tenere conto dell'esistenza di articoli in sospeso. Qualsiasi query selezionata deve considerare se includere dati in sospeso. La maggior parte dovrebbe ignorarlo. E gli aggiornamenti sono del tutto un'altra storia.

Opzione 5: consenti al microservizio di condividere la sua query

Nessuna delle altre opzioni lo fa per te? Allora diventiamo poco ortodossi .

A seconda dell'azienda, questo potrebbe essere inaccettabile. Sono consapevole. Questo non è ortodosso. Se non è accettabile, segui un'altra strada. Ma se questo si adatta alla tua situazione, risolve il problema in modo semplice e potente. Potrebbe essere solo il compromesso più accettabile.

Esiste un modo per trasformare le query da più microservizi in una semplice transazione con un solo database.

Restituisce la query, anziché eseguirla.

Begin transaction
Execute our own query
Make external call, receiving a query
Execute received query
Commit transaction

Per quanto riguarda la rete, ogni microservizio deve poter accedere a ciascun database. Tienilo a mente, anche per quanto riguarda il ridimensionamento futuro.

Se i database coinvolti nella transazione si trovano sullo stesso server, questa sarà una transazione regolare. Se si trovano su server diversi, sarà una transazione distribuita. Il codice è lo stesso a prescindere.

Riceviamo la query, incluso il suo tipo di connessione, i suoi parametri e la sua stringa di connessione. Possiamo racchiuderlo in un'ordinata classe Command eseguibile, mantenendo il flusso leggibile: la chiamata al microservizio si traduce in un comando, che eseguiamo, come parte della nostra transazione.

La stringa di connessione è ciò che ci dà il microservizio di origine, quindi a tutti gli effetti la query è ancora considerata eseguita da quel microservizio. Lo stiamo semplicemente instradando fisicamente attraverso il microservizio client. Questo fa la differenza? Bene, ci consente di inserirlo nella stessa transazione con un'altra query.

Se il compromesso è accettabile, questo approccio ci offre la semplice transazionalità di un'applicazione monolitica, in un'architettura a microservizi.


0

Vorrei iniziare con la scomposizione dello spazio problematico, identificando i confini del servizio . Se eseguito correttamente, non dovresti mai avere transazioni tra servizi.

Servizi diversi hanno i propri dati, comportamenti, forze motivazionali, governo, regole aziendali, ecc. Un buon inizio è elencare le capacità di alto livello della vostra azienda. Ad esempio, marketing, vendite, contabilità, supporto. Un altro punto di partenza è la struttura organizzativa, ma tieni presente che esiste un avvertimento: per alcuni motivi (politici, ad esempio) potrebbe non essere lo schema di decomposizione aziendale ottimale. Un approccio più rigoroso è l' analisi della catena del valore . Ricorda, i tuoi servizi possono includere anche persone, non è strettamente software. I servizi dovrebbero comunicare tra loro tramite eventi .

Il prossimo passo è quello di scolpire questi servizi. Di conseguenza si ottengono aggregati ancora relativamente indipendenti . Rappresentano un'unità di coerenza. In altre parole, i loro interni dovrebbero essere coerenti e ACIDI. Gli aggregati comunicano tra loro tramite eventi.

Se ritieni che il tuo dominio richieda innanzitutto coerenza, ripensaci. Nessuno dei grandi sistemi mission-critical è stato creato tenendo presente questo obiettivo. Sono tutti distribuiti e alla fine coerenti. Controlla la classica carta di Pat Helland .

Ecco alcuni consigli pratici su come costruire un sistema distribuito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.