Points Clés
- L’architecture microservices ne se réfère pas à la taille des services: la décomposition de votre solution en "micro" parties n'est pas le but du modèle. Pensez à votre solution comme un tout, puis regardez les exigences pour vous guider dans le choix des morceaux à séparer. L'article vous donne un exemple de cette démarche.
- Le concept de découplage des services est au cœur du modèle des microservices ; l'objectif final du modèle est de faciliter le développement, l'exploitation et la maintenance d'une solution distribuée, ce qui est plus facile à réaliser lorsque les services sont découplés.
- Le découplage des services comporte deux aspects 1) Les services n'interagissent pas directement entre eux, mais utilisent un service d'intégration. 2) Les services sont versionnés.
- L'utilisation d'un service d'intégration (par exemple, un bus de messages) libère vos microservices de leur dépendance les uns par rapport aux autres.
- Le versionnement ne doit pas être une réflexion “après coup” dans les microservices: mettez-le en œuvre dès le premier jour. Si le versionnement vous semble excessif, vous êtes probablement en train de découper votre solution en microservices prématurément.
[Mes mots sont des opinions personnelles et ne représentent pas mon entreprise]
L’architecture Microservices est puissante, et sa popularité ne va cesser de croître car elle résout de nombreux problèmes liés aux applications distribuées. Certains développeurs trouvent que les microservices apportent tellement de complexité qu'ils ne cessent de corriger ce que le modèle leur propose plutôt que de bénéficier des avantages qu'il promet. Ayant participé à plusieurs projets microservices, dont certains ont échoué, j'ai pensé qu'il pourrait être utile à la communauté de partager des retours pratiques : les "caractéristiques d’une bonne et d’une mauvaise architecture microservices" (qui était à l’origine le titre de cet article). En reprenant chaque projet et en analysant ses facteurs d’échec ou de succès, j'ai découvert un fait intéressant : les équipes considérant les microservices comme une architecture de "petits services" ont abouti à des solutions complexes et impossibles à maintenir, tandis que les équipes considérant les microservices comme une architecture de "services découplés" ont pu en tirer la quintessence (la compréhension du découpage ("petit" vs "découplé") oblige les développeurs à concevoir des services cohérents avec cette architecture). En analysant ceci, je pense qu’il est important de discuter de la différence fondamentale entre une bonne et une mauvaise implémentation : les "petits services" par rapport aux "services découplés" ou ce que j'appelle ici la "lettre" par rapport à l'"esprit". Au cours de cet article, nous discuterons également de certains concepts importants concernant cette architecture.
La Lettre : les Microservices doivent être petits
Selon Wikipédia, "micro" est un "préfixe qui vient du grec μικρός (mikrós), signifiant "petit"". Ce choix malheureux des mots "micro services" a conduit à une grande confusion au sujet du modèle, car beaucoup de développeurs ont fait du découpage de la solution en "petits" morceaux son objectif final, alors que ce n’est en réalité qu'un moyen d'atteindre les objectifs du modèle.
J'ai observé ce malentendu il y a quelques années lorsque j'ai cloné le dépôt git d'un ancien client et que j'ai découvert que chaque méthode (comme GetName, GetEmail) était définie dans sa propre classe (C#) et que chaque classe était définie dans son propre projet - il y avait littéralement plusieurs dizaines de projets qu'il fallait faire défiler avec la souris dans l'explorateur de solutions (Visual Studio) pour en voir l’ensemble. J'ai appris plus tard qu'ils avaient fait cela en imaginant que c'était la bonne façon de concevoir une architecture microservices.
Cela peut sembler choquant au premier abord, mais cela ne devrait pas vous surprendre alors que vous pouvez lire de nombreuses définitions en ligne qui incluent le mot "petit" dans la définition des microservices. En fin de compte, en comprenant les microservices comme de petits services, nous sommes inévitablement confrontés à la question suivante : "Jusqu'à quel point cela doit-il être petit ?" et vous pourriez être tenté d’éluder cette question en rendant votre solution/code aussi petit que possible et ainsi vous assurer de ne pas passer à côté des avantages des microservices.
Il est vrai qu'il s'agit d'un exemple extrême mais cet antipattern existe dans de nombreux autres projets, même si cela est fait avec des nuances plus subtiles. Soyons donc clairs : le préfixe "micro" n'indique pas ici une "petite taille absolue" ; il se peut à l’origine qu'ils l'aient appelé micro/petit dans le contexte d'un "monolithe", pour signifier simplement "partie" d'un tout (étant donné que toute partie est "plus petite" que le tout qu’elle compose), ou peut-être qu'ils l'ont appelé micro parce que le monolithe est divisé sur la base d'une fonctionnalité spécifique (une fonctionnalité "unique" ne signifie pas nécessairement une fonctionnalité "petite") ou pour toute autre raison que l'on peut imaginer, mais pour moi toutes ces explications ne permettent pas de capter l’essence du modèle. Nous ne pouvons pas changer son nom mais nous pouvons arrêter d'utiliser le mot "petit" pour le définir, nous devons commencer par réconcilier la définition du modèle avec son implémentation pour le rendre plus simple et plus facile à mettre en pratique.
Veuillez noter que le fait de dire que les microservices ne sont pas des petits services ne signifie pas que les microservices devraient être des gros services - tout ce que je dis, c'est que la taille n'est pas pertinente ici et qu'elle reflète le mauvais côté du modèle et fausse ainsi sa mise en œuvre.
Si les microservices ne sont pas de petits services, alors que sont-ils ?
Wikipedia propose une bonne définition des microservices : "technique de développement logiciel qui structure une application comme un ensemble de services faiblement couplés".
L'idée de ce modèle est simple : différentes parties de la solution peuvent avoir des besoins différents. Dans ce cas, nous découplons ces parties pour répondre aux besoins de chacune d'entre elles sans affecter les autres. Au fur et à mesure que nous les découplons, nous normalisons la façon dont elles communiquent avec d'autres et faisons abstraction de leurs implémentations internes, de sorte que ces parties deviennent interopérables et réutilisables.
Prenons un exemple.
La figure A montre une solution monolithique, tous les services (représentés par des engrenages) de cette solution sont étroitement liés. Prenons ce monolithe comme référence pour comprendre pourquoi/quand nous avons besoin de microservices.
Note : J'ai été tenté de donner aux engrenages des formes/couleurs différentes pour indiquer les différents types de services/fonctionnalités mais j'ai décidé de tous les garder identiques parce que je veux souligner qu'un monolithe n'a pas besoin d'être composé de différents types de services pour tirer parti du modèle des microservices. Le monolithe pourrait être entièrement composé d'un seul type de service (par exemple, tous des batchs) et pourtant être partitionné si un ou un sous-ensemble de ces services ont des besoins spécifiques différents du reste des services (par exemple, s’ils ont un objectif de résilience ou d'échelle différent). Il s'agit là d'un aspect subtil mais important à comprendre dans ce modèle.
Revenons à notre exemple. Supposons que l'un des engrenages de la solution subisse une charge importante et que nous ayons besoin de le mettre à l’échelle, choisissons l'un des engrenages de la figure A et appelons-le GearX. GearX fait partie du monolithe, nous devons donc mettre à l'échelle l'ensemble du monolithe pour satisfaire notre exigence de mise à l'échelle de GearX :
Mission accomplie ! La figure B a réussi à mettre à l'échelle GearX et notre solution peut maintenant gérer une charge de travail plus importante. Cependant, nous remarquons que pour satisfaire notre exigence de mise à l'échelle, nous avons introduit un autre problème par inadvertance (un gros problème en fait !) - comme vous pouvez le voir sur la figure B, l'effet secondaire de la mise à l'échelle de GearX dans un monolithe est que tous les engrenages ont également été mis à l'échelle, ce qui représente un gaspillage massif de ressources ! À l'époque des infrastructures «on premise», cela n'aurait pas été un gros problème car nous investissions dans nos propres centres de données et nous n’avions pas de frais supplémentaires lors de la mise à l'échelle des applications. Cependant, avec le Cloud et son modèle de coût à la consommation, nous ne pouvons pas nous permettre de mettre à l'échelle tous ces engrenages et ainsi payer pour leur consommation si nous souhaitions simplement mettre à l'échelle GearX. Bienvenue aux microservices, nous avons maintenant notre premier cas d'utilisation valide pour mettre en œuvre ce modèle.
Pour résoudre ce problème, découplons GearX afin de pouvoir le faire évoluer indépendamment des autres engrenages - figure C :
Nous pouvons maintenant augmenter/diminuer la taille de GearX sans gaspiller de ressources et nous pouvons maintenant satisfaire notre besoin initial de mise à l'échelle tout en maintenant un coût optimal de fonctionnement pour la solution dans son ensemble.
Remarque : Ce sont pour ces raisons de coûts à la consommation de ressources que les microservices sont souvent associés au cloud, même si l'architecture microservices est née bien avant et qu'elle n'est pas la seule architecture valable pour le cloud.
Je parie que vous pouvez comprendre où cela nous mène : si nous pouvons cloisonner un engrenage pour optimiser la mise à l’échelle/le coût, pourquoi ne pas tirer parti de cette même méthodologie pour atteindre d'autres objectifs ? Par exemple, si un autre rouage du monolithe est un générateur de publicité géré par une équipe distincte, opérant dans une autre partie du monde et qui a besoin de déploiements fréquents pour générer des campagnes publicitaires quotidiennement. Au lieu de dépendre des déploiements de notre monolithe, nous pouvons partitionner ce service et donner à l'équipe chargée de la publicité un contrôle total sur celui-ci : elle peut utiliser n'importe quel langage, n'importe quelle base de données ou n'importe quelle fréquence de déploiement, cela nous est égal puisque ce service est maintenant découplé/indépendant.
Au fur et à mesure que nous découplons ces services et que nous normalisons leur mode de communication, nous devons nous efforcer de les rendre «stateless» (dans la mesure du possible) afin qu'ils soient faciles à mettre à l‘échelle et à remplacer sans temps d'arrêt ou presque.
En résumé, ne découpez pas votre solution en petits morceaux dès le départ car cela rendra très probablement votre solution plus difficile à maintenir. Le processus est tout à fait inverse : considérez votre solution comme un tout (monolithe), puis examinez les exigences pour voir si vous pouvez partitionner certaines parties de ce monolithe pour y répondre. S'il y a une vague exigence qui *pourrait* être résolue en partitionnant une partie de la solution, je recommande de ne pas la partitionner prématurément mais de développer votre solution conformément aux bonnes pratiques (SOLID / 12 facteurs ... etc.) afin de pouvoir facilement extraire cette partition à l'avenir si l'exigence devient concrète. Évitez à tout prix la figure E !
L’Esprit : les Microservices doivent être découplés
Revenons à la définition Wikipedia des Microservices : "technique de développement logiciel qui structure une application comme un ensemble de services faiblement couplés". Décortiquons la partie "faiblement couplés" car c'est bien là l'esprit du modèle. Bien qu'il puisse y avoir beaucoup de facteurs qui contribuent à rendre vos services faiblement couplés (un stockage dédié pour chaque service, un répertoire de sources dédié ... etc), je veux m'assurer que nous partions d’une même base et mentionner les deux choses qui me paraissent essentielles (c'est-à-dire que vous ne pouvez pas vous passer de ces deux principes mais que vous pouvez les compléter) :
- Une interaction entre services basée sur des événements
- Un versionnement des services
Principe 1 : Une interaction entre services basée sur des événements
En d'autres termes, si un appel au serviceA déclenche une chaîne d'appels aux serviceB, C et D qui doivent tous aboutir pour que le serviceA renvoie une réponse, vous n’êtes pas en train d’implémenter les microservices de la bonne manière.
Idéalement, les services n'interagissent pas directement les uns avec les autres. Au lieu de cela, ils utilisent un service d'intégration pour communiquer ensemble. C'est ce que l'on fait généralement avec un bus de messages. L’objectif ici est de rendre chaque service indépendant des autres, de sorte que chaque service dispose de tout ce dont il a besoin pour commencer sa tâche et ne se soucie pas de ce qui se passe une fois cette tâche terminée. Dans les cas exceptionnels où un service appelle directement un autre service, il doit gérer correctement les cas d’échec de ce second service.
Voici notre monolithe transformé en une solution microservices typique :
Principe 2 : Un versionnement des services
En d'autres termes, si l'équipe du serviceA a besoin d'une réunion avec l'équipe du serviceB pour pouvoir modifier quelque chose dans le serviceA, vous n’êtes pas en train d’implémenter les microservices de la bonne manière.
Les microservices nous placent devant un défi intéressant : d'une part, les services doivent être découplés mais d'autre part, ils doivent tous être en bonne santé pour que la solution soit performante ; ils doivent donc pouvoir évoluer sans casser la solution. Prenons un exemple simple :
ServiceA est un service qui génère un message json avec un champ username sous forme de String et il place ce message dans une file d'attente. ServiceB est un service qui attend un message json avec un champ username sous forme de String pour rechercher l'utilisateur et effectuer un traitement. Supposons maintenant que le serviceA doive inclure le nom d'utilisateur (username) et son adresse mail sous forme de tableau dans le message json. ServiceB ne pourra pas traiter ce message car il attend un String pour le champ username. L'équipe de ServiceA doit être agile et répondre aux changements, mais en même temps elle doit faire attention à ne pas introduire de bugs comme celui-ci.
Il peut être tentant de résoudre ce problème en coordonnant les équipes qui travaillent sur ces services et en déployant une seule version incluant tous les services (lorsque ServiceA et ServiceB auront terminé leurs modifications), mais vous vous rendrez rapidement compte que cela n'est pas maintenable et que c'est en fait une recette typique pour générer des bugs. Plus important encore, si vous adoptez cette approche, cela vaut la peine de revoir les hypothèses initiales et de vous rappeler l'avantage d’avoir découplé ces deux services - les réintégrer ensemble pourrait être une bonne solution si les services ne peuvent pas être couplés de manière souple.
Votre meilleure chance de résoudre ce problème est de continuer à développer ces services de manière indépendante, mais en les versionnant de façon à ce qu'ils soient informés lorsqu'un changement se produit (par conséquent, maintenez une certaine rétrocompatibilité jusqu'à ce que tous les services cessent d'utiliser les anciennes versions). Dans notre exemple, ServiceA continuera à envoyer un message avec un champ username sous forme de String (version 1) mais enverra désormais aussi une version 2 avec le nom d'utilisateur sous forme de tableau. ServiceB vérifie le numéro de version et traite les messages qu'il peut traiter (version 1) tout en travaillant au développement de son code pour s'adapter à la version 2.
Il existe de multiples façons de faire du versionnement et n'importe quelle convention ferait l'affaire. J'aime la version sémantique à trois chiffres 0.0.0 car elle est largement comprise par la plupart des développeurs et il est facile de dire quel type de changement le service a fait en regardant simplement quel chiffre parmi les trois a été mis à jour. Si vous avez besoin d'une stratégie de versionnement plus complète, vous pouvez également consulter la façon dont Microsoft versionne les paquets NuGet.
Versionner dès le démarrage est-il prématuré pour les microservices ?
C'est une question intéressante à se poser. Je suis sceptique à l'égard de ces choses que l'on ajoute dès le premier jour et il n'y a pas d'exception dans notre cas. Même si je recommande d'ajouter le versionnement dès le démarrage, je ne recommande pas de mettre en œuvre une architecture microservices dès le premier jour à moins que cela ne présente des avantages évidents. Une fois que nous avons décidé du bien-fondé de l’utilisation d’une architecture microservices, nous avons automatiquement accepté l’ensemble des défis inhérents aux systèmes distribués - le fait de partitionner une solution en différents services signifie nécessairement que ces services évolueront différemment ; nous résolvons ce défi en introduisant le versionnement. De plus, il est difficile d'introduire le versionnement plus tard dans la phase de développement : vous vous épargnerez beaucoup d'efforts si vous l'introduisez dès le premier jour.
Conclusion
Définir les microservices comme de "petits services" n'est pas totalement faux, mais cela présente le mauvais aspect du modèle et conduit à une implémentation incorrecte. Le "découplage des services" est l’essence même du modèle. Il existe deux principes essentiels de découplage : 1) les services ne doivent pas communiquer directement entre eux et 2) les services doivent être versionnés afin de pouvoir évoluer indépendamment les uns des autres.
A propos de l’auteur
Alaa Tadmori est un Architecte Solution Cloud chez Microsoft. Il est un adepte de la première heure et un enthousiaste du Cloud. Alaa croit en l'informatique responsable, les solutions que nous construisons aujourd'hui façonnent notre monde et seront le monde de nos enfants. Nous avons donc la responsabilité et le choix, chaque jour, de faire de notre monde un endroit meilleur. Lorsqu'il n'est pas au travail, Alaa aime passer du temps avec sa famille et lire des ouvrages documentaires.