Condividi tramite


Progettare un'applicazione orientata ai microservizi

Suggerimento

Questo contenuto è un estratto dell'eBook, Architettura di microservizi .NET per applicazioni .NET containerizzati, disponibile in documentazione .NET o come PDF scaricabile gratuitamente leggibile offline.

Architettura di Microservizi .NET per Applicazioni .NET Containerizzate miniatura della copertina dell'eBook.

Questa sezione è incentrata sullo sviluppo di un'applicazione aziendale ipotetica lato server.

Specifiche dell'applicazione

L'applicazione ipotetica gestisce le richieste eseguendo logica di business, accedendo ai database e restituendo risposte HTML, JSON o XML. Si supponga che l'applicazione debba supportare vari client, inclusi i browser desktop che eseguono applicazioni a pagina singola (SPA), le app Web tradizionali, le app Web per dispositivi mobili e le app native per dispositivi mobili. L'applicazione potrebbe anche esporre un'API da utilizzare da terze parti. Dovrebbe anche essere in grado di integrare i microservizi o le applicazioni esterne in modo asincrono, in modo che l'approccio contribuirà alla resilienza dei microservizi in caso di errori parziali.

L'applicazione sarà costituita da questi tipi di componenti:

  • Componenti di presentazione. Questi componenti sono responsabili della gestione dell'interfaccia utente e dell'utilizzo di servizi remoti.

  • Dominio o logica aziendale. Questo componente è la logica di dominio dell'applicazione.

  • Logica di accesso al database. Questo componente è costituito da componenti di accesso ai dati responsabili dell'accesso ai database (SQL o NoSQL).

  • Logica di integrazione dell'applicazione. Questo componente include un canale di messaggistica basato su broker di messaggi.

L'applicazione richiede una scalabilità elevata, consentendo al contempo il ridimensionamento autonomo dei sottosistemi verticali, perché alcuni sottosistemi richiedono una maggiore scalabilità rispetto ad altri.

L'applicazione deve essere in grado di essere distribuita in più ambienti di infrastruttura (più cloud pubblici e locali) e idealmente deve essere multipiattaforma, in grado di passare da Linux a Windows (o viceversa) facilmente.

Contesto del team di sviluppo

Si presuppone anche quanto segue sul processo di sviluppo per l'applicazione:

  • Sono disponibili più team di sviluppo incentrati su diverse aree aziendali dell'applicazione.

  • I nuovi membri del team devono diventare produttivi rapidamente e l'applicazione deve essere facile da comprendere e modificare.

  • L'applicazione avrà un'evoluzione a lungo termine e regole business in continua evoluzione.

  • È necessaria una buona manutenibilità a lungo termine, il che significa avere agilità quando si implementano nuove modifiche in futuro, pur essendo in grado di aggiornare più sottosistemi con impatto minimo sugli altri sottosistemi.

  • Si vuole praticare l'integrazione continua e la distribuzione continua dell'applicazione.

  • Si vogliono sfruttare le tecnologie emergenti (framework, linguaggi di programmazione e così via) durante l'evoluzione dell'applicazione. Non si vogliono eseguire migrazioni complete dell'applicazione quando si passa a nuove tecnologie, perché ciò comporta costi elevati e influisce sulla prevedibilità e sulla stabilità dell'applicazione.

Scelta di un'architettura

Che cosa deve essere l'architettura di distribuzione dell'applicazione? Le specifiche per l'applicazione, insieme al contesto di sviluppo, suggeriscono vivamente di progettare l'applicazione scomponendola in sottosistemi autonomi sotto forma di microservizi e contenitori che collaborano, in cui un microservizio è un contenitore.

In questo approccio, ogni servizio (contenitore) implementa un set di funzioni coese e strettamente correlate. Ad esempio, un'applicazione può essere costituita da servizi come il servizio catalogo, il servizio di ordinamento, il servizio carrello, il servizio profili utente e così via.

I microservizi comunicano usando protocolli come HTTP (REST), ma anche in modo asincrono (ad esempio, usando AMQP) quando possibile, soprattutto quando si propagano aggiornamenti con eventi di integrazione.

I microservizi vengono sviluppati e distribuiti come contenitori indipendentemente l'uno dall'altro. Questo approccio significa che un team di sviluppo può sviluppare e distribuire un determinato microservizio senza influire su altri sottosistemi.

Ogni microservizio ha un proprio database, consentendone la separazione completa da altri microservizi. Quando necessario, la coerenza tra database di microservizi diversi viene ottenuta usando eventi di integrazione a livello di applicazione (tramite un bus di eventi logici), come gestito in Command and Query Responsibility Segregation (CQRS). Per questo motivo, i vincoli aziendali devono abbracciare la consistenza eventuale tra i diversi microservizi e i database correlati.

eShopOnContainers: applicazione di riferimento per .NET e microservizi distribuiti tramite contenitori

In questo modo è possibile concentrarsi sull'architettura e sulle tecnologie invece di pensare a un dominio aziendale ipotetico che potrebbe non essere noto, abbiamo selezionato un dominio aziendale noto, ovvero un'applicazione di e-commerce semplificata (e-shop) che presenta un catalogo di prodotti, accetta ordini dai clienti, verifica l'inventario ed esegue altre funzioni aziendali. Questo codice sorgente dell'applicazione basato su contenitori è disponibile nel repository GitHub eShopOnContainers .

L'applicazione è costituita da più sottosistemi, tra cui diversi front-end dell'interfaccia utente dell'archivio (un'applicazione Web e un'app per dispositivi mobili nativa), insieme ai microservizi back-end e ai contenitori per tutte le operazioni sul lato server necessarie con diversi gateway API come punti di ingresso consolidati ai microservizi interni. La figura 6-1 mostra l'architettura dell'applicazione di riferimento.

Diagramma delle app client che usano eShopOnContainers in un singolo host Docker.

Figura 6-1. Architettura dell'applicazione di riferimento eShopOnContainers per l'ambiente di sviluppo

Il diagramma precedente mostra che i client mobile e SPA comunicano con singoli endpoint del gateway API, che quindi comunicano con i microservizi. I client Web tradizionali comunicano con il microservizio MVC, che comunica con i microservizi tramite il gateway API.

Ambiente di hosting. Nella figura 6-1 vengono visualizzati diversi contenitori distribuiti in un singolo host Docker. Ciò potrebbe verificarsi quando si esegue la distribuzione in un singolo host Docker con il comando docker-compose up. Tuttavia, se si usa un agente di orchestrazione o un cluster di contenitori, ogni contenitore potrebbe essere in esecuzione in un host (nodo) diverso e qualsiasi nodo potrebbe eseguire un numero qualsiasi di contenitori, come illustrato in precedenza nella sezione architettura.

Architettura di comunicazione. L'applicazione eShopOnContainers usa due tipi di comunicazione, a seconda del tipo di azione funzionale (query rispetto agli aggiornamenti e alle transazioni):

  • Comunicazione HTTP dal client al microservizio tramite gateway API. Questo approccio viene usato per le query e quando si accettano comandi di aggiornamento o transazionali dalle app client. L'approccio con i gateway API è illustrato in dettaglio nelle sezioni successive.

  • Comunicazione asincrona basata su eventi. Questa comunicazione avviene tramite un bus di eventi per propagare gli aggiornamenti tra microservizi o per l'integrazione con applicazioni esterne. Il bus di eventi può essere implementato con qualsiasi tecnologia di infrastruttura broker di messaggistica come RabbitMQ o usando bus di servizio di livello superiore (a livello di astrazione) come il bus di servizio di Azure, NServiceBus, MassTransit o Brighter.

L'applicazione viene distribuita come set di microservizi sotto forma di contenitori. Le app client possono comunicare con questi microservizi in esecuzione come contenitori tramite gli URL pubblici pubblicati dai gateway API.

Sovranità dei dati per microservizio

Nell'applicazione di esempio ogni microservizio è proprietario del proprio database o origine dati, anche se tutti i database di SQL Server vengono distribuiti come singolo contenitore. Questa decisione di progettazione è stata presa solo per semplificare per uno sviluppatore di ottenere il codice da GitHub, clonarlo e aprirlo in Visual Studio o Visual Studio Code. In alternativa, semplifica la compilazione delle immagini Docker personalizzate usando l'interfaccia della riga di comando di .NET e l'interfaccia della riga di comando docker, quindi distribuirle ed eseguirle in un ambiente di sviluppo Docker. In entrambi i casi, l'uso dei contenitori per le origini dati consente agli sviluppatori di compilare e distribuire in pochi minuti senza dover effettuare il provisioning di un database esterno o di qualsiasi altra origine dati con dipendenze rigide dall'infrastruttura (cloud o locale).

In un ambiente di produzione reale, per la disponibilità elevata e per la scalabilità, i database devono essere basati su server di database nel cloud o in locale, ma non in contenitori.

Di conseguenza, le unità di distribuzione per i microservizi (e anche per i database in questa applicazione) sono contenitori Docker e l'applicazione di riferimento è un'applicazione multi-contenitore che abbraccia i principi dei microservizi.

Risorse aggiuntive

Vantaggi di una soluzione basata su microservizi

Una soluzione basata su microservizi come questa offre molti vantaggi:

Ogni microservizio è relativamente piccolo, facile da gestire ed evolvere. In particolare:

  • È facile per uno sviluppatore comprendere e iniziare rapidamente con una buona produttività.

  • I contenitori partono rapidamente, rendendo gli sviluppatori più produttivi.

  • Un IDE come Visual Studio può caricare rapidamente progetti più piccoli, rendendo gli sviluppatori produttivi.

  • Ogni microservizio può essere progettato, sviluppato e distribuito indipendentemente da altri microservizi, che offrono agilità perché è più facile distribuire le nuove versioni dei microservizi di frequente.

È possibile ampliare le singole aree dell'applicazione. Ad esempio, potrebbe essere necessario scalare il servizio catalogo o il servizio carrello, ma non il processo di ordinamento. Un'infrastruttura di microservizi sarà molto più efficiente per quanto riguarda le risorse usate quando si aumenta il numero di istanze rispetto a un'architettura monolitica.

È possibile dividere il lavoro di sviluppo tra più team. Ogni servizio può essere di proprietà di un singolo team di sviluppo. Ogni team può gestire, sviluppare, distribuire e ridimensionare il servizio indipendentemente dal resto dei team.

I problemi sono più isolati. Se si verifica un problema in un servizio, solo il servizio è inizialmente interessato (tranne quando viene usata la progettazione errata, con dipendenze dirette tra microservizi) e altri servizi possono continuare a gestire le richieste. Al contrario, un componente non funzionante in un'architettura di distribuzione monolitica può ridurre l'intero sistema, soprattutto quando comporta risorse, ad esempio una perdita di memoria. Inoltre, quando viene risolto un problema in un microservizio, è possibile distribuire solo il microservizio interessato senza influire sul resto dell'applicazione.

È possibile usare le tecnologie più recenti. Poiché è possibile iniziare a sviluppare servizi in modo indipendente ed eseguirli side-by-side (grazie ai contenitori e .NET), è possibile iniziare a usare le tecnologie e i framework più recenti invece di rimanere bloccati in uno stack o un framework meno recenti per l'intera applicazione.

Svantaggi di una soluzione basata su microservizi

Una soluzione basata su microservizi come questa presenta anche alcuni svantaggi:

Applicazione distribuita. La distribuzione dell'applicazione aggiunge complessità per gli sviluppatori durante la progettazione e la compilazione dei servizi. Ad esempio, gli sviluppatori devono implementare la comunicazione tra servizi usando protocolli come HTTP o AMQP, che aggiunge complessità per il test e la gestione delle eccezioni. Aggiunge anche la latenza al sistema.

Complessità della distribuzione. Un'applicazione con decine di tipi di microservizi e richiede una scalabilità elevata (deve essere in grado di creare molte istanze per servizio e bilanciare tali servizi in molti host) significa un elevato grado di complessità di distribuzione per le operazioni e la gestione IT. Se non si usa un'infrastruttura orientata ai microservizi (ad esempio un agente di orchestrazione e un'utilità di pianificazione), questa complessità aggiuntiva può richiedere molto più attività di sviluppo rispetto all'applicazione aziendale stessa.

Transazioni atomiche. Le transazioni atomiche tra più microservizi in genere non sono possibili. I requisiti aziendali devono adottare la coerenza eventuale tra più microservizi. Per altre informazioni, vedere le sfide dell'elaborazione dei messaggi idempotenti.

Aumento delle esigenze di risorse globali (memoria totale, unità e risorse di rete per tutti i server o gli host). In molti casi, quando si sostituisce un'applicazione monolitica con un approccio ai microservizi, la quantità di risorse globali iniziali necessarie per la nuova applicazione basata su microservizi sarà maggiore rispetto alle esigenze dell'infrastruttura dell'applicazione monolitica originale. Questo approccio è dovuto al fatto che il livello di granularità e i servizi distribuiti più elevati richiedono più risorse globali. Tuttavia, dato il basso costo delle risorse in generale e il vantaggio di poter scalare orizzontalmente alcune aree dell'applicazione rispetto ai costi a lungo termine nell'evolvere applicazioni monolitiche, l'aumento dell'uso delle risorse è generalmente un buon compromesso per applicazioni di grandi dimensioni e durature.

Problemi relativi alla comunicazione diretta da client a microservizio. Quando l'applicazione è di grandi dimensioni, con decine di microservizi, esistono problemi e limitazioni se l'applicazione richiede comunicazioni dirette da client a microservizi. Un problema è una potenziale mancata corrispondenza tra le esigenze del client e le API esposte da ognuno dei microservizi. In alcuni casi, l'applicazione client potrebbe dover effettuare molte richieste separate per comporre l'interfaccia utente, che può essere inefficiente su Internet e sarebbe poco pratico su una rete mobile. Pertanto, le richieste dall'applicazione client al sistema back-end dovrebbero essere ridotte al minimo.

Un altro problema con le comunicazioni dirette da client a microservizi consiste nel fatto che alcuni microservizi potrebbero usare protocolli non compatibili con il Web. Un servizio potrebbe usare un protocollo binario, mentre un altro servizio potrebbe usare la messaggistica AMQP. Questi protocolli non sono compatibili con il firewall e vengono usati internamente. In genere, un'applicazione deve usare protocolli come HTTP e WebSocket per la comunicazione all'esterno del firewall.

Un altro svantaggio di questo approccio diretto da client a servizio è che rende difficile effettuare il refactoring dei contratti per tali microservizi. Nel corso del tempo gli sviluppatori potrebbero voler modificare il modo in cui il sistema viene partizionato in servizi. Ad esempio, possono unire due servizi o suddividere un servizio in due o più servizi. Tuttavia, se i client comunicano direttamente con i servizi, l'esecuzione di questo tipo di refactoring può interrompere la compatibilità con le app client.

Come accennato nella sezione architettura, durante la progettazione e la creazione di un'applicazione complessa basata su microservizi, è possibile considerare l'uso di più gateway API con granularità fine anziché l'approccio di comunicazione diretto da client a microservizio più semplice.

Partizionamento dei microservizi. Infine, indipendentemente dall'approccio adottato per l'architettura di microservizi, un'altra sfida consiste nel decidere come partizionare un'applicazione end-to-end in più microservizi. Come indicato nella sezione architettura della guida, esistono diverse tecniche e approcci che è possibile adottare. In sostanza, è necessario identificare le aree dell'applicazione che sono disaccoppiate dalle altre aree e che hanno un numero ridotto di dipendenze rigide. In molti casi, questo approccio è allineato ai servizi di partizionamento in base al caso d'uso. Nell'applicazione di e-shop, ad esempio, è disponibile un servizio di ordinamento responsabile di tutta la logica di business correlata al processo di ordine. Abbiamo anche il servizio catalogo e il servizio carrello che implementano altre funzionalità. Idealmente, ogni servizio deve avere solo un piccolo set di responsabilità. Questo approccio è simile al principio di responsabilità singola (SRP) applicato alle classi, che indica che una classe deve avere un solo motivo per cambiare. In questo caso, tuttavia, si tratta di microservizi, quindi l'ambito sarà maggiore di una singola classe. Soprattutto, un microservizio deve essere autonomo e gestire tutto il processo, inclusa la responsabilità per le proprie fonti di dati.

Architettura esterna e interna e modelli di progettazione

L'architettura esterna è l'architettura di microservizi costituita da più servizi, seguendo i principi descritti nella sezione relativa all'architettura di questa guida. Tuttavia, a seconda della natura di ogni microservizio e indipendentemente dall'architettura di microservizi di alto livello scelta, è comune e talvolta consigliabile avere architetture interne diverse, ognuna basata su modelli diversi, per microservizi diversi. I microservizi possono anche usare tecnologie e linguaggi di programmazione diversi. La figura 6-2 illustra questa diversità.

Diagramma che confronta modelli di architettura esterni e interni.

Figura 6-2. Architettura e progettazione interne e esterne

Ad esempio, nell'esempio eShopOnContainers i microservizi catalogo, carrello e profilo utente sono semplici (fondamentalmente, sottosistemi CRUD). Pertanto, la loro architettura e progettazione interna è semplice. Tuttavia, potrebbero essere presenti altri microservizi, ad esempio il microservizio di ordinamento, che è più complesso e rappresenta regole business in continua evoluzione con un elevato grado di complessità del dominio. In casi simili a questi, è possibile implementare modelli più avanzati all'interno di un particolare microservizio, come quelli definiti con approcci DDD (Domain-Driven Design), come nel microservizio di ordinamento di eShopOnContainers . Questi modelli DDD verranno esaminati più avanti nella sezione che illustra l'implementazione del microservizio di ordinamento di eShopOnContainers .

Un altro motivo per una tecnologia diversa per ogni microservizio potrebbe essere la natura di ogni microservizio. Ad esempio, potrebbe essere preferibile usare un linguaggio di programmazione funzionale come F#, o anche un linguaggio come R se si ha come destinazione domini di intelligenza artificiale e Machine Learning, anziché un linguaggio di programmazione più orientato agli oggetti come C#.

La linea inferiore consiste nel fatto che ogni microservizio può avere un'architettura interna diversa in base a modelli di progettazione diversi. Non tutti i microservizi devono essere implementati usando modelli DDD avanzati, perché sarebbero sovraprogetti. Analogamente, i microservizi complessi con logica di business in continua evoluzione non devono essere implementati come componenti CRUD oppure è possibile ottenere codice di bassa qualità.

Il nuovo mondo: più modelli architetturali e microservizi poliglotta

Esistono molti modelli architetturali usati dagli architetti software e dagli sviluppatori. Di seguito sono riportati alcuni (combinazione di stili di architettura e modelli di architettura):

È anche possibile creare microservizi con molte tecnologie e linguaggi, ad esempio ASP.NET API Web Core, NancyFx, ASP.NET Core SignalR (disponibile con .NET Core 2 o versione successiva), F#, Node.js, Python, Java, C++, GoLang e altro ancora.

Il punto importante è che nessun modello o stile di architettura particolare, né alcuna particolare tecnologia, è giusto per tutte le situazioni. La figura 6-3 illustra alcuni approcci e tecnologie (anche se non in un ordine particolare) che possono essere usati in microservizi diversi.

Diagramma che mostra 12 microservizi complessi in un'architettura globale poliglotta.

Figura 6-3. Modelli per architetture multiple e il mondo dei microservizi poliglotta

Il modello multi-architetturale e i microservizi poliglotta consentono di combinare e associare linguaggi e tecnologie alle esigenze di ogni microservizio e di comunicare tra loro. Come illustrato nella figura 6-3, nelle applicazioni composte da molti microservizi (contesti delimitati nella terminologia di progettazione basata su dominio o semplicemente "sottosistemi" come microservizi autonomi), è possibile implementare ogni microservizio in modo diverso. Ognuno può avere un modello di architettura diverso e usare linguaggi e database diversi a seconda della natura dell'applicazione, dei requisiti aziendali e delle priorità. In alcuni casi, i microservizi potrebbero essere simili. In genere, tuttavia, questo non è il caso, perché i requisiti e i limiti di contesto di ogni sottosistema sono in genere diversi.

Ad esempio, per una semplice applicazione di manutenzione CRUD, potrebbe non essere opportuno progettare e implementare modelli DDD. Tuttavia, per il dominio principale o il core business, potrebbe essere necessario applicare modelli più avanzati per affrontare la complessità aziendale con regole business in continua evoluzione.

In particolare quando si gestiscono applicazioni di grandi dimensioni composte da più sottosistemi, non è consigliabile applicare un'unica architettura di primo livello in base a un unico modello di architettura. Ad esempio, CQRS non deve essere applicato come architettura di primo livello per un'intera applicazione, ma può essere utile per un set specifico di servizi.

Non esiste un proiettile d'argento o un modello di architettura corretto per ogni caso specifico. Non è possibile avere "un modello di architettura per regolarli tutti". A seconda delle priorità di ogni microservizio, è necessario scegliere un approccio diverso per ognuno, come illustrato nelle sezioni seguenti.