Transition du monolithe aux microservices : histoire et pratique

Dans cet article, je parlerai de la façon dont le projet sur lequel je travaille est passé d'un grand monolithe à un ensemble de microservices.

Le projet a commencé son histoire il y a assez longtemps, au début des années 2000. Les premières versions ont été écrites en Visual Basic 6. Au fil du temps, il est devenu évident que le développement dans ce langage serait difficile à prendre en charge à l'avenir, puisque l'IDE et la langue elle-même est peu développée. À la fin des années 2000, il a été décidé de passer au C#, plus prometteur. La nouvelle version a été écrite parallèlement à la révision de l'ancienne, progressivement de plus en plus de code a été écrit en .NET. Le backend en C# était initialement axé sur une architecture de services, mais lors du développement, des bibliothèques communes avec logique ont été utilisées et les services ont été lancés en un seul processus. Le résultat a été une application que nous avons appelée un « monolithe de services ».

L'un des rares avantages de cette combinaison était la capacité des services à s'appeler via une API externe. Il y avait des conditions préalables claires pour la transition vers un service plus correct et, à l'avenir, vers une architecture de microservices.

Nous avons commencé nos travaux sur la décomposition vers 2015. Nous n'avons pas encore atteint un état idéal - il existe encore des parties d'un grand projet que l'on peut difficilement qualifier de monolithes, mais elles ne ressemblent pas non plus à des microservices. Néanmoins, les progrès sont significatifs.
J'en parlerai dans l'article.

Transition du monolithe aux microservices : histoire et pratique

Teneur

Architecture et problématiques de la solution existante


Initialement, l'architecture ressemblait à ceci : l'interface utilisateur est une application distincte, la partie monolithique est écrite en Visual Basic 6, l'application .NET est un ensemble de services associés fonctionnant avec une base de données assez volumineuse.

Inconvénients de la solution précédente

Point de défaillance unique
Nous avions un seul point de défaillance : l’application .NET s’exécutait en un seul processus. Si un module tombait en panne, l'application entière tombait en panne et devait être redémarrée. Étant donné que nous automatisons un grand nombre de processus pour différents utilisateurs, en raison d'une défaillance de l'un d'entre eux, tout le monde n'a pas pu travailler pendant un certain temps. Et en cas d'erreur logicielle, même la sauvegarde n'a pas aidé.

File d'attente d'améliorations
Cet inconvénient est plutôt organisationnel. Notre application compte de nombreux clients, et ils souhaitent tous l’améliorer au plus vite. Auparavant, il était impossible de faire cela en parallèle et tous les clients faisaient la queue. Ce processus était négatif pour les entreprises car elles devaient prouver que leur tâche était utile. Et l’équipe de développement a passé du temps à organiser cette file d’attente. Cela a demandé beaucoup de temps et d’efforts, et le produit n’a finalement pas pu évoluer aussi rapidement qu’ils l’auraient souhaité.

Utilisation sous-optimale des ressources
Lors de l’hébergement de services en un seul processus, nous copions toujours entièrement la configuration d’un serveur à l’autre. Nous souhaitions placer séparément les services les plus chargés afin de ne pas gaspiller de ressources et d'obtenir un contrôle plus flexible sur notre schéma de déploiement.

Difficile de mettre en œuvre les technologies modernes
Un problème familier à tous les développeurs : il y a une volonté d'introduire des technologies modernes dans le projet, mais il n'y a aucune opportunité. Avec une grande solution monolithique, toute mise à jour de la bibliothèque actuelle, sans parler du passage à une nouvelle, se transforme en une tâche plutôt non triviale. Il faut beaucoup de temps pour prouver au chef d'équipe que cela apportera plus de bonus que de nerfs gaspillés.

Difficulté à émettre des modifications
C'était le problème le plus sérieux : nous publiions des versions tous les deux mois.
Chaque version s'est transformée en un véritable désastre pour la banque, malgré les tests et les efforts des développeurs. L'entreprise a compris qu'en début de semaine certaines de ses fonctionnalités ne fonctionneraient pas. Et les développeurs ont compris qu'une semaine d'incidents graves les attendait.
Tout le monde avait envie de changer la situation.

Attentes des microservices


Sortie des composants une fois prêts. Livraison des composants lorsqu'ils sont prêts en décomposant la solution et en séparant les différents processus.

Petites équipes de produits. Ceci est important car une grande équipe travaillant sur l’ancien monolithe était difficile à gérer. Une telle équipe était obligée de travailler selon un processus strict, mais elle souhaitait plus de créativité et d’indépendance. Seules les petites équipes pouvaient se le permettre.

Isolement des services dans des processus distincts. Idéalement, je voulais l'isoler dans des conteneurs, mais un grand nombre de services écrits dans le .NET Framework ne fonctionnent que sous Windows. Des services basés sur .NET Core font désormais leur apparition, mais ils sont encore peu nombreux.

Flexibilité de déploiement. Nous aimerions combiner les services selon nos besoins, et non selon les exigences du code.

Utilisation des nouvelles technologies. C'est intéressant pour tout programmeur.

Problèmes de transition


Bien sûr, s'il était facile de diviser un monolithe en microservices, il ne serait pas nécessaire d'en parler lors de conférences et d'écrire des articles. Il y a de nombreux écueils dans ce processus, je vais décrire les principaux qui nous ont freinés.

Premier problème typique de la plupart des monolithes : cohérence de la logique métier. Lorsque nous écrivons un monolithe, nous souhaitons réutiliser nos classes pour ne pas écrire de code inutile. Et lors du passage aux microservices, cela devient un problème : tout le code est assez étroitement couplé et il est difficile de séparer les services.

Au moment du début des travaux, le référentiel comptait plus de 500 projets et plus de 700 XNUMX lignes de code. C'est une décision assez importante et deuxième problème. Il n’était pas possible de simplement le prendre et de le diviser en microservices.

Troisième problème — le manque d'infrastructures nécessaires. En fait, nous copiions manuellement le code source sur les serveurs.

Comment passer du monolithe aux microservices


Mise à disposition de microservices

Premièrement, nous avons immédiatement déterminé que la séparation des microservices est un processus itératif. Nous avons toujours été amenés à développer en parallèle des problèmes commerciaux. La manière dont nous allons mettre en œuvre cela techniquement est déjà notre problème. Nous nous sommes donc préparés à un processus itératif. Cela ne fonctionnera pas autrement si vous avez une application volumineuse et qu’elle n’est pas initialement prête à être réécrite.

Quelles méthodes utilisons-nous pour isoler les microservices ?

La première méthode — déplacer les modules existants en tant que services. À cet égard, nous avons eu de la chance : il existait déjà des services enregistrés qui fonctionnaient avec le protocole WCF. Ils ont été séparés en assemblées distinctes. Nous les avons portés séparément, en ajoutant un petit lanceur à chaque version. Il a été écrit à l'aide de la merveilleuse bibliothèque Topshelf, qui vous permet d'exécuter l'application à la fois en tant que service et en tant que console. Ceci est pratique pour le débogage puisqu’aucun projet supplémentaire n’est requis dans la solution.

Les services étaient connectés selon une logique métier, puisqu'ils utilisaient des assemblys communs et travaillaient avec une base de données commune. On peut difficilement les qualifier de microservices dans leur forme pure. Cependant, nous pourrions fournir ces services séparément, selon des processus différents. Cela seul a permis de réduire leur influence les uns sur les autres, réduisant ainsi le problème du développement parallèle et du point de défaillance unique.

L'assemblage avec l'hôte n'est qu'une ligne de code dans la classe Program. Nous avons caché le travail avec Topshelf dans une classe auxiliaire.

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

       }
    }
}

La deuxième façon d’attribuer des microservices est la suivante : créez-les pour résoudre de nouveaux problèmes. Si en même temps le monolithe ne grandit pas, c’est déjà excellent, ce qui signifie que nous allons dans la bonne direction. Pour résoudre de nouveaux problèmes, nous avons essayé de créer des services distincts. Si une telle opportunité existait, nous créions alors des services plus « canoniques » qui gèrent entièrement leur propre modèle de données, une base de données distincte.

Comme beaucoup, nous avons commencé avec les services d’authentification et d’autorisation. Ils sont parfaits pour cela. Ils sont indépendants et disposent en règle générale d’un modèle de données distinct. Eux-mêmes n'interagissent pas avec le monolithe, mais celui-ci se tourne vers eux pour résoudre certains problèmes. En utilisant ces services, vous pouvez commencer la transition vers une nouvelle architecture, déboguer l'infrastructure sur celles-ci, essayer certaines approches liées aux bibliothèques réseau, etc. Nous n’avons aucune équipe dans notre organisation qui ne pourrait pas créer un service d’authentification.

La troisième façon d'attribuer des microservicesCelui que nous utilisons nous est un peu spécifique. Il s'agit de la suppression de la logique métier de la couche d'interface utilisateur. Notre principale application d'interface utilisateur est celle de bureau ; elle, comme le backend, est écrite en C#. Les développeurs commettaient périodiquement des erreurs et transféraient vers l'interface utilisateur des parties de logique qui auraient dû exister dans le backend et être réutilisées.

Si vous regardez un exemple réel du code de la partie UI, vous pouvez voir que la plupart de cette solution contient une véritable logique métier qui est utile dans d'autres processus, pas seulement pour créer le formulaire d'interface utilisateur.

Transition du monolithe aux microservices : histoire et pratique

La véritable logique de l'interface utilisateur n'est présente que dans les deux dernières lignes. Nous l'avons transféré sur le serveur afin qu'il puisse être réutilisé, réduisant ainsi l'interface utilisateur et obtenant la bonne architecture.

La quatrième et la plus importante façon d'isoler les microservices, qui permet de réduire le monolithe, est la suppression des services existants avec traitement. Lorsque nous supprimons des modules existants tels quels, le résultat n’est pas toujours du goût des développeurs et le processus métier peut être devenu obsolète depuis la création de la fonctionnalité. Avec la refactorisation, nous pouvons prendre en charge un nouveau processus métier car les exigences métier évoluent constamment. Nous pouvons améliorer le code source, supprimer les défauts connus et créer un meilleur modèle de données. De nombreux avantages en découlent.

La séparation des services du traitement est inextricablement liée au concept de contexte délimité. Il s'agit d'un concept de Domain Driven Design. Cela signifie une section du modèle de domaine dans laquelle tous les termes d'une seule langue sont définis de manière unique. Prenons l'exemple du contexte de l'assurance et des factures. Nous avons une application monolithique et nous devons travailler avec le compte en assurance. Nous nous attendons à ce que le développeur trouve une classe Account existante dans un autre assembly, la référence à partir de la classe Insurance et nous aurons un code fonctionnel. Le principe DRY sera respecté, la tâche sera effectuée plus rapidement en utilisant le code existant.

Il s’avère ainsi que les contextes de comptabilité et d’assurance sont liés. À mesure que de nouvelles exigences émergent, ce couplage interférera avec le développement, augmentant ainsi la complexité d'une logique métier déjà complexe. Pour résoudre ce problème, vous devez trouver les limites entre les contextes dans le code et supprimer leurs violations. Par exemple, dans le cadre de l’assurance, il est fort possible qu’un numéro de compte Banque Centrale à 20 chiffres et la date d’ouverture du compte soient suffisants.

Pour séparer ces contextes délimités les uns des autres et commencer le processus de séparation des microservices d'une solution monolithique, nous avons utilisé une approche telle que la création d'API externes au sein de l'application. Si nous savions qu'un module devait devenir un microservice, modifié d'une manière ou d'une autre au cours du processus, alors nous appelions immédiatement la logique appartenant à un autre contexte limité via des appels externes. Par exemple, via REST ou WCF.

Nous avons fermement décidé que nous n'éviterions pas le code qui nécessiterait des transactions distribuées. Dans notre cas, il s’est avéré assez simple de suivre cette règle. Nous n'avons pas encore rencontré de situations où des transactions distribuées strictes sont vraiment nécessaires - la cohérence finale entre les modules est tout à fait suffisante.

Regardons un exemple spécifique. Nous avons le concept d'orchestrateur - un pipeline qui traite l'entité de « l'application ». Il crée tour à tour un client, un compte et une carte bancaire. Si le client et le compte sont créés avec succès, mais que la création de la carte échoue, l'application ne passe pas au statut « réussie » et reste dans le statut « carte non créée ». À l’avenir, l’activité en arrière-plan le récupérera et le terminera. Le système est dans un état d'incohérence depuis un certain temps, mais nous en sommes globalement satisfaits.

Si une situation se présente lorsqu'il est nécessaire de sauvegarder systématiquement une partie des données, nous opterons très probablement pour une consolidation du service afin de le traiter en un seul processus.

Regardons un exemple d'allocation d'un microservice. Comment pouvez-vous le mettre en production de manière relativement sûre ? Dans cet exemple, nous avons une partie distincte du système - un module de service de paie, dont nous aimerions créer l'une des sections de code en microservice.

Transition du monolithe aux microservices : histoire et pratique

Tout d'abord, nous créons un microservice en réécrivant le code. Nous améliorons certains aspects qui ne nous satisfaisaient pas. Nous mettons en œuvre les nouvelles exigences commerciales du client. Nous ajoutons une passerelle API à la connexion entre l'interface utilisateur et le backend, qui assurera le transfert d'appel.

Transition du monolithe aux microservices : histoire et pratique

Ensuite, nous mettons cette configuration en service, mais dans un état pilote. La plupart de nos utilisateurs travaillent encore avec d'anciens processus métier. Pour les nouveaux utilisateurs, nous développons une nouvelle version de l'application monolithique qui ne contient plus ce processus. Essentiellement, nous avons une combinaison d'un monolithe et d'un microservice fonctionnant comme pilote.

Transition du monolithe aux microservices : histoire et pratique

Avec un pilote réussi, nous comprenons que la nouvelle configuration est effectivement réalisable, nous pouvons supprimer l'ancien monolithe de l'équation et laisser la nouvelle configuration à la place de l'ancienne solution.

Transition du monolithe aux microservices : histoire et pratique

Au total, nous utilisons presque toutes les méthodes existantes pour diviser le code source d'un monolithe. Tous nous permettent de réduire la taille des parties de l'application et de les traduire dans de nouvelles bibliothèques, améliorant ainsi le code source.

Travailler avec la base de données


La base de données peut être pire que le code source, car elle contient non seulement le schéma actuel, mais également les données historiques accumulées.

Notre base de données, comme beaucoup d'autres, présentait un autre inconvénient important : sa taille énorme. Cette base de données a été conçue selon la logique métier complexe d'un monolithe et les relations accumulées entre les tables de divers contextes délimités.

Dans notre cas, pour couronner tous les problèmes (base de données volumineuse, nombreuses connexions, limites parfois floues entre les tables), un problème est apparu, qui se produit dans de nombreux grands projets : l'utilisation du modèle de base de données partagée. Les données étaient extraites des tables via vue, via réplication, et expédiées vers d'autres systèmes où cette réplication était nécessaire. Par conséquent, nous ne pouvions pas déplacer les tables dans un schéma distinct car elles étaient activement utilisées.

La même division en contextes limités dans le code nous aide dans la séparation. Cela nous donne généralement une assez bonne idée de la façon dont nous décomposons les données au niveau de la base de données. Nous comprenons quelles tables appartiennent à un contexte délimité et lesquelles à un autre.

Nous avons utilisé deux méthodes globales de partitionnement de bases de données : le partitionnement des tables existantes et le partitionnement avec traitement.

La séparation des tables existantes est une bonne méthode à utiliser si la structure des données est bonne, répond aux exigences de l'entreprise et que tout le monde en est satisfait. Dans ce cas, nous pouvons séparer les tables existantes dans un schéma distinct.

Un département avec transformation est nécessaire lorsque le modèle économique a beaucoup changé et que les tableaux ne nous satisfont plus du tout.

Fractionner les tables existantes. Nous devons déterminer ce que nous allons séparer. Sans cette connaissance, rien ne fonctionnera, et ici la séparation des contextes délimités dans le code nous aidera. En règle générale, si vous pouvez comprendre les limites des contextes dans le code source, il devient clair quelles tables doivent être incluses dans la liste du département.

Imaginons que nous ayons une solution dans laquelle deux modules monolithiques interagissent avec une seule base de données. Nous devons nous assurer qu'un seul module interagit avec la section de tables séparées et que l'autre commence à interagir avec elle via l'API. Pour commencer, il suffit que seul l'enregistrement soit effectué via l'API. C’est une condition nécessaire pour parler d’indépendance des microservices. Les connexions de lecture peuvent persister tant qu'il n'y a pas de problème majeur.

Transition du monolithe aux microservices : histoire et pratique

L'étape suivante consiste à séparer la section de code qui fonctionne avec des tables séparées, avec ou sans traitement, en un microservice distinct et à l'exécuter dans un processus distinct, un conteneur. Il s'agira d'un service distinct avec une connexion à la base de données monolithique et aux tables qui ne s'y rapportent pas directement. Le monolithe interagit toujours pour la lecture avec la partie détachable.

Transition du monolithe aux microservices : histoire et pratique

Plus tard, nous supprimerons cette connexion, c'est-à-dire que la lecture des données d'une application monolithique à partir de tables séparées sera également transférée vers l'API.

Transition du monolithe aux microservices : histoire et pratique

Ensuite, nous sélectionnerons dans la base de données générale les tables avec lesquelles seul le nouveau microservice fonctionne. Nous pouvons déplacer les tables vers un schéma distinct ou même vers une base de données physique distincte. Il existe toujours une connexion de lecture entre le microservice et la base de données monolithique, mais il n'y a pas de quoi s'inquiéter, dans cette configuration, elle peut vivre assez longtemps.

Transition du monolithe aux microservices : histoire et pratique

La dernière étape consiste à supprimer complètement toutes les connexions. Dans ce cas, nous devrons peut-être migrer les données de la base de données principale. Parfois, nous souhaitons réutiliser certaines données ou répertoires répliqués à partir de systèmes externes dans plusieurs bases de données. Cela nous arrive périodiquement.

Transition du monolithe aux microservices : histoire et pratique

Département de traitement. Cette méthode est très similaire à la première, mais dans l’ordre inverse. Nous allouons immédiatement une nouvelle base de données et un nouveau microservice qui interagit avec le monolithe via une API. Mais en même temps, il reste un ensemble de tables de base de données que nous souhaitons supprimer à l'avenir. Nous n'en avons plus besoin, nous l'avons remplacé dans le nouveau modèle.

Transition du monolithe aux microservices : histoire et pratique

Pour que ce projet fonctionne, nous aurons probablement besoin d’une période de transition.

Il y a alors deux approches possibles.

Premier: nous dupliquons toutes les données dans les nouvelles et anciennes bases de données. Dans ce cas, nous avons une redondance des données et des problèmes de synchronisation peuvent survenir. Mais nous pouvons prendre deux clients différents. L’un fonctionnera avec la nouvelle version, l’autre avec l’ancienne.

Deuxième: nous divisons les données selon certains critères métiers. Par exemple, nous avions 5 produits dans le système qui étaient stockés dans l'ancienne base de données. Nous plaçons la sixième au sein de la nouvelle tâche métier dans une nouvelle base de données. Mais nous aurons besoin d'une passerelle API qui synchronisera ces données et montrera au client où et quoi obtenir.

Les deux approches fonctionnent, choisissez en fonction de la situation.

Une fois que nous sommes sûrs que tout fonctionne, la partie du monolithe qui fonctionne avec les anciennes structures de bases de données peut être désactivée.

Transition du monolithe aux microservices : histoire et pratique

La dernière étape consiste à supprimer les anciennes structures de données.

Transition du monolithe aux microservices : histoire et pratique

Pour résumer, on peut dire que nous avons des problèmes avec la base de données : il est difficile de travailler avec elle par rapport au code source, elle est plus difficile à partager, mais cela peut et doit être fait. Nous avons trouvé des moyens qui nous permettent de le faire en toute sécurité, mais il est toujours plus facile de se tromper avec les données qu'avec le code source.

Travailler avec le code source


Voici à quoi ressemblait le diagramme du code source lorsque nous avons commencé à analyser le projet monolithique.

Transition du monolithe aux microservices : histoire et pratique

Il peut être grossièrement divisé en trois couches. Il s'agit d'une couche de modules, plugins, services et activités individuelles lancés. En fait, il s’agissait de points d’entrée au sein d’une solution monolithique. Tous étaient étroitement scellés avec une couche commune. Il y avait une logique métier selon laquelle les services étaient partagés et de nombreuses connexions. Chaque service et plugin utilisait jusqu'à 10 assemblys communs ou plus, en fonction de leur taille et de la conscience des développeurs.

Nous avons eu la chance de disposer de bibliothèques d'infrastructure pouvant être utilisées séparément.

Parfois, une situation survenait lorsque certains objets communs n'appartenaient pas réellement à cette couche, mais étaient des bibliothèques d'infrastructure. Cela a été résolu en renommant.

La plus grande préoccupation concernait les contextes limités. Il est arrivé que 3 à 4 contextes soient mélangés dans un assemblage commun et s'utilisent les uns les autres au sein des mêmes fonctions métiers. Il était nécessaire de comprendre où cela pouvait être divisé et selon quelles limites, et que faire ensuite pour mapper cette division en assemblages de code source.

Nous avons formulé plusieurs règles pour le processus de fractionnement du code.

première: Nous ne souhaitions plus partager la logique métier entre services, activités et plugins. Nous voulions rendre la logique métier indépendante au sein des microservices. Les microservices, en revanche, sont idéalement considérés comme des services qui existent de manière totalement indépendante. Je pense que cette approche est quelque peu inutile et difficile à réaliser, car, par exemple, les services en C# seront de toute façon connectés par une bibliothèque standard. Notre système est écrit en C# ; nous n’avons pas encore utilisé d’autres technologies. Nous avons donc décidé que nous pouvions nous permettre d'utiliser des assemblages techniques communs. L'essentiel est qu'ils ne contiennent aucun fragment de logique métier. Si vous disposez d'un wrapper pratique sur l'ORM que vous utilisez, sa copie d'un service à l'autre coûte très cher.

Notre équipe est fan de la conception axée sur le domaine, l'architecture en oignon nous convenait donc parfaitement. La base de nos services n'est pas la couche d'accès aux données, mais un assemblage avec une logique de domaine, qui contient uniquement une logique métier et n'a aucune connexion avec l'infrastructure. Dans le même temps, nous pouvons modifier indépendamment l'assembly de domaine pour résoudre les problèmes liés aux frameworks.

A ce stade, nous avons rencontré notre premier problème sérieux. Le service devait faire référence à un seul assemblage de domaine, nous voulions rendre la logique indépendante, et le principe DRY nous a beaucoup gêné ici. Les développeurs voulaient réutiliser les classes des assemblys voisins pour éviter la duplication et, par conséquent, les domaines ont recommencé à être liés entre eux. Nous avons analysé les résultats et décidé que le problème réside peut-être également dans le domaine du périphérique de stockage du code source. Nous avions un grand référentiel contenant tout le code source. La solution pour l’ensemble du projet était très difficile à assembler sur une machine locale. Par conséquent, de petites solutions distinctes ont été créées pour certaines parties du projet, et personne n'a interdit d'y ajouter un assemblage commun ou de domaine et de les réutiliser. Le seul outil qui ne nous permettait pas de faire cela était la révision du code. Mais parfois, cela échouait aussi.

Ensuite, nous avons commencé à passer à un modèle avec des référentiels séparés. La logique métier ne circule plus de service en service, les domaines sont véritablement devenus indépendants. Les contextes délimités sont pris en charge plus clairement. Comment réutiliser les bibliothèques d’infrastructure ? Nous les avons séparés dans un référentiel séparé, puis les avons placés dans des packages Nuget, que nous avons mis dans Artifactory. Avec tout changement, l'assemblage et la publication se font automatiquement.

Transition du monolithe aux microservices : histoire et pratique

Nos services ont commencé à référencer les packages d'infrastructure internes de la même manière que les packages externes. Nous téléchargeons des bibliothèques externes depuis Nuget. Pour travailler avec Artifactory, où nous avons placé ces packages, nous avons utilisé deux gestionnaires de packages. Dans les petits référentiels, nous avons également utilisé Nuget. Dans les référentiels comportant plusieurs services, nous avons utilisé Paket, qui offre plus de cohérence de version entre les modules.

Transition du monolithe aux microservices : histoire et pratique

Ainsi, en travaillant sur le code source, en modifiant légèrement l'architecture et en séparant les référentiels, nous rendons nos services plus indépendants.

Problèmes d'infrastructures


La plupart des inconvénients du passage aux microservices sont liés à l’infrastructure. Vous aurez besoin d'un déploiement automatisé, vous aurez besoin de nouvelles bibliothèques pour exécuter l'infrastructure.

Installation manuelle dans les environnements

Dans un premier temps, nous avons installé manuellement la solution pour les environnements. Pour automatiser ce processus, nous avons créé un pipeline CI/CD. Nous avons choisi le processus de livraison continue car le déploiement continu n'est pas encore acceptable pour nous du point de vue des processus métiers. Par conséquent, l'envoi pour fonctionnement s'effectue à l'aide d'un bouton et pour le test - automatiquement.

Transition du monolithe aux microservices : histoire et pratique

Nous utilisons Atlassian, Bitbucket pour le stockage du code source et Bamboo pour la construction. Nous aimons écrire des scripts de build dans Cake car c'est la même chose que C#. Les packages prêts à l'emploi arrivent sur Artifactory et Ansible parvient automatiquement aux serveurs de test, après quoi ils peuvent être testés immédiatement.

Transition du monolithe aux microservices : histoire et pratique

Journalisation séparée


À une certaine époque, l’une des idées du monolithe était de permettre une exploitation forestière partagée. Nous devions également comprendre quoi faire avec les journaux individuels présents sur les disques. Nos journaux sont écrits dans des fichiers texte. Nous avons décidé d'utiliser une pile ELK standard. Nous n'avons pas écrit à ELK directement via les fournisseurs, mais avons décidé de finaliser les journaux de texte et d'y écrire l'ID de trace comme identifiant, en ajoutant le nom du service, afin que ces journaux puissent être analysés ultérieurement.

Transition du monolithe aux microservices : histoire et pratique

En utilisant Filebeat, nous avons la possibilité de collecter nos journaux sur les serveurs, puis de les transformer, d'utiliser Kibana pour créer des requêtes dans l'interface utilisateur et de voir comment l'appel s'est déroulé entre les services. Trace ID aide beaucoup à cela.

Services liés aux tests et au débogage


Au départ, nous ne comprenions pas parfaitement comment déboguer les services en cours de développement. Tout était simple avec le monolithe, nous l'avons exécuté sur une machine locale. Au début, ils ont essayé de faire la même chose avec les microservices, mais parfois, pour lancer complètement un microservice, vous devez en lancer plusieurs autres, ce qui n'est pas pratique. Nous avons réalisé que nous devions passer à un modèle dans lequel nous laissons sur la machine locale uniquement le ou les services que nous souhaitons déboguer. Les services restants sont utilisés à partir de serveurs correspondant à la configuration avec prod. Après le débogage, lors des tests, pour chaque tâche, seuls les services modifiés sont émis vers le serveur de test. Ainsi, la solution est testée sous la forme sous laquelle elle apparaîtra en production dans le futur.

Il existe des serveurs qui exécutent uniquement des versions de production des services. Ces serveurs sont nécessaires en cas d'incidents, pour vérifier la livraison avant le déploiement et pour la formation interne.

Nous avons ajouté un processus de test automatisé utilisant la populaire bibliothèque Specflow. Les tests s'exécutent automatiquement à l'aide de NUnit immédiatement après le déploiement depuis Ansible. Si la couverture des tâches est entièrement automatique, aucun test manuel n’est nécessaire. Bien que parfois des tests manuels supplémentaires soient encore nécessaires. Nous utilisons des balises dans Jira pour déterminer les tests à exécuter pour un problème spécifique.

De plus, le besoin de tests de charge s'est accru ; auparavant, ils n'étaient effectués que dans de rares cas. Nous utilisons JMeter pour exécuter des tests, InfluxDB pour les stocker et Grafana pour créer des graphiques de processus.

Qu’avons-nous réalisé ?


Premièrement, nous nous sommes débarrassés de la notion de « libération ». Fini les versions monstrueuses de deux mois où ce colosse était déployé dans un environnement de production, perturbant temporairement les processus métiers. Désormais, nous déployons des services en moyenne tous les 1,5 jours, en les regroupant car ils entrent en service après approbation.

Il n’y a aucune défaillance fatale dans notre système. Si nous publions un microservice avec un bug, alors la fonctionnalité qui lui est associée sera interrompue et toutes les autres fonctionnalités ne seront pas affectées. Cela améliore considérablement l’expérience utilisateur.

Nous pouvons contrôler le modèle de déploiement. Vous pouvez sélectionner des groupes de services séparément du reste de la solution, si nécessaire.

De plus, nous avons considérablement réduit le problème grâce à une longue file d’améliorations. Nous disposons désormais d’équipes produit distinctes qui travaillent indépendamment avec certains services. Le processus Scrum convient déjà bien ici. Une équipe spécifique peut avoir un Product Owner distinct qui lui attribue des tâches.

Résumé

  • Les microservices sont bien adaptés à la décomposition de systèmes complexes. Ce faisant, nous commençons à comprendre ce qu’il y a dans notre système, quels sont les contextes limités et où se situent leurs limites. Cela vous permet de répartir correctement les améliorations entre les modules et d'éviter toute confusion dans le code.
  • Les microservices offrent des avantages organisationnels. On en parle souvent uniquement en tant qu'architecture, mais toute architecture est nécessaire pour répondre aux besoins de l'entreprise, et non seule. Par conséquent, nous pouvons dire que les microservices sont bien adaptés pour résoudre des problèmes dans de petites équipes, étant donné que Scrum est désormais très populaire.
  • La séparation est un processus itératif. Vous ne pouvez pas prendre une application et la diviser simplement en microservices. Il est peu probable que le produit résultant soit fonctionnel. Lors de la dédiation de microservices, il est avantageux de réécrire l'héritage existant, c'est-à-dire de le transformer en code que nous aimons et qui répond mieux aux besoins de l'entreprise en termes de fonctionnalités et de rapidité.

    Une petite mise en garde : Les coûts du passage aux microservices sont assez importants. Il a fallu beaucoup de temps pour résoudre seul le problème des infrastructures. Ainsi, si vous disposez d'une petite application qui ne nécessite pas de mise à l'échelle spécifique, à moins qu'un grand nombre de clients se disputent l'attention et le temps de votre équipe, les microservices ne sont peut-être pas ce dont vous avez besoin aujourd'hui. C'est assez cher. Si vous démarrez le processus avec des microservices, les coûts seront initialement plus élevés que si vous démarrez le même projet avec le développement d'un monolithe.

    PS Une histoire plus émouvante (et comme pour vous personnellement) - selon lien.
    Voici la version complète du rapport.

Source: habr.com

Ajouter un commentaire