Transizione da monolite a microservizi: storia e pratica

In questo articolo parlerò di come il progetto su cui sto lavorando si sia trasformato da un grande monolite in una serie di microservizi.

Il progetto ha avuto inizio molto tempo fa, all'inizio degli anni 2000. Le prime versioni erano scritte in Visual Basic 6. Nel corso del tempo, divenne chiaro che sarebbe stato difficile sostenere lo sviluppo in questo linguaggio in futuro, poiché l'IDE e il linguaggio stesso si sviluppavano poco. Alla fine degli anni 2000, si decise di passare a C#, un linguaggio più promettente. La nuova versione è stata scritta parallelamente alla modifica della vecchia, con sempre più codice in .NET. Il backend in C# inizialmente mirava a un'architettura basata su servizi, ma nello sviluppo venivano utilizzate librerie comuni con logica, e i servizi venivano avviati in un unico processo. Risultò un'applicazione che chiamavamo 'monolite dei servizi'.

Uno dei pochi vantaggi di questa combinazione era la possibilità di far sì che i servizi si chiamassero a vicenda tramite un'API esterna. C'erano chiare premesse per passare a un'architettura basata su servizi più corretta e, in prospettiva, a una microservizio.

Abbiamo iniziato il nostro lavoro di decomposizione intorno al 2015. Non abbiamo ancora raggiunto uno stato ideale: ci sono parti di un grande progetto che è difficile chiamare monoliti, ma non somigliano nemmeno a microservizi. Tuttavia, i progressi sono significativi.
Di questo parlerò nell'articolo.

Transizione da monolite a microservizi: storia e pratica

Contenuto

Architettura e problemi della soluzione attuale


Inizialmente, l'architettura era la seguente: l'UI era un'applicazione separata, la parte monolitica scritta in Visual Basic 6, e l'applicazione su .NET era un insieme di servizi correlati che lavoravano con un database piuttosto grande.

Svantaggi della soluzione precedente

Punto unico di fault
Avevamo un unico punto di fallimento: l'applicazione .NET veniva eseguita all'interno di un solo processo. Se uno dei moduli andava in crash, l'intera applicazione smetteva di funzionare e doveva essere riavviata. Poiché automatizziamo un gran numero di processi per diversi utenti, a causa di un guasto in uno di essi, tutti non potevano lavorare per un certo periodo. E anche con un errore software, non aiutava la ridondanza.

Coda di modifiche
Questo difetto è più organizzativo. Nella nostra applicazione ci sono molti clienti, e tutti vogliono che le loro richieste vengano soddisfatte il prima possibile. In passato, non era possibile farlo in parallelo, e tutti i clienti si mettevano in fila. Questo processo generava frustrazione nel business, poiché dovevano dimostrare che la loro richiesta avesse valore. E il team di sviluppo spendeva tempo per organizzare questa coda. Ciò richiedeva molte risorse e il prodotto, alla fine, non poteva evolversi così rapidamente come avrebbero voluto.

Utilizzo non ottimale delle risorse
Nell'implementazione dei servizi in un unico processo, abbiamo sempre copiato completamente la configurazione da server a server. Volevamo separare i servizi più carichi in modo da non sprecare risorse e ottenere una gestione più flessibile del nostro schema di distribuzione.

È difficile adottare tecnologie moderne
Un problema noto a tutti gli sviluppatori: c'è la volontà di integrare tecnologie moderne nel progetto, ma mancano le opportunità. In una grande soluzione monolitica, ogni aggiornamento della libreria esistente, per non parlare del passaggio a una nuova, diventa un compito piuttosto impegnativo. È necessario convincere il team leader che questo porterà più vantaggi che nervosismo speso.

Difficoltà nell'applicazione delle modifiche
Questa era la problematica più seria: riuscivamo a fare un rilascio ogni due mesi.
Ogni rilascio si trasformava in una vera catastrofe per la banca, nonostante i test e gli sforzi degli sviluppatori. Il business sapeva che all'inizio della settimana non avrebbe avuto parte delle funzionalità operative. E gli sviluppatori capivano che li aspettava una settimana di seri incidenti.
Tutti desideravano cambiare la situazione.

Aspettative dai microservizi


Uscita dei componenti al momento della disponibilità. Uscita dei componenti man mano che diventano disponibili, grazie alla decomposizione delle soluzioni e alla separazione dei vari processi.

Piccole squadre di prodotto. Questo è importante perché gestire una grande squadra che lavora su un vecchio monolite è complicato. Tale squadra è costretta a seguire processi rigorosi, mentre c'è il desiderio di maggiore creatività e autonomia. Solo le piccole squadre possono permetterselo.

Isolamento dei servizi in processi separati. Idealmente, si vorrebbe isolare all'interno di container, ma un gran numero di servizi scritti in .NET Framework funziona solo su Windows. Adesso iniziano a emergere servizi su .NET Core, ma sono ancora pochi.

Flessibilità nel deployment. Vorremmo combinare i servizi secondo le nostre necessità, e non in base a quanto imposto dal codice.

Utilizzo di nuove tecnologie. Questo è interessante per ogni programmatore.

Problemi di transizione


Certo, se fosse semplice scomporre un monolite in microservizi, non ci sarebbe bisogno di parlarne alle conferenze o scrivere articoli. Ci sono molte insidie in questo processo, e descriverò le principali che ci hanno ostacolato.

Il primo problema tipica per la maggior parte dei monoliti: coesione della logica aziendale. Quando scriviamo un monolite, vogliamo riutilizzare le nostre classi per evitare di scrivere codice superfluo. Ma con il passaggio ai microservizi, questo diventa un problema: tutto il codice è abbastanza strettamente legato, e risulta difficile separare i servizi.

Al momento dell'inizio dei lavori, nel repository c'erano oltre 500 progetti e oltre 700.000 righe di codice. Si tratta di una soluzione di dimensioni considerevoli e il secondo problema. Semplicemente prendere e dividerlo in microservizi non sembrava possibile.

Il terzo problema è la mancanza dell'infrastruttura necessaria. In effetti, ci siamo occupati di copiare manualmente il codice sorgente sui server.

Come passare da un monolito a microservizi


Separazione dei microservizi

Innanzitutto, abbiamo subito definito che la separazione dei microservizi è un processo iterativo. Ci veniva sempre richiesto di portare avanti parallelamente lo sviluppo delle esigenze aziendali. Come intendiamo attuare tecnicamente questo è un nostro problema. Pertanto, ci siamo preparati a un processo iterativo. In un'applicazione di grandi dimensioni, non si può fare diversamente se non è pronta a essere riscritta.

Quali metodi utilizziamo per la separazione dei microservizi?

Primo metodo — estrarre moduli esistenti come servizi. In questo caso, siamo stati fortunati: esistevano già servizi configurati che funzionavano tramite il protocollo WCF. Erano distribuiti in assembly separati. Noi li trasferivamo singolarmente, aggiungendo a ciascun assembly un piccolo modulo di avvio. Questo era scritto utilizzando la fantastica libreria Topshelf, che consente di eseguire l'applicazione sia come servizio che come console. Questo è comodo per il debug, poiché non sono necessari progetti aggiuntivi nella soluzione.

I servizi erano collegati dalla logica di business, poiché utilizzavano assembly condivisi e operavano con un database comune. Era difficile definirli microservizi in senso stretto. Tuttavia, potevamo fornire questi servizi separatamente, in processi diversi. Già questo riduceva l'impatto reciproco, diminuendo il problema dello sviluppo parallelo e del singolo punto di guasto.

L'assembly con l'host è solo una riga di codice nella classe Program. Abbiamo nascosto il lavoro con Topshelf in una classe di supporto.

namespace RBA.Services.Accounts.Host
{
   internal class Program
   {
      private static void Main(string[] args)
      {
        HostRunner.Run("RBA.Services.Accounts.Host");

       }
    }
}

Il secondo modo per isolare i microservizi: creare per affrontare nuove sfide. Se nel processo il monolite non cresce, è già un ottimo segno, significa che ci stiamo muovendo nella direzione giusta. Per affrontare nuove sfide, abbiamo cercato di realizzare servizi separati. Quando possibile, abbiamo creato servizi più "canonici", che gestiscono completamente il proprio modello di dati e un database separato.

Come molti, abbiamo iniziato con servizi di autenticazione e autorizzazione. Sono perfetti per questo scopo. Sono indipendenti, generalmente hanno un modello di dati separato. Non interagiscono con il monolite, ma solo lui si rivolge a loro per risolvere determinati compiti. Su questi servizi possiamo iniziare il passaggio a una nuova architettura, ottimizzare l'infrastruttura, provare alcune soluzioni legate alle librerie di rete, ecc. Nella nostra organizzazione non ci sono team che non siano riusciti a realizzare un servizio di autenticazione.

Il terzo modo per estrarre microservizi, che utilizziamo, è un po’ specifico per noi. Si tratta di separare la logica di business dallo strato UI. La nostra principale applicazione UI è desktop e, come il backend, è scritta in C#. Gli sviluppatori commettevano errori e a volte portavano nella UI logiche che avrebbero dovuto esistere nel backend e essere riutilizzate.

Se guardiamo un esempio reale dal codice della parte UI, si può vedere che gran parte di questa soluzione contiene la reale logica di business, utile in altri processi, non solo per costruire moduli UI.

Transizione da monolite a microservizi: storia e pratica

La logica reale della UI è rappresentata solo dagli ultimi pochi righi. L'abbiamo trasferita sul server per poterla riutilizzare, riducendo così l’UI e raggiungendo una corretta architettura.

Il quarto, il modo più importante per evidenziare i microservizi, che consente di ridurre il monolite, è l'estrazione dei servizi esistenti con ristrutturazione. Quando estraiamo i moduli esistenti così come sono, il risultato non sempre soddisfa gli sviluppatori, e il processo aziendale potrebbe essere diventato obsoleto dal momento della creazione delle funzionalità. Grazie al refactoring, possiamo sostenere un nuovo processo aziendale, poiché le esigenze del business cambiano continuamente. Possiamo migliorare il codice sorgente, eliminare difetti noti e creare un modello di dati di qualità superiore. Si accumulano molti vantaggi.

La separazione dei servizi con gestione dei contesti è indissolubilmente legata al concetto di contesto limitato. Questo concetto proviene dalla progettazione orientata agli oggetti. Indica un'area del modello di dominio in cui tutti i termini di un linguaggio univocamente definiti. Consideriamo ad esempio il contesto delle assicurazioni e delle fatture. Abbiamo un'applicazione monolitica e dobbiamo lavorare con una fattura nelle assicurazioni. Ci aspettiamo che lo sviluppatore trovi in un'altra build la classe esistente "Fattura", faccia riferimento ad essa dalla classe "Assicurazione" e otteniamo un codice funzionante. Il principio DRY sarà rispettato, e il compito verrà completato più velocemente utilizzando il codice esistente.

Alla fine risulta che i contesti delle fatture e delle assicurazioni sono collegati. Quando emergeranno nuove esigenze, questa connessione ostacolerà lo sviluppo, aumentando la complessità di una logica di business già complicata. Per risolvere questo problema, è necessario nel codice identificare i confini tra i contesti ed eliminare le loro violazioni. Ad esempio, per il contesto delle assicurazioni, potrebbe essere sufficiente un numero di fattura di 20 cifre e la data di apertura della fattura.

Per separare questi contesti limitati e avviare il processo di estrazione dei microservizi da una soluzione monolitica, abbiamo adottato un approccio che prevede la creazione di API esterne all'interno dell'applicazione. Quando sapevamo che un certo modulo doveva diventare un microservizio o subire alcune modifiche nel processo, effettuavamo immediatamente chiamate alla logica che apparteneva a un altro contesto limitato tramite chiamate esterne. Ad esempio, attraverso REST o WCF.

Abbiamo deciso fermamente di non evitare codice che richiederebbe l'uso di transazioni distribuite. Nel nostro caso, è stato relativamente facile rispettare questa regola. Fino ad ora, non si sono verificate situazioni in cui fossero necessarie transazioni distribuite rigorose: è sempre stata sufficiente la coerenza finale tra i moduli.

Consideriamo un esempio concreto. Abbiamo il concetto di orchestratore — un pipeline che gestisce l'entità "richiesta". Creiamo a turno un cliente, un conto e una carta bancaria. Se il cliente e il conto vengono creati con successo, ma la creazione della carta fallisce, la richiesta non passa allo stato "riuscita" e rimane nello stato "carta non creata". In futuro, un'attività in background la riprenderà e la completerà. Il sistema si trova per un certo periodo in uno stato di incoerenza, ma questo, fondamentalmente, ci va bene.

Nel caso in cui si presentasse una situazione in cui sia necessario mantenere coerentemente parte dei dati, probabilmente procederemo a consolidare il servizio per gestire tutto in un unico processo.

Esaminiamo l'esempio di estrazione di un microservizio. In che modo possiamo portarlo in produzione in modo relativamente sicuro? In questo esempio, abbiamo una parte separata del sistema — il modulo di gestione stipendi, uno dei cui segmenti di codice vorremmo rendere microserviziale.

Transizione da monolite a microservizi: storia e pratica

Innanzitutto, creiamo un microservizio riscrivendo il codice. Miglioriamo alcuni aspetti che non ci soddisfacevano. Implementiamo le nuove esigenze aziendali del cliente. Aggiungiamo un API Gateway nel collegamento tra UI e backend, che fornirà il passaggio delle chiamate.

Transizione da monolite a microservizi: storia e pratica

Successivamente, rilasciamo questa configurazione in produzione, ma in modalità pilota. La maggior parte degli utenti continua a utilizzare i vecchi processi aziendali. Per i nuovi utenti, sviluppiamo una nuova versione dell'applicazione monolitica, che non include più questo processo. In sostanza, abbiamo un collegamento pilota tra il monolite e il microservizio.

Transizione da monolite a microservizi: storia e pratica

Con un pilota di successo, ci rendiamo conto che la nuova configurazione è effettivamente funzionante, possiamo eliminare il vecchio monolite e mantenere la nuova configurazione al posto della soluzione precedente.

Transizione da monolite a microservizi: storia e pratica

In sintesi, utilizziamo praticamente tutti i metodi esistenti per separare il codice sorgente del monolite. Tutti questi ci permettono di ridurre le dimensioni delle parti dell'applicazione e di trasferirle a nuove librerie, migliorando così la qualità del codice sorgente.

Lavoro con il DB


I database sono più difficili da suddividere rispetto al codice sorgente, poiché contengono non solo lo schema attuale, ma anche dati storici accumulati.

Il nostro database, come molti altri, presentava un'altra importante mancanza: la sua enorme dimensione. Questo database era stato progettato seguendo una logica aziendale complessa e si erano accumulate connessioni tra tabelle di diversi contesti limitati.

Nel nostro caso, per completare tutte le difficoltà (grande database, molte connessioni e a volte confini poco chiari tra le tabelle) è emerso un problema comune in molti progetti di grandi dimensioni: l'uso del modello di database condiviso. I dati venivano estratti dalle tabelle tramite view, attraverso replicazione e caricati in altri sistemi dove era necessaria tale replicazione. Di conseguenza, non potevamo estrarre le tabelle in uno schema separato, poiché venivano utilizzate attivamente.

Nella suddivisione ci aiuta proprio la segmentazione in contesti limitati nel codice. Questo generalmente ci offre una buona comprensione di come suddividiamo i dati a livello di database. Sappiamo quali tabelle appartengono a un contesto limitato e quali a un altro.

Abbiamo adottato due approcci generali per la separazione del database: la separazione delle tabelle esistenti e la separazione con ristrutturazione.

La separazione delle tabelle esistenti è un metodo che si applica bene quando la struttura dei dati è di qualità, soddisfa i requisiti aziendali e va bene per tutti. In questo caso, possiamo estrarre in uno schema separato le tabelle esistenti.

La separazione con ristrutturazione è necessaria quando il modello di business è cambiato drasticamente e le tabelle non soddisfano più le nostre esigenze.

Separazione delle tabelle esistenti. Dobbiamo determinare quali elementi separare. Senza questa conoscenza non otterremo risultati, e qui ci aiuta la separazione dei contesti limitati nel codice. In genere, se riusciamo a comprendere i confini dei contesti nel codice sorgente, diventa chiaro quali tabelle devono essere incluse nell'elenco per la separazione.

Immaginiamo di avere una soluzione in cui due moduli di un monolite interagiscono con un'unica base di dati. Dobbiamo assicurarci che solo un modulo interagisca con la porzione di tabelle separabili, mentre l'altro inizi a interagire con essa tramite API. Inizialmente, è sufficiente che tramite API avvenga solo la scrittura. Questa è una condizione necessaria affinché possiamo parlare di indipendenza dei microservizi. Le connessioni in lettura possono rimanere, finché non ci sono grandi problemi.

Transizione da monolite a microservizi: storia e pratica

Il passo successivo consiste già nel poter estrarre la parte di codice che lavora con le tabelle separabili, con o senza riprogettazione, in un microservizio autonomo e farlo funzionare in un processo separato, in un container. Questo sarà un servizio separato con una connessione alla base dati del monolite e alle tabelle che non lo riguardano direttamente. Il monolite continua a interagire in lettura con la parte separata.

Transizione da monolite a microservizi: storia e pratica

Successivamente, elimineremo questa connessione, cioè trasferiremo anche la lettura dei dati dell'applicazione monolitica dalle tabelle separabili all'API.

Transizione da monolite a microservizi: storia e pratica

Iniziamo estraendo dalla base di dati generale le tabelle con cui interagisce solo il nuovo microservizio. Possiamo spostare le tabelle in uno schema separato o persino in un database fisico distinti. Rimane una connessione in lettura tra il microservizio e il database del monolite, ma non c'è nulla di cui preoccuparsi, in questa configurazione può funzionare a lungo.

Transizione da monolite a microservizi: storia e pratica

L'ultimo passaggio è rimuovere completamente tutte le connessioni. In questo caso, potrebbe essere necessaria una migrazione dei dati dalla base di dati principale. A volte desideriamo riutilizzare in diverse basi dati alcuni dati replicabili da sistemi esterni o delle referenze. Questo accade periodicamente.

Transizione da monolite a microservizi: storia e pratica

Separazione con rielaborazione. Questo metodo è molto simile al primo, solo che avviene al contrario. Viene immediatamente creata una nuova base di dati e un nuovo microservizio, che interagisce con il monolite tramite API. Tuttavia, rimane un insieme di tabelle del database che vogliamo eliminare in futuro. Non ci sarà più bisogno di esse, nella nuova configurazione le abbiamo sostituite.

Transizione da monolite a microservizi: storia e pratica

Affinché questo schema funzioni, probabilmente avremo bisogno di un periodo di transizione.

Ci sono due approcci possibili.

Primo: Duplichiamo tutti i dati nei nuovi e vecchi database. In questo caso, abbiamo un'eccesso di dati e possono sorgere problemi di sincronizzazione. Tuttavia, possiamo utilizzare due clienti diversi. Uno lavorerà con la nuova versione, l'altro con quella vecchia.

Secondo: Separiamo i dati in base a un particolare criterio aziendale. Ad esempio, nel nostro sistema erano presenti 5 prodotti memorizzati nel vecchio database. Il sesto, per una nuova esigenza aziendale, viene inserito nel nuovo database. Ma avremo bisogno di un API Gateway per sincronizzare questi dati e mostrare al cliente da dove e cosa prelevare.

Entrambi gli approcci sono validi, scegli in base alla situazione.

Dopo aver verificato che tutto funziona, è possibile disattivare la parte del monolite che gestisce le vecchie strutture del database.

Transizione da monolite a microservizi: storia e pratica

L'ultimo passo sarà eliminare le vecchie strutture di dati.

Transizione da monolite a microservizi: storia e pratica

In sintesi, possiamo dire che abbiamo problemi con il database: è difficile lavorarci rispetto al codice sorgente, è più complicato dividerlo, ma è possibile e necessario. Abbiamo trovato alcuni modi per farlo in modo abbastanza sicuro, poiché è comunque più facile commettere errori con i dati rispetto al codice sorgente.

Lavoro con il codice sorgente


Ecco com'era la struttura del codice sorgente quando abbiamo iniziato ad analizzare il progetto monolitico.

Transizione da monolite a microservizi: storia e pratica

È possibile dividerlo in tre strati. Questo strato comprende moduli eseguibili, plugin, servizi e singole attività. In effetti, erano i punti di accesso all'interno della soluzione monolitica. Tutti erano strettamente uniti dallo strato Common. Qui si trovava la logica di business utilizzata dai servizi comuni e una miriade di legami. Ogni servizio e plugin utilizzava fino a 10 o più assembly comuni, a seconda della loro grandezza e della correttezza degli sviluppatori.

Siamo stati fortunati, avevamo librerie di infrastruttura che potevamo utilizzare separatamente.

A volte si presentava la situazione in cui alcuni oggetti Common in realtà non appartenevano a questo strato, ma erano librerie infrastrutturali. Questo veniva risolto attraverso il rinominare.

Le preoccupazioni maggiori riguardavano i contesti limitati. A volte, 3-4 contesti si mescolavano in un'unica build comune e si utilizzavano l'uno con l'altro all'interno delle stesse funzioni aziendali. Era necessario capire dove poter separare e quali fossero i confini, e cosa fare successivamente con il mapping di questa separazione alle build del codice sorgente.

Abbiamo formulato alcune regole per il processo di separazione del codice.

Primo: non volevamo più condividere la logica di business tra servizi, attività e plugin. Volevamo rendere la logica di business indipendente all'interno dei microservizi. D'altra parte, i microservizi, idealmente, sono percepiti come servizi che esistono in totale indipendenza. Ritengo che questo approccio sia piuttosto dispendioso e difficile da raggiungere, poiché, ad esempio, i servizi C# saranno comunque collegati alla libreria standard. Il nostro sistema è scritto in C#, e al momento non abbiamo dovuto utilizzare altre tecnologie. Pertanto, abbiamo deciso che ci possiamo permettere di utilizzare assembly tecnici comuni. L'importante è che non ci siano frammenti di logica di business. Se avete un wrapper comodo per l'ORM che state utilizzando, copiarlo da un servizio all'altro è molto costoso.

Il nostro team è appassionato di design orientato agli oggetti, quindi l'«architettura a cipolla» si è rivelata perfetta per noi. Alla base dei nostri servizi non c'è un data access layer, ma un'aggregazione con logica di dominio, che contiene solo la logica di business ed è priva di legami con l'infrastruttura. In questo modo, possiamo sviluppare indipendentemente l'aggregazione di dominio per risolvere i problemi legati ai framework.

In questa fase abbiamo incontrato il primo grande problema. Il servizio doveva fare riferimento a un'unica build di dominio, mentre volevamo creare una logica indipendente, e qui il principio DRY ci ha ostacolato notevolmente. Gli sviluppatori volevano evitare la duplicazione riutilizzando le classi da build adiacenti, e di conseguenza i domini hanno cominciato di nuovo a collegarsi tra loro. Abbiamo analizzato i risultati e abbiamo deciso che, forse, il problema risiedeva anche nell'architettura della memorizzazione del codice sorgente. Avevamo un grande repository che conteneva tutto il codice sorgente. Era molto difficile raccogliere la soluzione per l'intero progetto su una macchina locale. Pertanto, per le parti del progetto venivano create piccole soluzioni separate, e nessuno vietava di aggiungere in esse una Common- o una build di dominio e riutilizzarle. L'unico strumento che non ci permetteva di farlo era il code review. Ma anche questo talvolta falliva.

Allora abbiamo iniziato a passare a un modello con repository separati. La logica aziendale ha smesso di trapelare da un servizio all'altro, i domini sono diventati realmente indipendenti. I contesti limitati sono supportati in modo più chiaro. Come riutilizziamo le librerie infrastrutturali? Le abbiamo scorporate in un repository separato, quindi le abbiamo inserite in pacchetti Nuget, che abbiamo collocato in Artifactory. Con qualsiasi modifica, la compilazione e la pubblicazione avvengono automaticamente.

Transizione da monolite a microservizi: storia e pratica

I nostri servizi ora fanno riferimento ai pacchetti infrastrutturali interni proprio come a quelli esterni. Scarichiamo le librerie esterne da Nuget. Per lavorare con Artifactory, dove collocavamo questi pacchetti, abbiamo utilizzato due gestori di pacchetti. Nei piccoli repository abbiamo usato Nuget. Nei repository con più servizi abbiamo utilizzato Paket, che garantisce maggiore coerenza delle versioni tra i moduli.

Transizione da monolite a microservizi: storia e pratica

In questo modo, lavorando sul codice sorgente, modificando leggermente l'architettura e separando i repository, rendiamo i nostri servizi più indipendenti.

Problemi di infrastruttura


La maggior parte degli svantaggi nel passaggio ai microservizi è legata all'infrastruttura. Avrai bisogno di distribuzioni automatizzate e di nuove librerie per la gestione dell'infrastruttura.

Installazione manuale negli ambienti

Inizialmente, installavamo manualmente le soluzioni sugli ambienti. Per automatizzare questo processo, abbiamo creato una pipeline CI/CD. Abbiamo scelto il processo di continuous delivery, poiché il continuous deployment non è ancora accettabile per noi dal punto di vista dei processi aziendali. Pertanto, il rilascio in produzione avviene con un clic, mentre la fase di test è automatica.

Transizione da monolite a microservizi: storia e pratica

Utilizziamo Atlassian, Bitbucket per l'archiviazione del codice sorgente e Bamboo per la build. Ci piace scrivere script di build in Cake, poiché è lo stesso C#. I pacchetti pronti arrivano in Artifactory, e Ansible viene automaticamente inviato ai server di test, quindi possono essere testati immediatamente.

Transizione da monolite a microservizi: storia e pratica

Logging separato


In passato, una delle idee del monolite era garantire la registrazione condivisa. Dovevamo anche capire cosa fare con i singoli log che si trovavano sui dischi. I log vengono scritti in file di testo. Abbiamo deciso di utilizzare il classico stack ELK. Non abbiamo scritto direttamente in ELK tramite i provider, ma abbiamo scelto di modificare i log di testo e inserire in essi l'ID di tracciamento come identificatore, aggiungendo il nome del servizio, in modo da poter analizzare poi questi log.

Transizione da monolite a microservizi: storia e pratica

Con Filebeat abbiamo la possibilità di raccogliere i nostri log da server, poi elaborarli, costruire query con Kibana nell'interfaccia utente e vedere come avveniva la chiamata tra i servizi. In questo ci aiuta molto l'ID di tracciamento.

Test e debugging dei servizi correlati


Inizialmente non capivamo completamente come debbiamo fare il debug dei servizi in fase di sviluppo. Con il monolite era semplice, lo avviavamo sulla macchina locale. Inizialmente cercavamo di fare lo stesso con i microservizi, ma a volte per avviare completamente un microservizio è necessario avviarne anche altri, e questo non è pratico. Abbiamo capito che era necessario passare a un modello in cui lasciamo sulla macchina locale solo il servizio o i servizi che vogliamo debuggare. Gli altri servizi vengono utilizzati da server che corrispondono alla configurazione di produzione. Dopo il debug, durante i test, per ogni compito sul server di test vengono distribuiti solo i servizi modificati. In questo modo, la soluzione viene testata nella forma in cui si troverà in produzione in futuro.

Ci sono server che ospitano solo le versioni di produzione dei servizi. Questi server sono necessari in caso di incidenti, per verificare la distribuzione prima del deployment e per formazione interna.

Abbiamo implementato un processo di test automatico utilizzando la popolare libreria Specflow. I test vengono eseguiti automaticamente tramite NUnit subito dopo il deploy da Ansible. Se la copertura del task è completamente automatica, non è necessaria la verifica manuale. Tuttavia, a volte è comunque richiesto un ulteriore testing manuale. Per determinare quali test eseguire per un task specifico, utilizziamo i tag in Jira.

Inoltre, è aumentata la necessità di effettuare test di carico, precedentemente condotti solo in rare occasioni. Per eseguire i test utilizziamo JMeter, per il loro storage InfluxDB, e per visualizzare i grafici del processo Grafana.

Quali risultati abbiamo ottenuto?


Prima di tutto, abbiamo eliminato il concetto di 'release'. Sono scomparsi i mostruosi rilasci bimestrali, quando questa macchina veniva distribuita in ambiente di produzione, interrompendo temporaneamente i processi aziendali. Ora distribuiamo servizi in media ogni 1,5 giorni, raggruppandoli, poiché entrano in produzione dopo un'approvazione.

Nel nostro sistema non ci sono errori fatali. Se rilasciamo un microservizio con un errore, la funzionalità associata sarà compromessa, mentre tutte le altre funzionalità rimarranno intatte. Questo migliora notevolmente l'esperienza dell'utente.

Possiamo gestire lo schema di distribuzione. È possibile isolare i gruppi di servizi dal resto della soluzione, se necessario.

Inoltre, abbiamo ridotto significativamente il problema delle lunghe code di modifiche. Abbiamo creato team di prodotto separati che lavorano su alcune parti dei servizi in modo indipendente. Qui, il processo Scrum si adatta bene. Ogni team può avere un proprio proprietario di prodotto che assegna i compiti.

Riepilogo

  • I microservizi sono particolarmente adatti per la decomposizione di sistemi complessi. Durante il processo, iniziamo a capire cosa c'è nel nostro sistema, quali contesti limitati esistono e dove passano i loro confini. Questo ci consente di distribuire correttamente le modifiche tra i moduli e di evitare che il codice si confonda.
  • I micrservizi offrono vantaggi organizzativi. Spesso se ne parla solo come di architettura, ma ogni architettura è necessaria per soddisfare le esigenze aziendali, e non esiste per sé stessa. Pertanto, possiamo affermare che i micrservizi sono adatti per risolvere compiti in piccoli team, considerando la popolarità del Scrum al giorno d'oggi.
  • La suddivisione è un processo iterativo. Non è possibile prendere un'applicazione e semplicemente dividerla in micrservizi. Il prodotto risultante difficilmente sarà funzionante. Quando si estraggono i micrservizi, è vantaggioso riscrivere il legacy esistente, cioè trasformarlo in codice che ci piace e soddisfa meglio le esigenze aziendali in termini di funzionalità e velocità.

    Una piccola avvertenza: I costi di transizione verso i microservizi sono piuttosto sostanziali. Solo per risolvere i problemi di infrastruttura è stato impiegato molto tempo. Pertanto, se hai un'applicazione piccola che non richiede scalabilità specifica, se non ci sono molti clienti che competono per l'attenzione e il tempo del tuo team, allora forse i microservizi non sono ciò di cui hai bisogno oggi. È piuttosto costoso. Se inizi il processo con i microservizi, i costi iniziali saranno maggiori rispetto all'avvio dello stesso progetto con uno sviluppo monolitico.

    P.S. Una narrazione più emotiva (come se fosse personale per te) – su link.
    Qui c'è la versione completa della relazione.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster