Transizione da monolite a microservizi: storia e pratica

In questo articolo parlerò di come il progetto su cui lavoro si è trasformato da un grande monolite in un insieme di microservizi.

Il progetto ha iniziato la sua storia molto tempo fa, all'inizio degli anni 2000. Le prime versioni furono scritte in Visual Basic 6. Con il passare del tempo, è diventato chiaro che lo sviluppo in questo linguaggio sarebbe stato difficile da mantenere in futuro, poiché l'IDE e il linguaggio stesso si sviluppano lentamente. Alla fine degli anni 2000, si decise di passare a C# più promettente. La nuova versione è stata scritta parallelamente al miglioramento della vecchia, con sempre più codice su .NET. Il backend in C# inizialmente era orientato verso un'architettura di servizi, tuttavia nello sviluppo sono state utilizzate librerie comuni con logica, e i servizi venivano avviati in un unico processo. È risultato un'applicazione che chiamavamo "monolite di servizio".

Uno dei pochi vantaggi di tale accoppiamento era la possibilità per i servizi di chiamarsi a vicenda tramite API esterne. C'erano chiare premesse per passare a un'architettura di servizi più corretta e, in prospettiva, a un'architettura a microservizi.

Abbiamo iniziato il nostro lavoro di decomposizione intorno al 2015. Anche se non abbiamo ancora raggiunto uno stato ideale - ci sono rimasti parti del grande progetto che sono difficili da chiamare monoliti, ma non assomigliano 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 esistente


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

Svantaggi della soluzione precedente

Unico punto di fallimento
Avevamo un unico punto di fallimento: l'applicazione su .NET veniva eseguita in un unico processo. Se uno dei moduli andava in errore, 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 in caso di errore di programmazione, nemmeno la riserva era di aiuto.

Coda delle modifiche
Questo difetto è principalmente organizzativo. La nostra applicazione ha molteplici clienti, e tutti vogliono apportare modifiche il prima possibile. In passato, era impossibile farlo in parallelo e tutti i clienti dovevano mettersi in coda. Questo processo creava frustrazione per il business, poiché dovevano dimostrare che il loro compito aveva valore. E il team di sviluppo spendeva tempo per organizzare questa coda. Questo richiedeva molte energie e il prodotto alla fine non poteva cambiare così rapidamente come avrebbero voluto.

Utilizzo inefficiente delle risorse
Quando posizionavamo i servizi in un unico processo, copiavamo sempre completamente la configurazione da un server all'altro. Volevamo posizionare i servizi più gravosi separatamente, per non sprecare risorse e ottenere una gestione più flessibile del nostro schema di distribuzione.

Difficoltà nell'implementazione di tecnologie moderne
Un problema familiare a tutti gli sviluppatori: c'è la voglia di introdurre tecnologie moderne nel progetto, ma non ci sono possibilità. Con una soluzione monolitica, qualsiasi aggiornamento della libreria corrente, per non parlare del passaggio a una nuova, diventa un compito piuttosto complesso. Occorre molto tempo per convincere il team leader che questo porterà più vantaggi delle energie spese.

Difficoltà nell'implementare cambiamenti
Questo era il problema più serio: emettevamo rilasci ogni due mesi.
Ogni rilascio si trasformava in una vera catastrofe per la banca, nonostante i test e gli sforzi degli sviluppatori. Il business capiva che all'inizio della settimana non avrebbe funzionato parte della funzionalità. E gli sviluppatori sapevano che li aspettava una settimana di seri incidenti.
Desiderio di cambiare la situazione c'era da parte di tutti.

Aspettative dai microservizi


Emessa la componente al termine della preparazione. Emessa la componente man mano che era pronta grazie alla decomposizione della soluzione e alla separazione dei vari processi.

Piccole squadre di prodotto. È importante perché gestire un grande team che lavora su un vecchio monolite è difficile. Un team del genere deve seguire un processo rigoroso, mentre c'è desiderio di maggiore creatività e indipendenza. Solo le piccole squadre possono permetterselo.

Isolamento dei servizi in processi separati. Idealmente, vorremmo isolare in contenitori, ma un gran numero di servizi scritti su .NET Framework viene eseguito solo su Windows. Attualmente stanno emergendo servizi su .NET Core, ma ce ne sono ancora pochi.

Flessibilità nel deployment. Vorremmo combinare i servizi come necessario per noi, e non come impone il codice.

Utilizzo di nuove tecnologie. È un aspetto interessante per qualsiasi programmatore.

Problemi nella transizione


Certo, se fosse facile suddividere un monolite in microservizi, non se ne parlerebbe nei convegni e non si scriverebbero articoli. Ci sono molte insidie in questo processo, descriverò le principali che ci hanno ostacolato.

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

Al momento dell'inizio dei lavori nel repository c'erano più di 500 progetti e oltre 700.000 righe di codice. Questa è una soluzione piuttosto grande e il secondo problema. Non era possibile semplicemente prenderlo e dividerlo in microservizi.

Il terzo problema — mancanza di infrastruttura necessaria. Di fatto, ci occupavamo di copiare manualmente il codice sorgente sui server.

Come passare da un monolite a microservizi


Identificazione dei microservizi

Innanzitutto, abbiamo subito stabilito che la separazione dei microservizi è un processo iterativo. Ci è sempre stato richiesto di condurre parallelamente lo sviluppo delle attività aziendali. Come realizzeremo tecnicamente ciò è un nostro problema. Pertanto, ci siamo preparati a un processo iterativo. Non può essere altrimenti se hai una grande applicazione e non è inizialmente pronta per essere riscritta.

Quali metodi utilizziamo per identificare i microservizi?

Primo modo — estrarre i moduli esistenti come servizi. In questo senso siamo stati fortunati: c'erano già servizi configurati che operavano secondo il protocollo WCF. Erano distribuiti su diversi assembly. Li abbiamo trasferiti singolarmente, aggiungendo a ciascun assembly un piccolo modulo di avvio. Questo è stato scritto utilizzando l'ottima libreria Topshelf, che consente di eseguire l'applicazione sia come servizio che come console. È comodo per il debug, poiché non richiede progetti aggiuntivi nella soluzione.

I servizi erano collegati per logica aziendale, poiché utilizzavano assembly comuni e lavoravano con un database condiviso. Era difficile definirli microservizi nel senso più puro. Tuttavia, potevamo pubblicare questi servizi separatamente, in processi diversi. Già questo permetteva di ridurre l'influenza reciproca, attenuando i problemi di sviluppo parallelo e il punto unico di errore.

L'assembly con l'host è composta da una sola riga di codice nella classe Program. Il lavoro con Topshelf è stato nascosto 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 estrarre microservizi: crearne di nuovi per affrontare obiettivi. Se in questo modo il monolite non cresce, è già un ottimo segno, significa che stiamo procedendo nella direzione giusta. Per affrontare nuovi compiti cercavamo di creare servizi separati. Se c'era l'opportunità, creavamo servizi più "canonici", che gestiscono completamente il proprio modello di dati, con un database separato.

Iniziammo, come molti, con servizi di autenticazione e autorizzazione. Questi sono perfetti per questo scopo. Sono indipendenti e, in genere, hanno un modello di dati isolato. Non interagiscono direttamente con il monolite, solo quest'ultimo si rivolge a loro per risolvere determinate questioni. Su questi servizi si può iniziare la transizione verso una nuova architettura, debuggarvi l'infrastruttura, provare alcuni approcci legati 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 estrarre la logica di business dallo strato UI. La nostra principale applicazione UI è desktop, ed è scritta in C#, così come il backend. Gli sviluppatori commettevano occasionalmente errori estraendo nello UI parti della logica che avrebbero dovuto esistere nel backend e essere riutilizzate.

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

Transizione da monolite a microservizi: storia e pratica

La logica reale dell'UI è presente solo nelle ultime righe. L'abbiamo trasferita sul server per poterla riutilizzare, riducendo l'UI e ottenendo una corretta architettura.

Il quarto, modo più importante di distinguere i microservizi, che consente di ridurre il monolite, è quello di estrarre i servizi esistenti con una revisione. Quando estraiamo i moduli esistenti così come sono, il risultato non sempre piace agli sviluppatori, e il processo aziendale nel tempo dalla creazione della funzionalità potrebbe essere obsoleto. Grazie al refactoring, possiamo supportare un nuovo processo aziendale, poiché le esigenze dell'azienda cambiano costantemente. Possiamo migliorare il codice sorgente, eliminare difetti noti, e creare un modello di dati di maggiore qualità. Se accumulano molti vantaggi.

La separazione dei servizi con revisione è indissolubilmente legata al concetto di contesto limitato. Questo concetto proviene dal design orientato agli oggetti. Significa un'area del modello di dominio in cui tutti i termini di un linguaggio comune sono definiti in modo univoco. Prendiamo come esempio il contesto delle assicurazioni e delle fatture. Abbiamo un'applicazione monolitica, e dobbiamo lavorare con la fattura nelle assicurazioni. Ci aspettiamo che lo sviluppatore trovi in un'altra assembly la classe esistente "Fattura", faccia riferimento ad essa dalla classe "Assicurazione", e otteniamo un codice funzionante. Il principio DRY sarà rispettato, e il compito sarà completato più rapidamente grazie all'uso del codice esistente.

Alla fine si scopre che i contesti di conti e assicurazioni sono collegati. Quando appariranno nuove richieste, questo legame ostacolerà lo sviluppo, aumentando la complessità di una logica di business già complessa. Per affrontare questo problema, è necessario nel codice individuare i confini tra i contesti e rimuovere le loro violazioni. Ad esempio, al contesto delle assicurazioni potrebbe essere sufficiente un numero di conto della Banca Centrale di 20 cifre e la data di apertura del conto.

Per separare questi contesti limitati e avviare il processo di estrazione dei microservizi da una soluzione monolitica, abbiamo utilizzato un approccio come la creazione di API esterne all'interno dell'applicazione. Se sapevamo che un dato modulo doveva diventare un microservizio e modificarsi in qualche modo nel processo, immediatamente effettuavamo chiamate alla logica appartenente a un altro contesto limitato tramite chiamate esterne. Ad esempio, tramite REST o WCF.

Abbiamo deciso di non evitare il codice che richiede di eseguire transazioni distribuite. Nel nostro caso, è stato abbastanza facile rispettare questa regola. Finora non abbiamo mai incontrato situazioni in cui siano davvero necessarie transazioni distribuite rigorose: è bastata la coerenza finale tra i moduli.

Consideriamo un esempio concreto. Abbiamo il concetto di orchestratore — un nastro trasportatore che elabora l'entità "richiesta". Esso crea a turno un cliente, un conto e una carta bancaria. Se il cliente e il conto vengono creati con successo e la creazione della carta fallisce, la richiesta non passa allo stato di "successo" e rimane nello stato di "carta non creata". In futuro, un'attività in background la recupererà e la concluderà. Il sistema rimarrà per un certo periodo in uno stato di incoerenza, ma questo ci va bene nel complesso.

Nel caso in cui si presenti comunque una situazione in cui sia necessario salvare in modo coerente una parte dei dati, probabilmente procederemo a un accorpamento del servizio per gestire tutto in un unico processo.

Consideriamo un esempio di estrazione di un microservizio. Come possiamo portarlo in modo relativamente sicuro in produzione? In questo esempio abbiamo una parte separata del sistema — un modulo di gestione stipendi, uno dei quali segmenti di codice vogliamo rendere microserviziale.

Transizione da monolite a microservizi: storia e pratica

Iniziamo creando un microservizio, riscrivendo il codice. Ottimizziamo alcuni aspetti che non ci soddisfacevano. Implementiamo nuove requisiti di business del cliente. Aggiungiamo un API Gateway nel collegamento tra UI e backend, che garantirà il passaggio delle chiamate.

Transizione da monolite a microservizi: storia e pratica

Successivamente, rilasciamo questa configurazione in esercizio, ma in modalità pilota. La maggior parte degli utenti continua a lavorare con i vecchi processi aziendali. Per i nuovi utenti sviluppiamo una nuova versione dell'applicazione monolitica, che non contiene più questo processo. Fondamentalmente abbiamo una combinazione di monolite e microservizio in modalità pilota.

Transizione da monolite a microservizi: storia e pratica

Con un pilota di successo comprendiamo che la nuova configurazione è davvero funzionante, possiamo escludere dal calcolo il vecchio monolite e mantenere la nuova configurazione al posto della vecchia soluzione.

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 metodi ci permettono di ridurre le dimensioni delle parti dell'applicazione e di migrarle su nuove librerie, producendo un codice sorgente di qualità superiore.

Creiamo un database — MySQL. Se hai PhpMyAdmin installato, crea un nuovo database "


Il database si presta peggio alla separazione rispetto al codice sorgente, in quanto contiene non solo lo schema attuale, ma anche i dati storici accumulati.

Il nostro database, come molti altri, aveva un altro importante svantaggio: una dimensione enorme. Questo database è stato progettato secondo una logica aziendale complessa del monolite, e tra le tabelle di vari contesti limitati si sono accumulate relazioni.

Nel nostro caso, per completare tutto questo (grande database, molte relazioni, a volte confini poco chiari tra le tabelle) è emerso un problema comune a molti progetti di grandi dimensioni: l'uso del modello di database condiviso. I dati venivano prelevati dalle tabelle attraverso viste, tramite replica e trasferiti in altri sistemi dove era necessaria tale replica. Di conseguenza, non potevamo estrarre le tabelle in uno schema separato, poiché erano utilizzate attivamente.

Nella separazione ci aiuta proprio quella suddivisione in contesti limitati nel codice. Essa, di solito, ci fornisce un'ottima visione di come distribuiamo i dati a livello di database. Comprendiamo quali tabelle appartengono a un contesto limitato e quali a un altro.

Abbiamo applicato due strategie globali per la separazione del database: la separazione delle tabelle esistenti e la separazione con riprogettazione.

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 a tutti. In questo caso, possiamo выделить in uno schema separato le tabelle esistenti.

La separazione con riprogettazione è necessaria quando il modello di business è cambiato notevolmente, e le tabelle non ci soddisfano più.

Separazione delle tabelle esistenti. Dobbiamo determinare cosa andremo a separare. Senza questa conoscenza non sarà possibile, e qui ci aiuterà la separazione dei contesti limitati nel codice. In genere, se riusciamo a comprendere i confini dei contesti nel codice sorgente, è chiaro quali tabelle devono essere incluse nell’elenco da separare.

Immaginiamo di avere una soluzione in cui due moduli di un monolite interagiscono con un unico database. Dobbiamo fare in modo che solo un modulo interagisca con le tabelle da separare, mentre l'altro inizia a interagire con esse tramite API. Inizialmente, è sufficiente che attraverso API vengano effettuate solo scritture. Questo è un requisito fondamentale affinché possiamo parlare di indipendenza dei microservizi. I collegamenti in lettura possono rimanere, finché non ci sono grandi problemi.

Transizione da monolite a microservizi: storia e pratica

Il passo successivo sarà quello di separare il segmento di codice che lavora con le tabelle da separare, con riprogettazione o senza, in un microservizio separato e lanciarlo in un processo o contenitore a parte. Sarà un servizio autonomo con un collegamento al database del monolite e alle tabelle che non gli appartengono direttamente. Il monolite continuerà a interagire in lettura con la parte separata.

Transizione da monolite a microservizi: storia e pratica

Successivamente, rimuoveremo questo collegamento, ovvero anche la lettura dei dati dell'applicazione monolitica dalle tabelle separate sarà trasferita su API.

Transizione da monolite a microservizi: storia e pratica

Poi separeremo dalle tabelle dell'DB comune quelle con cui lavora solo il nuovo microservizio. Possiamo estrarre le tabelle in uno schema separato o persino in un database fisico separato. Rimane un collegamento in lettura tra il microservizio e il database del monolite, ma non c'è nulla di cui preoccuparsi, in questa configurazione può coesistere per un tempo abbastanza lungo.

Transizione da monolite a microservizi: storia e pratica

L'ultimo passo è rimuovere completamente tutte le connessioni. In questo caso, potrebbe essere necessario migrare i dati dalla banca dati principale. A volte vogliamo riutilizzare in diverse banche dati alcuni dati o registri replicati da sistemi esterni. Questo accade periodicamente da noi.

Transizione da monolite a microservizi: storia e pratica

Sezione con rielaborazione. Questo metodo è molto simile al primo, ma avviene in ordine inverso. Viene subito creata una nuova banca dati e un nuovo microservizio che interagisce con il monolite tramite API. Tuttavia, rimane un insieme di tabelle del database che intendiamo eliminare in futuro. Non ci servirà più, nella nuova modellazione l'abbiamo sostituito.

Transizione da monolite a microservizi: storia e pratica

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

Successivamente ci sono due approcci possibili.

Primo: dupliciamo tutti i dati nelle nuove e nelle vecchie banche. In questo caso abbiamo una ridondanza dei dati, possono sorgere problemi di sincronizzazione. Ma possiamo prendere due client diversi. Uno lavorerà con la nuova versione, l'altro con la vecchia.

Secondo: separiamo i dati secondo qualche criterio di business. Ad esempio, nel nostro sistema c'erano 5 prodotti archiviati nella vecchia banca dati. Il sesto, nell'ambito di un nuovo obiettivo di business, lo mettiamo nella nuova banca dati. Ma avremo bisogno di un API Gateway che sincronizzi questi dati e mostri al client da dove e cosa prelevare.

Entrambi gli approcci sono funzionali, scegliete in base alla situazione.

Una volta assicurati che tutto funzioni, possiamo disabilitare la parte del monolite che lavora con le vecchie strutture dei database.

Transizione da monolite a microservizi: storia e pratica

L'ultimo passo sarà l'eliminazione delle vecchie strutture dati.

Transizione da monolite a microservizi: storia e pratica

In sintesi, possiamo dire che abbiamo problemi con i database: è difficile lavorarci rispetto al codice sorgente, è più complicato separare, ma è possibile e necessario farlo. Abbiamo trovato alcuni modi che ci permettono di farlo in modo ragionevolmente sicuro; è comunque più facile commettere errori con i dati rispetto al codice sorgente.

Lavoro con il codice sorgente


Ecco come appariva lo schema del codice sorgente quando abbiamo iniziato ad analizzare il progetto monolitico.

Transizione da monolite a microservizi: storia e pratica

È possibile suddividerla in tre strati. Questo è lo strato dei moduli, dei plugin, dei servizi e delle singole attività avviabili. In effetti, erano i punti d'ingresso all'interno di una soluzione monolitica. Tutti erano rigidamente collegati da uno strato comune. In esso vi era la logica di business utilizzata dai servizi in comune e numerose interconnessioni. Ogni servizio e plugin utilizzava fino a 10 o più assembly comuni, a seconda delle loro dimensioni e della coscienza degli sviluppatori.

Siamo stati fortunati, avevamo librerie infrastrutturali che potevano essere utilizzate separatamente.

A volte si presentava una situazione in cui alcuni oggetti comuni in realtà non appartenevano a questo strato, ma erano librerie infrastrutturali. Questo veniva risolto rinominando.

Le preoccupazioni principali riguardavano i contesti limitati. A volte, 3-4 contesti si mescolavano in un'unica assembly comune e si utilizzavano a vicenda all'interno di una stessa funzione di business. Era necessario capire dove fosse possibile separare e quali fossero i confini, e cosa fare dopo con il mapping di questa separazione sulle assembly 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 completamente in modo indipendente. Ritengo che questo approccio sia un po' sprecone e difficile da raggiungere, poiché ad esempio, i servizi in C# saranno comunque collegati dalla libreria standard. Il nostro sistema è scritto in C#, altre tecnologie non sono state finora utilizzate. Pertanto, abbiamo deciso che potevamo permetterci di utilizzare assembly tecniche comuni. L'importante è che non ci siano frammenti di logica di business al loro interno. Se hai un comodo wrapper sopra l'ORM che utilizzi, allora copiare da un servizio all'altro è molto costoso.

Il nostro team è appassionato di progettazione orientata agli oggetti, quindi l'«architettura a cipolla» si adatta perfettamente a noi. La base dei nostri servizi non è stata il data access layer, ma una raccolta con logica di dominio, che contiene solo logica aziendale ed è priva di collegamenti con l'infrastruttura. In questo modo possiamo modificare in modo indipendente la raccolta di dominio per affrontare i problemi legati ai framework.

In questa fase abbiamo incontrato il primo serio problema. Il servizio doveva fare riferimento a una raccolta di dominio, volevamo rendere la logica indipendente, e in questo ci ostacolava fortemente il principio DRY. Gli sviluppatori volevano riutilizzare classi da raccolte vicine per evitare duplicazioni e, di conseguenza, i domini hanno ricominciato a collegarsi tra loro. Abbiamo analizzato i risultati e deciso che forse il problema risiedeva anche nel modo in cui era strutturato il repository del codice sorgente. Avevamo un grande repository in cui si trovavano tutti i codici sorgenti. Era molto difficile assemblare la soluzione per l'intero progetto sulla macchina locale. Di conseguenza, per le parti del progetto venivano creati piccoli solution separati, e a nessuno era vietato aggiungere in essi una raccolta Common o di dominio e riutilizzarla. L'unico strumento che non ci permetteva di farlo era il codice di revisione. Ma a volte anche questo 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 realmente diventati indipendenti. I contesti limitati sono supportati in modo più chiaro. Come riutilizziamo le librerie infrastrutturali in questo caso? Le abbiamo isolate in un repository separato, poi le abbiamo inserite in pacchetti Nuget, che abbiamo posizionato in Artifactory. Qualsiasi modifica provoca un'assemblaggio e una pubblicazione automatici.

Transizione da monolite a microservizi: storia e pratica

I nostri servizi hanno iniziato a fare riferimento ai pacchetti infrastrutturali interni esattamente come a quelli esterni. Scarichiamo le librerie esterne da Nuget. Per lavorare con Artifactory, dove posizionavamo questi pacchetti, abbiamo applicato due gestori di pacchetti. Nei piccoli repository abbiamo utilizzato anche Nuget. Nei repository con più servizi abbiamo usato 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 delle difficoltà nel passaggio ai microservizi è legata all'infrastruttura. Avrai bisogno di un deployment automatizzato e saranno necessarie nuove librerie per gestire l'infrastruttura.

Installazione manuale negli ambienti

Inizialmente, installavamo manualmente le soluzioni negli ambienti. Per automatizzare questo processo, abbiamo creato una pipeline CI/CD. Abbiamo scelto il processo di continuous delivery, perché il continuous deployment al momento non è accettabile per i nostri processi aziendali. Pertanto, il passaggio in produzione avviene su richiesta, mentre il testing è automatico.

Transizione da monolite a microservizi: storia e pratica

Utilizziamo Atlassian, Bitbucket per l'archiviazione del codice sorgente e Bamboo per la compilazione. Ci piace scrivere gli script di build in Cake, perché è lo stesso C#. In Artifactory arrivano già pacchetti pronti e Ansible viene trasferito automaticamente sui server di test, dove possono essere immediatamente testati.

Transizione da monolite a microservizi: storia e pratica

Logging separato


In passato, una delle idee del monolite era garantire una registrazione unificata. Dovevamo anche capire come gestire i log separati che risiedono sui dischi. I log vengono scritti in file di testo. Abbiamo deciso di utilizzare il classico stack ELK. Non abbiamo optato per scrivere direttamente in ELK tramite provider, ma abbiamo deciso di affinare i log di testo e registrare in essi gli ID di tracciamento come identificatore, aggiungendo il nome del servizio, in modo che questi log possano essere successivamente analizzati.

Transizione da monolite a microservizi: storia e pratica

Con Filebeat otteniamo la possibilità di raccogliere i nostri log da server, poi di elaborarli, utilizzare Kibana per costruire query nell'interfaccia utente e osservare come sono stati effettuati gli inviti tra i servizi. Questo è particolarmente aiutato dagli ID di tracciamento.

Testing e debug dei servizi correlati


Inizialmente non capivamo appieno come ottimizzare i servizi che stavamo sviluppando. Con il monolite era tutto semplice, lo avviavamo sulla macchina locale. Allo stesso modo cercavamo di fare con i microservizi, ma a volte per eseguire un microservizio è necessario avviarne anche altri, il che è scomodo. Abbiamo capito che era necessario passare a un modello in cui lasciavamo sulla macchina locale solo il servizio o i servizi che volevamo debugare. Gli altri servizi vengono utilizzati da server configurati in modo identico a prod. Dopo il debug, durante i test, per ogni task sul server di test vengono distribuiti solo i servizi modificati. In questo modo, la soluzione viene testata nella forma in cui si presenterà in futuro su prod.

Ci sono server su cui sono installate solo versioni di produzione dei servizi. Questi server sono necessari in caso di incidenti, per verificare la distribuzione prima del deploy e per la formazione interna.

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

È aumentata la necessità di test di carico, che in precedenza venivano eseguiti solo in rare occasioni. Per eseguire i test utilizziamo JMeter, per il loro stoccaggio InfluxDB, e per la creazione di grafici del processo Grafana.

Cosa abbiamo ottenuto?


Innanzitutto, ci siamo liberati del concetto di "rilascio". Sono scomparsi i mostruosi rilasci bimestrali, quando questa macchina veniva distribuita nell'ambiente di produzione, interrompendo temporaneamente i processi aziendali. Ora distribuiamo i servizi in media ogni 1,5 giorni, raggruppandoli, poiché entrano in esercizio dopo approvazione.

Nel nostro sistema non ci sono crash fatali. Se rilasciamo un microservizio con un errore, la funzionalità a esso associata sarà compromessa, mentre tutta l'altra funzionalità non sarà influenzata. Questo migliora notevolmente l'esperienza dell'utente.

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

Inoltre, abbiamo notevolmente ridotto il problema dell'alta coda di revisioni. Sono emerse diverse squadre di prodotto che lavorano su alcune parte dei servizi in modo indipendente. Qui già il processo Scrum si adatta bene. Un team specifico può avere un proprietario di prodotto distinto che assegna i compiti.

Riepilogo

  • I microservizi si prestano bene alla scomposizione di sistemi complessi. Durante questo processo, iniziamo a capire cosa c'è nel nostro sistema, quali sono i contesti limitati e dove si trovano i loro confini. Questo consente di distribuire correttamente le revisioni tra i moduli e di evitare confusione nel codice.
  • I microservizi offrono vantaggi organizzativi. Si parla spesso di loro solo come architettura, ma qualsiasi architettura esiste per soddisfare le esigenze del business, non per se stessa. Pertanto, possiamo affermare che i microservizi sono adatti per affrontare compiti con piccoli team, considerando che attualmente lo Scrum è molto popolare.
  • La separazione è un processo iterativo. Non si può prendere un'applicazione e semplicemente dividerla in microservizi. Il prodotto risultante sarà difficilmente funzionante. Quando si creano microservizi, è vantaggioso riscrivere il legacy esistente, cioè convertirlo in un codice che ci piace e che soddisfa meglio le esigenze del business in termini di funzionalità e velocità.

    Un piccolo avvertimento: i costi per la transizione ai microservizi sono piuttosto significativi. Solo per risolvere il problema dell'infrastruttura ci è voluto molto tempo. Quindi, se hai una piccola applicazione che non richiede scalabilità specifica, e se non ci sono molti clienti che competono per l'attenzione e il tempo del tuo team, allora potrebbe essere che i microservizi non siano ciò di cui hai bisogno oggi. È piuttosto costoso. Se inizi il processo con i microservizi, i costi iniziali saranno superiori rispetto a quelli dell'avvio dello stesso progetto con lo sviluppo di un monolite.

    P.S. Una narrazione più emotiva (e come se fosse personale per te) – su link.
    Ecco 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