Architecture microservices de A à Z
Cet article a pour objectif d’aborder l’architecture microservices de sa définition à un exemple concret.
Plan :
- Qu’est ce qu’une architecture microservices
- Avantages et inconvénients d’une architecture microservices
- Orchestration vs Chorégraphie
- Conseils et bonnes pratiques
- Preuve de concept
Les résultats suivants ont été réalisés via des sources diverses : publications scientifiques, articles, vidéos, documentations de grandes entreprises, etc. On peut citer notamment le site internet de Martin Fowler, ingénieur logiciel et auteur de renom, qui contient une riche documentation sur les microservices. La bibliographie complète se trouve ici: Bibliographie.
1. Qu’est ce qu’une architecture microservices ?
Essentiellement, l’architecture microservices est une méthode de développement logiciel qui vise à décomposer une application pour en isoler les fonctions clés, chacune de ces fonctions est appelée « service ». Ces services sont créés pour répondre à un besoin métier précis et unique, par exemple : la gestion des utilisateurs, les paiements, l’envoi d’e-mails ou encore les notifications. De plus, ils sont indépendants et modulables, cela permet que chacun soit développé et déployé sans affecter les autres. Ce type d’architecture se veut à l’opposé des architectures monolithes qui sont construites comme une seule unité autonome.
Cette définition peut rappeler un autre type d’architecture assez similaire, l’architecture Orientée Services (SOA), une conception logicielle déjà bien établie.
Qu’est-ce qui différencie une architecture SOA d’une architecture microservices ?
Commençons déjà par un constat clair, émis par les précurseurs des microservices : cette architecture est une extension du concept de SOA. La plupart des principes de conception des microservices était déjà disponible dans le monde de la SOA. Certaines personnes affirment que « l’architecture des microservices est une architecture SOA bien conçue ». Voyons les différences entre SOA et microservices selon plusieurs axes :
- La taille : Une des grosses différences est la taille et la portée des services. Comme on peut le deviner avec le nom, en microservices, la taille de chaque service est beaucoup plus faible que celle des services en SOA. Dans une architecture microservices, chaque service a une seule responsabilité tandis qu’avec l’architecture SOA, les services peuvent contenir de nombreuses fonctions métiers.
- La réutilisation : Un des objectifs de la SOA est la réutilisation des composants sans se soucier du couplage alors qu’en microservices on essaye de minimiser la réutilisation puisque cela crée des dépendances qui réduisent l’agilité et la résilience. On privilégie un couplage faible quitte à avoir de la duplication de code.
- La communication : En SOA, la communication entre les services se fait de manière synchrone généralement à travers un enterprise service bus (ESB), cela introduit un point de défaillance critique et de la latence. Dans une application microservices, la tolérance aux pannes est meilleure grâce à l’indépendance de chaque service, la communication asynchrone est également privilégiée entre les services (par exemple un modèle publish subscribe peut être utilisé pour permettre à un service de rester à jour sur les modifications apportées aux données d’un autre), permettant une haute disponibilité.
- La duplication des données : Un des objectifs d’une SOA est de permettre à toutes les applications d’avoir accès aux données de manière synchrone directement à la source. Avec une architecture microservices, chaque service a accès à toutes les données dont il a besoin localement (idéalement) pour avoir des performances et une agilité maximale, même si cela implique de devoir dupliquer des données et donc de rajouter de la complexité.
Résumons tout cela avec un schéma :
2. Avantages et inconvénients d’une architecture microservices
Maintenant que l’on comprend mieux ce qu’est une architecture microservices, voyons quels sont les avantages et inconvénients de celle-ci par rapport à une architecture monolithe. On a déjà vu un certain nombre d’avantages précédemment, mais repassons les tous en revue :
- Développement indépendant : Les équipes peuvent choisir les technologies qui conviennent le mieux à chaque service à l’aide de diverses piles technologiques et ne sont pas limitées par les choix faits au début du projet. De plus, une seule équipe de développement peut créer, tester et déployer un service, ce qui permet un déploiement plus rapide et facilite l’innovation en continu. Il est également plus difficile de commettre des erreurs puisqu’il y a des frontières fortes entre les différents services.
- Déploiement indépendant : Les microservices sont déployés de manière indépendante. Un service peut être mis à jour sans avoir à redéployer l’application entière, ce qui facilite la gestion des correctifs de bugs et des nouvelles fonctionnalités. Cela simplifie l’intégration et déploiement continu. Dans de nombreuses applications traditionnelles, s’il existe un bug dans une partie de l’application, il peut bloquer tout le processus de mise en production.
- Scaling indépendant : Les services peuvent scaler de façon indépendante pour s’adapter aux besoins, on optimise alors les coûts et le temps puisqu’il n’est pas nécessaire de scaler l’application entière comme ce serait le cas avec un monolithe.
- Petites équipes ciblées : Les équipes peuvent être ciblées uniquement sur un seul service, ce qui rend le code plus simple à comprendre et facilite l’arrivée de nouveaux membres dans l’équipe, plus besoin de passer des semaines à comprendre le fonctionnement d’un monolithe complexe. Cela favorise également la souplesse, les grandes équipes ont tendance à être moins productives, car la communication est plus lente, le temps de gestion augmente et l’agilité diminue.
- Base de code de petite taille : Dans une application monolithique, les dépendances de code ont tendance à s’entremêler au fil du temps. L’ajout d’une nouvelle fonctionnalité nécessite de modifier le code à beaucoup d’endroits. Avec une architecture microservices, qui ne partage pas de code, ni de base de données, on minimise ces dépendances, ce qui facilite l’ajout de nouvelles fonctionnalités. Cela complète également le point précédent sur le fait que cela facilite la compréhension du code et l’introduction de nouveaux membres dans l’équipe.
- Isolation des données : Dans une architecture microservices, chaque service à un accès privé à sa base de données (idéalement), on peut alors effectuer une mise à jour du schéma de base de données sans affecter les autres services. Dans une application monolithique, les mises à jour de schéma peuvent devenir très difficiles et risquées puisque plusieurs parties de l’application peuvent utiliser les mêmes données.
- Résilience : Avec une architecture microservices on diminue considérablement les points de défaillances critiques. Lorsqu’un service tombe en panne, l’ensemble de l’application ne cesse pas de fonctionner comme c’est le cas avec le modèle monolithique et donc le risque est également diminué quand de nouvelles fonctionnalités sont développées. Les erreurs sont également isolées et alors plus simples à corriger.
- Avancées technologiques : Les avancées récentes des technologies cloud et de la conteneurisation rendent la mise en place d’une architecture microservices de plus en plus simple. Chaque fournisseur cloud dispose de solutions pour ce type d’architecture pour simplifier la vie des développeurs.
Maintenant que nous avons une meilleure vision de tous les avantages qu’apporte une architecture microservices, voyons quels sont les inconvénients et défis de celle-ci :
- Complexité : Si chaque service est plus simple, le système dans son ensemble est plus complexe. Étant donné qu’il s’agit d’un système distribué, il faut choisir et mettre en place avec précaution tous les services et les bases de données puis déployer chacun de ces composants de façon indépendante. Tous les challenges d’un système distribué doivent être pris en compte.
- Tests : Avoir une multitude de services indépendants peut rendre l’écriture des tests plus complexe, particulièrement quand il y a de nombreuses dépendances entre les services. Un mock doit être utilisé pour chaque service dépendant pour pouvoir tester de manière unitaire un service.
- Intégrité des données : Les microservices ont une architecture de base de données distribuée, ce qui est un challenge pour l’intégrité des données. Certaines transactions métiers, qui nécessitent de mettre à jour plusieurs fonctions métiers de l’application, ont besoin de mettre à jour plusieurs bases de données détenues par différents services. Cela oblige à mettre en place une cohérence éventuelle des données, ce qui est bien évidemment plus complexe et moins intuitif pour les développeurs.
- Latence du réseau : L’utilisation de nombreux services de petite taille peut se traduire par une intensification des communications entre les services. De plus, si l’on a une chaîne de dépendance entre les services pour exécuter une transaction métier, la latence supplémentaire qui en découle peut devenir un problème. Il faut favoriser les communications asynchrones quand l’usage le permet, mais cela rajoute encore une fois de la complexité au système.
En vue de ces avantages et inconvénients il est alors plus ou moins pertinent d’adopter une architecture microservices, il n’existe pas de règle universelle, chaque projet est unique et les motivations envers une architecture ou l’autre dépendent des besoins et du contexte de celui-ci.
3. Orchestration et Chorégraphie
Dans une architecture microservices, il est inévitable que certains services aient besoin de communiquer entre eux, ces communications peuvent être réalisées de deux façons, l’orchestration ou la chorégraphie. De nombreuses documentations existent sur ces deux patterns avec notamment une publication de Chaitanya Rudrabhatla dans l’IJACSA (International Journal of Advanced Computer Science and Applications), directeur exécutif et architecte chez Sony Pictures Entertainment.
Orchestration :
La première idée intuitive pour faire communiquer les services entre eux est tout simplement de faire des appels REST entre les services :
Cette approche est simple à mettre en place mais le système devient rapidement complexe à maintenir puisque l’on introduit des dépendances entre les services, ce que l’on souhaite éviter au maximum. Une façon plus réfléchie d’effectuer cette transaction est d’introduire une abstraction supplémentaire avec nouveau service, un orchestrateur :
L’orchestrateur est le seul à avoir connaissance des autres services, en cas de mise à jour des services il faudra tout de même être prudent de ne pas « casser » l’orchestrateur, mais on a l’avantage qu’il soit le seul impacté par les modifications. Cependant, avec cette approche ou la précédente, il y a toujours des inconvénients qui sont la latence que cela peut introduire du fait des appels synchrones, et une mauvaise tolérance aux pannes. De plus sur un système composé d’une grande quantité de services, on peut rapidement arriver à un système avec beaucoup de dépendances qui s’apparente à un monolithe distribué. L’orchestration est donc à utiliser avec parcimonie.
Chorégraphie :
La chorégraphie permet de pallier les différents inconvénients de l’orchestration, il n’y a aucune dépendance ni latence dans le système. La solution est d’utiliser des événements avec un modèle publish-subscribe :
Lorsqu’une action est effectuée, le service en question va publier un événement auquel les autres services vont pouvoir souscrire pour effectuer les modifications nécessaires. La communication est asynchrone et les services n’ont pas connaissance des autres, par conséquent le système est performant et reste simple à maintenir à grande échelle. En cas de panne d’un service, le système continue de fonctionner, mais l’on peut perdre la cohérence des données, on privilégie la haute disponibilité du système, dans les cas où la cohérence des données est critique, il est tout à fait possible de mettre en place des coupe-feux pour désactiver automatiquement certains services.
4. Conseils et bonnes pratiques
Les conseils et bonnes pratiques suivants s’inspirent en grande partie de Michael Bryzek, co-fondateur et directeur technique chez Flow.io, et sa conférence « Design microservice architectures the right way ».
Langage des microservices :
On a vu précédemment que l’un des avantages d’une architecture microservices est que chaque service peut être développé avec le langage que l’on souhaite. En pratique, il est conseillé de limiter au maximum le nombre de langages utilisés. Cela permet aux développeurs de changer d’équipe facilement et de travailler sur n’importe quel service et être productifs très rapidement, encore plus s’il y a une forte cohérence entre les services. En limitant le nombre de langages, le recrutement est également simplifié.
API définition :
Une architecture pilotée par ses spécifications est le meilleur moyen de produire des API de qualité. Les définitions d’API peuvent être faites en JSON ou YAML indépendamment du code (pas généré avec des annotations) de façon à ne pas être dépendant des langages utilisés dans les services. Le tout placé dans un projet séparé qui regroupe toutes les spécifications afin d’avoir un lieu unique pour gérer les spécifications. Il est alors possible de mettre en place un linter sur ce projet pour garantir la cohérence de style de toutes les API. Dans la définition des API, il est judicieux de prévoir une route pour connaître l’état du service (exemple : “/health”) et aussi de prévoir le versionnage des API pour ne jamais rien « casser » pour les utilisateurs qui l’utilisent.
Génération de code :
Grâce à une bonne définition des API, il est possible de faire de la génération de code pour créer tous les services. De cette façon on est certain que la définition correspond réellement à ce que les services exposent (le code généré ne doit évidemment pas être modifié, mais doit exposer des interfaces permettant d’implémenter les méthodes nécessaires) et on assure également une forte cohérence. On évite ainsi de perdre du temps avec des tâches répétitives qui n’ont pas de valeurs ajoutées. Dans cette génération de code, il faut aussi prévoir la génération des tests et des mocks nécessaires, il est important d’y penser dès le début pour éviter que cela devienne très complexe.
Bases de données :
Chaque service a sa propre base de données à laquelle aucun autre service ne peut accéder. Si l’on perd cette règle très importante, les mises à jour de bases de données deviennent risquées et complexes puisqu’il faut faire des correctifs dans tous les services qui accèdent à cette base. Si un service a besoin de faire des modifications sur la base d’un autre service, il doit passer par son API ou via des événements.
Déploiement continu (CD) :
Le déploiement continu est un prérequis pour gérer une architecture microservices. Sans déploiement continu, il faudrait déployer chaque service à la main, ce qui serait chronophage, source d’erreurs et ralentirait considérablement le temps entre chaque mise à jour.
Communication des services :
Pour permettre la communication entre les services, il est préférable d’utiliser des flux d’événements plutôt que d’utiliser des requêtes REST sur les API, n’utiliser les API que dans le cas où il n’y a pas d’autre choix que de faire une transaction synchrone. En utilisant des événements avec un modèle publish-subscribe on réduit la latence pour les utilisateurs (puisque la communication est asynchrone), et en cas d’erreur, il est possible de rejouer une chaîne d’événements s’ils ont été enregistrés dans un journal. De plus, on peut tracer tout ce qui s’est passé dans le système facilement. De la même façon que pour les API, il est important de tenir une spécification du schéma des événements en laquelle tous les développeurs peuvent avoir confiance. Dernier point, chaque producteur d’événements doit avoir au moins un destinataire et chaque consommateur doit être idempotent.
Maintenance des dépendances :
Maintenir une architecture microservices peut être un challenge. Chaque service a des dépendances qu’il faut mettre à jour tout en s’assurant que le service continue de fonctionner correctement. Selon le nombre de services de l’architecture, cette tâche peut s’avérer très longue. Heureusement, ce processus peut être automatisé avec un script qui s’exécute chaque semaine (par exemple), et grâce aux tests mis en place, on peut s’assurer que l’architecture continue de fonctionner.
5. Preuve de concept
Pour mettre en oeuvre toutes les bonnes pratiques ci-dessus, j’ai créé un petit exemple concret d’une architecture microservices pilotée par les spécifications. Ce projet est en quelque sorte un framework qui permet de rapidement démarrer une architecture microservices dans de bonnes conditions avec tous les outils nécessaires.
Donnez-lui une star si vous trouvez ce projet utile !