Eseguire il refactoring di un monolite in microservizi

Last reviewed 2024-06-26 UTC

Questa guida di riferimento è la seconda di una serie in quattro parti sulla progettazione, sulla creazione e sul deployment dei microservizi. Questa serie descrive i vari elementi di un'architettura a microservizi. La serie include informazioni su i vantaggi e gli svantaggi del pattern dell'architettura di microservizi e applicarla.

  1. Introduzione ai microservizi
  2. Esegui il refactoring di un monolite in microservizi (questo documento)
  3. Comunicazione tra servizi in una configurazione di microservizi
  4. Monitoraggio distribuito in un'applicazione di microservizi

Questa serie è destinata a sviluppatori di applicazioni e architetti che progettano e implementare la migrazione per il refactoring di un'applicazione monolitica in una un'applicazione.

Il processo di trasformazione di un'applicazione monolitica in microservizi è una forma di modernizzazione delle applicazioni. Per eseguire la modernizzazione delle applicazioni, ti consigliamo di non eseguire il refactoring di tutto il codice contemporaneamente. Ti consigliamo invece di eseguire il refactoring incrementale per la tua applicazione monolitica. Quando esegui il refactoring incrementale di un'applicazione, a creare gradualmente una nuova applicazione composta da microservizi ed eseguire e applicazione monolitica. Questo approccio è noto anche come il Schema dei fichi strangolati. Nel tempo, la quantità di funzionalità implementata dall'applicazione monolitica si riduce fino a scomparire del tutto o a diventare un altro microservice.

Per disaccoppiare le funzionalità da un monolite, devi estrarre con attenzione dati, logica e componenti rivolti all'utente della funzionalità, quindi reindirizzarli al nuovo servizio. È importante comprendere bene lo spazio del problema prima di passare allo spazio della soluzione.

Quando comprendi lo spazio dei problemi, comprendi i confini naturali nel dominio che forniscono il giusto livello di isolamento. Ti consigliamo di creare servizi più grandi anziché servizi più piccoli fino a quando comprendere il dominio.

La definizione dei confini dei servizi è un processo iterativo. Poiché si tratta di un processo di poco lavoro, devi valutare continuamente il costo rispetto ai vantaggi che ottieni. Di seguito sono riportati i fattori che ti aiuteranno a valutare l'approccio al disaccoppiamento di un monolite:

  • Evita di eseguire il refactoring di tutto contemporaneamente. Per dare la priorità al disaccoppiamento dei servizi, valuta i costi rispetto ai vantaggi.
  • I servizi in un'architettura di microservizi sono organizzati in base all'attività e non di natura tecnica.
  • Quando esegui la migrazione incrementale dei servizi, configura la comunicazione tra i servizi e il monolito in modo che passi attraverso contratti API ben definiti.
  • I microservizi richiedono molta più automazione: pensa in anticipo a integrazione continua (CI), deployment continuo (CD), logging e monitoraggio centralizzati.

Le sezioni seguenti illustrano varie strategie per disaccoppiare i servizi e eseguire la migrazione incrementale dell'applicazione monolitica.

Scollegamento tramite progettazione basata sui domini

I microservizi devono essere progettati in base alle funzionalità aziendali, non a livelli orizzontali come l'accesso ai dati o la messaggistica. I microservizi devono inoltre avere un accoppiamento sfuso e un'elevata coesione funzionale. I microservizi sono accoppiati in modo lasco se puoi modificare un servizio senza richiedere l'aggiornamento di altri servizi contemporaneamente. Un microservizio è coeso se ha uno scopo unico e ben definito, come la gestione degli account utente o l'elaborazione dei pagamenti.

Progettazione basata sul dominio (DDD) richiede una buona conoscenza del dominio per il quale viene scritto. La conoscenza settoriale necessaria per creare l'applicazione risiede all'interno persone che lo capiscono, gli esperti di dominio.

Puoi applicare l'approccio DDD in modo retroattivo a un'applicazione esistente che segue:

  1. Identifica un linguaggio onnipresente, ovvero un vocabolario comune condiviso da tutti gli stakeholder. In qualità di sviluppatore, è importante utilizzare nel codice termini che una persona senza competenze tecniche può capire. Ciò che il tuo codice cerca di ottenere deve rispecchiare i processi aziendali.
  2. Identifica i moduli pertinenti nell'applicazione monolitica, quindi applica il vocabolario comune a questi moduli.
  3. Definisci contesti delimitati in cui applichi confini espliciti ai moduli identificati con responsabilità ben chiare. I contesti delimitati che identifichi sono candidati per essere sottoposti a refactoring in microservizi più piccoli.

Il seguente diagramma mostra come applicare contesti limitati a un modello applicazione di ecommerce:

I contesti limitati vengono applicati a un'applicazione.

Figura 1. Le funzionalità dell'applicazione sono separate in contesti delimitati che vengono migrati ai servizi.

Nella figura 1, le funzionalità dell'applicazione di e-commerce sono separate in contesti delimitati e la migrazione ai servizi avviene nel seguente modo:

  • Le funzionalità di gestione e evasione degli ordini sono raggruppate nelle seguenti categorie:
    • La funzionalità di gestione degli ordini viene migrata al servizio degli ordini.
    • La funzionalità di gestione della consegna della logistica di consegna a domicilio.
    • La funzionalità di inventario viene migrato al servizio di inventario.
  • Le capacità di contabilità sono vincolate a un’unica categoria:
    • Le capacità di consumatori, venditori e terze parti sono vincolate ed eseguire la migrazione al servizio dell'account.

Dare la priorità ai servizi per la migrazione

Un punto di partenza ideale per disaccoppiare i servizi è identificare i a basso accoppiamento moduli nella tua applicazione monolitica. Puoi scegliere un modulo con accoppiamento lasco come uno dei primi candidati alla conversione in un microservizio. Per completare delle dipendenze di ciascun modulo, considera quanto segue:

  • Il tipo di dipendenza: dipendenze dai dati o da altri moduli.
  • La scala della dipendenza: in che modo una modifica nel modulo identificato potrebbero influire su altri moduli.

La migrazione di un modulo con dipendenze di dati elevate in genere non è un'attività banale. Se esegui prima la migrazione delle funzionalità e poi dei dati correlati in un secondo momento, è possibile che leggere e scrivere dati su più database. Pertanto, devi tenere conto delle sfide di integrità e sincronizzazione dei dati.

Ti consigliamo di estrarre i moduli con requisiti di risorse diversi rispetto al resto del monolite. Ad esempio, se un modulo ha un database in memoria, puoi convertirlo in un servizio, che può essere implementato su host con una memoria più elevata. Quando trasformi i moduli con requisiti di risorse specifici in servizi, puoi scalare molto più facilmente la tua applicazione.

Dal punto di vista operativo, il refactoring di un modulo in un proprio servizio comporta anche la modifica delle strutture di team esistenti. Il percorso migliore per una chiara assunzione di responsabilità è dare potere a piccoli team che possiedono un intero servizio.

Ulteriori fattori che possono influire sulla priorità dei servizi per la migrazione includono la criticità aziendale, una copertura di test completa, il livello di l'applicazione e il consenso dell'organizzazione. In base alle tue valutazioni, puoi classificare i servizi come descritto nel primo documento di questa serie in base al vantaggio che ricevi dal refactoring.

Estrai un servizio da un monolite

Dopo aver identificato il candidato di servizio ideale, devi identificare un modo per sia i microservizi che i moduli monolitici coesistono. Un modo per gestire questa situazione la coesistenza è l'introduzione di un adattatore di comunicazione tra processi (IPC), che possono aiutare i moduli a interagire. Nel tempo, il microservizio assume il carico ed elimina il componente monolitico. Questo processo incrementale riduce il rischio di passare dall'applicazione monolitica al nuovo microservizio perché puoi rilevare i bug o i problemi di prestazioni in modo graduale.

Il seguente diagramma mostra come implementare l'approccio IPC:

Viene implementato un approccio IPC per aiutare i moduli a interagire.

Figura 2. Un adattatore IPC coordina la comunicazione tra il modello monolitico e un modulo basato su microservizi.

Nella figura 2, il modulo Z è il servizio candidato da cui vuoi estrarre i dati dell'applicazione monolitica. I moduli X e Y dipendono dal modulo Z. I moduli microservizio X e Y utilizzano un adattatore IPC nell'applicazione monolitica per comunicare con il modulo Z tramite un'API REST.

Il documento successivo di questa serie, Comunicazione tra servizi in una configurazione di microservizi, descrive il pattern Strangler Fig e come decostruire un servizio dal monolito.

Gestisci un database monolitico

In genere, le applicazioni monolitiche hanno i propri database monolitici. Uno di i principi di un'architettura di microservizi è avere un database per microservizio. Quindi, quando modernizzi la tua applicazione monolitica di microservizi, devi suddividere il database monolitico in base al servizio confini che identifichi.

Per determinare dove suddividere un database monolitico, devi prima analizzare il database mapping. Nell'ambito dell'analisi di estrazione dei servizi, hai raccolto alcune informazioni sui microservizi che devi creare. Puoi utilizzare lo stesso approccio per analizzare l'utilizzo del database e mappare tabelle o altri oggetti del database ai nuovi microservizi. Strumenti come SchemaCrawler, SchemaSpy, e ERBuilder possono aiutarti a eseguire un'analisi di questo tipo. Le tabelle di mappatura e altri oggetti ti aiutano a comprendere l'accoppiamento tra gli oggetti del database che si estendono oltre i potenziali confini dei microservizi.

Tuttavia, la suddivisione di un database monolitico è complessa perché potrebbe non essere chiara separazione tra gli oggetti di database. Devi inoltre prendere in considerazione altri come la sincronizzazione dei dati, l'integrità transazionale, i join una latenza di pochi millisecondi. La sezione successiva descrive i pattern che possono aiutarti a rispondere a questi problemi quando dividi il database monolitico.

Tabelle di riferimento

Nelle applicazioni monolitiche, è normale che i moduli accedano ai dati richiesti da un altro modulo tramite una join SQL alla tabella dell'altro modulo. Il seguente diagramma utilizza l'esempio di applicazione di e-commerce precedente per mostrare questa procedura di accesso all'unione SQL:

Un modulo utilizza un join SQL per accedere ai dati di un altro modulo.

Figura 3. Un modulo unisce i dati alla tabella di un altro modulo.

Nella figura 3, per ottenere le informazioni sui prodotti, un modulo di ordine utilizza una product_id chiave esterna per unire un ordine alla tabella dei prodotti.

Tuttavia, se scomponi i moduli come singoli servizi, ti consigliamo Se il servizio ordini non ha, chiama direttamente il database del servizio del prodotto per eseguire un'operazione di join. Le sezioni seguenti descrivono le opzioni che puoi prendere in considerazione per separare gli oggetti del database.

Condividere i dati tramite un'API

Quando si separano le funzionalità o i moduli di base in microservizi, di solito usano le API per condividere ed esporre i dati. Il servizio a cui si fa riferimento espone i dati come API di cui ha bisogno il servizio chiamante, come mostrato nel seguente diagramma:

I dati sono esposti attraverso un'API.

Figura 4. Un servizio utilizza una chiamata API per recuperare i dati da un altro servizio.

Nella figura 4, un modulo ordini utilizza una chiamata API per recuperare i dati da un modulo prodotto. Questa implementazione presenta evidenti problemi di prestazioni a causa di chiamate aggiuntive alla rete e al database. Tuttavia, la condivisione dei dati tramite un'API funziona bene quando la dimensione dei dati è limitata. Inoltre, se il servizio chiamato restituisce dati che hanno un noto, puoi implementare una cache TTL locale sul chiamante ridurre le richieste di rete al servizio chiamato.

Replica dei dati

Un altro modo per condividere i dati tra due microservizi separati è replicare i dati nel database del servizio dipendente. La replica dei dati è di sola lettura e può essere ricostruita in qualsiasi momento. Questo pattern consente al servizio di essere più coeso. Il seguente diagramma mostra come funziona la replica dei dati tra due microservizi:

I dati vengono replicati tra microservizi.

Figura 5. I dati di un servizio vengono replicati in un database di servizi dipendente.

Nella figura 5, il database del servizio del prodotto viene replicato nel servizio ordini per configurare un database. Questa implementazione consente al servizio ordini di ricevere i dati di prodotto senza chiamate ripetute al servizio del prodotto.

Per creare la replica dei dati, puoi utilizzare tecniche come le viste materializzate, la tecnologia Change Data Capture (CDC) e le notifiche di eventi. I dati replicati sono infine coerenti, ma può esserci un ritardo nella replica dei dati, pertanto esiste il risico di pubblicare dati non aggiornati.

Dati statici come configurazione

I dati statici, come i codici paese e le valute supportate, sono lenti a modifica. Puoi iniettare questi dati statici come configurazione in un microservizio. I microservizi e i framework cloud moderni forniscono funzionalità per gestire di configurazione mediante server di configurazione, archivi di coppie chiave-valore e vault. Puoi includere queste funzionalità in modo dichiarativo.

Dati mutabili condivisi

Le applicazioni monolitiche hanno un pattern comune noto come stato mutabile condiviso. In una configurazione dello stato mutabile condiviso, più moduli utilizzano una singola tabella, come mostrato nel seguente diagramma:

Una configurazione condivisa dello stato modificabile rende disponibile una singola tabella per più moduli.

Figura 6. Più moduli utilizzano una singola tabella.

Nella figura 6, le funzionalità di ordine, pagamento e spedizione dell'applicazione di e-commerce utilizzano la stessa tabella ShoppingStatus per mantenere aggiornato lo stato dell'ordine del cliente durante il percorso di acquisto.

Per eseguire la migrazione di un monolite dello stato mutabile condiviso, puoi sviluppare un microservizio ShoppingStatus separato per gestire la tabella di database ShoppingStatus. Questo espone le API per gestire lo stato di acquisto di un cliente, come mostrato in il seguente diagramma:

Le API sono esposte ad altri servizi.

Figura 7. Un microservizio espone le API a più altri servizi.

Nella figura 7, i microservizi relativi a pagamento, ordine e spedizione utilizzano API dei microservizi ShoppingStatus. Se la tabella di database è strettamente correlata servizi, ti consigliamo di spostarli in quel servizio. Tu può quindi esporre i dati tramite un'API per consentire ad altri servizi di utilizzarli. Questa implementazione ti consente di assicurarti di non avere troppi servizi granulari che si richiamano spesso. Se suddividi i servizi in modo errato, ridefinire i confini dei servizi.

Transazioni distribuite

Dopo aver isolato il servizio dal monolite, una transazione locale nel sistema monolitico originale potrebbe essere distribuita tra più servizi. R di una transazione che abbraccia più servizi è considerata un transazione. Nell'applicazione monolitica, il sistema di database garantisce che le transazioni sono atomiche. Per gestire le transazioni tra vari servizi in un sistema basato su microservizi, devi creare un coordinatore delle transazioni globale. Il coordinatore delle transazioni gestisce il rollback, le azioni di compensazione e transazioni descritte nel prossimo documento di questa serie, Comunicazione tra servizi in una configurazione di microservizi.

Coerenza dei dati

Le transazioni distribuite introducono la sfida di mantenere i dati la coerenza tra i servizi. Tutti gli aggiornamenti devono essere eseguiti in modo atomico. In un applicazione monolitica, le proprietà delle transazioni garantiscono che una query restituisce una vista coerente del database in base al suo livello di isolamento.

Al contrario, prendiamo in considerazione una transazione in più passaggi in un'architettura basata su microservizi. Se una transazione di servizio non va a buon fine, i dati devono essere riconciliati entro il di eseguire il rollback dei passaggi che hanno avuto esito positivo negli altri servizi. In caso contrario, la visualizzazione globale dei dati dell'applicazione non è coerente tra i servizi.

Può essere difficile determinare quando una fase che implementa la coerenza non è andata a buon fine. Ad esempio, un passaggio potrebbe non avere esito negativo immediatamente, ma bloccarsi o scadere. Pertanto, potrebbe essere necessario implementare un qualche tipo di meccanismo di timeout. Se i dati duplicati non sono aggiornati quando il servizio chiamato vi accede, anche la memorizzazione nella cache o la replica dei dati tra i servizi per ridurre la latenza della rete può comportare dati incoerenti.

Il documento successivo della serie, Comunicazione tra servizi in una configurazione di microservizi, fornisce un esempio di pattern per gestire le transazioni distribuite tra microservizi.

Progettare la comunicazione tra servizi

In un'applicazione monolitica, i componenti (o i moduli dell'applicazione) si richiamano tra loro direttamente tramite chiamate di funzione. Al contrario, un'applicazione basata su microservizi è composta da più servizi che interagiscono tra loro sulla rete.

Quando progetti la comunicazione tra servizi, pensa innanzitutto a come si prevede che i servizi interagiscano tra loro. Le interazioni con i servizi possono essere seguenti:

  • Interazione uno a uno: ogni richiesta del client viene elaborata da esattamente un servizio.
  • Interazioni uno a molti: ogni richiesta viene elaborata da più servizi.

Considera inoltre se l'interazione è sincrona o asincrona:

  • Sincrona: il client si aspetta una risposta tempestiva dal servizio e potrebbe bloccarsi in attesa.
  • Asincrono: il client non blocca in attesa di una risposta. La risposta, se presente, non viene necessariamente inviata immediatamente.

La seguente tabella mostra le combinazioni di stili di interazione:

one-to-one One-to-many
Sincrona Richiesta e risposta: invia una richiesta a un servizio e attendi una risposta.
Asincrona Notifica: viene inviata una richiesta a un servizio, ma non è prevista o inviata alcuna risposta. Pubblica e iscriviti: il client pubblica un messaggio di notifica e zero o più servizi interessati lo consumano.
Richiesta e risposta asincrona: invia una richiesta a un servizio, che risponde in modo asincrono. Il client non blocca. Pubblicazione e risposte asincrone: il client pubblica una richiesta e aspetta le risposte dei servizi interessati.

In genere, ogni servizio utilizza una combinazione di questi stili di interazione.

Implementare la comunicazione tra i servizi

Per implementare la comunicazione tra servizi, puoi scegliere tra diverse tecnologie IPC. Ad esempio, i servizi possono usare modelli meccanismi di comunicazione come REST basato su HTTP, gRPC o Thrift. In alternativa, i servizi possono utilizzare meccanismi di comunicazione asincroni basati su messaggi come AMQP o STOMP. Puoi anche scegliere tra vari formati dei messaggi. Ad esempio, i servizi possono utilizzare modelli come JSON o XML. In alternativa, i servizi possono usare un formato binario come Avro o Protocol Buffers.

La configurazione dei servizi per chiamare direttamente altri servizi comporta un alto accoppiamento tra i servizi. Ti consigliamo invece di usare i messaggi o le comunicazione:

  • Messaggistica: quando implementi la messaggistica, non è più necessario che i servizi si chiamino direttamente tra loro. Tutti i servizi conoscono invece il broker di messaggi ed effettua il push dei messaggi a quel broker. Il broker di messaggi salva questi messaggi in una coda di messaggi. Altri servizi possono iscriversi ai messaggi che li interessano.
  • Comunicazione basata sugli eventi: quando implementi l'elaborazione basata su eventi, la comunicazione tra i servizi avviene attraverso eventi che i singoli dei servizi di machine learning. I singoli servizi scrivono i propri eventi in un messaggio l'intermediario. I servizi possono ascoltare gli eventi di interesse. Questo pattern mantiene i servizi in modo disaccoppiato perché gli eventi non includono payload.

In un'applicazione basata su microservizi, consigliamo di utilizzare la comunicazione asincrona tra servizi anziché la comunicazione sincrona. Il campo "Richiesta-risposta è un un pattern architetturale ben compreso, per cui la progettazione di un'API sincrona potrebbe più naturale rispetto alla progettazione di un sistema asincrono. Comunicazione asincrona tra i servizi possono essere implementati tramite messaggi o basati su eventi la comunicazione. L'uso della comunicazione asincrona offre quanto segue: vantaggi:

  • Basso accoppiamento: un modello asincrono suddivide la richiesta-risposta. l'interazione in due messaggi separati, uno per la richiesta e l'altro per la risposta. Il consumatore di un servizio avvia il messaggio di richiesta e attende la risposta, mentre il fornitore di servizi attende i messaggi di richiesta a cui risponde con messaggi di risposta. Questa configurazione significa che il chiamante non deve attendere il messaggio di risposta.
  • Isolamento degli errori: il mittente può continuare a inviare messaggi anche se il consumatore a valle non riesce. Il consumatore torna a occuparsi delle attività arretrate ogni volta che viene recuperata. Questa funzionalità è particolarmente utile in un'architettura di microservizi, perché ogni servizio ha il proprio ciclo di vita. Tuttavia, le API sincrone richiedono che il servizio a valle sia disponibile o l'operazione non andrà a buon fine.
  • Reattività: un servizio upstream può rispondere più velocemente in caso contrario. e attenderemo i servizi downstream. Se esiste una catena di dipendenze dei servizi (il servizio A chiama B, che chiama C e così via), l'attesa delle chiamate sincrone può aggiungere quantità inaccettabili di latenza.
  • Controllo del flusso: una coda di messaggi agisce da buffer, in modo che i destinatari possono elaborare i messaggi secondo le loro tempistiche.

Tuttavia, di seguito sono riportate alcune difficoltà dell'utilizzo della messaggistica asincrona in modo efficace:

  • Latenza: se il broker di messaggi diventa un collo di bottiglia, end-to-end potrebbe diventare elevata.
  • Overhead per lo sviluppo e i test: a seconda della scelta dell'infrastruttura di messaggistica o di eventi, è possibile che si verifichino messaggi duplicati, il che rende difficile rendere le operazioni idempotenti. Inoltre, può essere difficile implementare e testare la semantica di richiesta/risposta utilizzando la messaggistica asincrona. Devi trovare un modo per correlare la richiesta e la risposta messaggi.
  • Throughput: la gestione dei messaggi asincroni, che utilizza una coda centralizzata o un altro meccanismo, può diventare un collo di bottiglia nel sistema. La sistemi di backend, come code e consumer downstream, devono scalare in base ai requisiti di velocità effettiva del sistema.
  • Complica la gestione degli errori: in un sistema asincrono, chi chiama non sa se una richiesta è andata a buon fine o meno, pertanto la gestione degli errori deve essere gestita out of band. Questo tipo di impianto può rendere difficile implementare una logica come nuovi tentativi o backoff esponenziali. La gestione degli errori è complicata ulteriormente, se ci sono più chiamate asincrone concatenate tutti hanno successo o fallimento.

Il documento successivo della serie, Comunicazione tra servizi in una configurazione di microservizi, fornisce un'implementazione di riferimento per affrontare alcune delle sfide menzionate dell'elenco precedente.

Passaggi successivi