This post has already been read 1449 times!

Microservices. C’est une architecture dont on entend beaucoup parler, mais que se cache-t-il derrière ce terme ?

Avec une série de trois articles, nous allons tenter de découvrir ce qu’est une architecture microservices et ce qu’elle change par rapport à une architecture « classique ».

Le premier article s’intéressait tout d’abord aux concepts de ces architectures
Ce deuxième article illustrera ensuite des exemples possibles d’architectures microservices
Le troisième article s’attachera, quant à lui, aux pièges de ces architectures et aux pistes qui permettent de les éviter

Avant la présentation de ces architectures, intéressons nous rapidement à une application monolithique comme on en rencontre beaucoup actuellement. Voici un schéma présentant ce type d’application:

Capture d’écran 2015-03-09 à 14.44.12

Notre application est faite d’un seul package, toutes les couches communiquent entres elles via des appels de méthodes. Ce constat va nous servir de point de départ pour notre premier exemple d’architecture: les microservices en REST.
Microservices en REST

Comme expliqué dans les concepts, une architecture microservices est un système de services communicant entre eux.

Un des moyens les plus simples de faire communiquer ces services entre eux est de le faire à travers le protocole HTTP via une API REST. En reprenant l’exemple précédent, on peut imaginer isoler chaque partie métier de notre application pour la transformer en un service indépendant exposant sa propre interface. Les autres services du système communiqueront avec lui.

L’organisation de notre application ressemblera à ceci :

Capture d’écran 2015-03-09 à 14.44.23

Chacune des parties métier de notre application est devenue un service indépendant, ce qui présente plusieurs avantages:

Chaque service a une base de code plus petite, ce qui le rend plus lisible et facilite sa maintenance.
Chaque service étant indépendant du reste, il peut évoluer plus rapidement. Si le contrat d’interface est constant, les autres services ne seront pas impactés par les changements internes. De plus, toujours grâce à cette indépendance, il est possible d’écrire chaque service dans le langage qui correspond le mieux à l’équipe dédiée à son développement ou au besoin précis du service. Il est ainsi plus simple de focaliser les efforts de performance sur un service en particulier qui sera écrit en langage C, adopter l’isomorphisme javascript pour une interface web ou encore profiter de la richesse des bibliothèques java pour l’intégration avec le SI.
Chaque service peut travailler avec ses propres bases de données, du type qui correspond le mieux à son usage. Par exemple, il pourra utiliser une base NoSQL sans que les autres services soient obligés d’évoluer.

Un des gros avantages de cette architecture est de permettre aux services de scaler facilement et distinctement. Un service peu sollicité peut rester sur une instance unique tandis qu’un service devant supporter une charge importante peut multiplier ses instances d’exécutions de manière transparente.

Pour réaliser cela, il faut une méthode simple permettant aux services de se découvrir entre eux.

Chaque service doit connaitre les adresses de l’ensemble des services qu’il utilise. Une façon de faire serait d’indiquer l’adresse IP de chaque API dans la configuration du service. Cette pratique pose toutefois plusieurs problèmes:

Si l’adresse IP d’un service change, vous devrez changer l’intégralité des configurations utilisant ce service
Gérer les multiples instances d’un service avec une configuration par adresse IP est très compliqué. Cela oblige à maintenir manuellement l’inventaire des services utilisés

Etant donné que nous sommes dans un univers web, autant utiliser les moyens du web. La solution la plus immédiate consiste en la mise en place d’un loadbalancing qui va répartir la charge en entrée sur plusieurs instances du service. Quelle que soit l’implémentation technique retenue (IP, DNS, proxy, …), elle permet de scaler horizontalement, facilite la montée en version du service tout en offrant aux services consommateurs un point d’entrée constant.

Faisons un zoom sur le comportement d’un service fortement chargé où la communication est directe entre les services :

Capture d’écran 2015-03-09 à 14.44.28

Chacune des applications a été configurée pour accéder directement à un service en particulier. Une telle typologie, bien que simple à mettre en œuvre présente plusieurs inconvénients. D’une part il est impossible d’ajouter une instance d’un service consommé et d’autre part la mise à jour du service entraine nécessairement une rupture dans la continuité de service.

L’utilisation d’un loadbalancer pour supporter la montée en charge n’est pas spécifique aux architectures microservices mais il permet d’orchestrer efficacement la montée en version. Le nouveau service est démarré, le loadbalancer redirige les requêtes vers le nouveau service puis l’ancien service peut être décommissionné.

Maintenant si nous glissons un loadbalancer dans le système, il est facile d’ajouter de nouvelles instances de notre service (vert) sans bouleverser le système :

Capture d’écran 2015-03-09 à 14.44.32

Le loadbalancer est une bonne solution pour faciliter la scalabilité de votre système. Néanmoins, il faut être conscient que cela ajoute un élément actif sur l’acheminement de la requête et donc une latence supplémentaire ou une potentielle panne. Cet élément devient donc critique. Il s’avère être une solution simple pour quelques services mais peut s’avérer complexe à une échelle plus importante.

Comme nous venons de le voir, ce type d’architecture permet de mettre en place simplement des microservices grâce à un système de communication couramment utilisé, même dans des applications monolithiques. Néanmoins ce système révèle plusieurs problèmes:

Les appels REST sont synchrones, si un service ralentit, l’ensemble du système s’en trouve impacté
Certains appels REST ne sont pas idempotents (principalement POST et PUT) et ne peuvent être rejoués plus tard au premier échec
La multiplication des services peut entraîner un effet « spaghetti » entre les liens de communication
Transposer une application monolithique en microservices en remplaçant un appel de méthode par son équivalent en RPC (REST ou autre) limite les performances globales du système.

Résumer les services REST aux seuls appels synchrones serait un raccourci maladroit. Rien n’interdit en effet de concevoir une architecture reposant sur des messages asynchrones tout en utilisant le protocole HTTP. Certes ce dernier est synchrone par définition mais il est tout à fait possible qu’un service consomme un autre service en communicant une adresse de callback, laquelle sera appelée lorsque le traitement sera terminé. Nous rencontrons fréquemment cette séquence d’échanges dans les protocoles SSO par exemple. Le terme d’asynchronisme peut ainsi avoir une signification différente selon que l’on se place au niveau du protocole d’échange ou au niveau de l’orchestration des échanges.

Pour palier aux différents problèmes évoqués précédemment, un autre système de communication existe : la communication par bus de messages.
Microservices à travers un bus

La communication par bus de messages est fortement poussée dans les architectures microservices. Elle apporte une réelle souplesse que nous allons détailler par la suite.

Néanmoins, ce type de communication change complètement la manière de penser les applications. Il faut être conscient de ce fait avant de se lancer dans la conception d’une telle architecture.

Comme exposé précédemment, la communication par services REST peut devenir une faiblesse lorsque le nombre de liens se multiplie. Dans la communication par messages, les liens entre services sont remplacés par un bus central :

Capture d’écran 2015-03-09 à 14.44.44

Chaque service envoie des messages sur le bus qui seront consommés ensuite par d’autres services. On remarque tout de suite un avantage dans les échanges: chaque service ne connait que le bus de messages comme intermédiaire. Les liens directs entre services sont coupés. Naturellement, chaque service doit connaitre la typologie des messages qu’il souhaite utiliser.

La responsabilité d’un bus est volontairement limitée: transmettre les messages. Suivre cette approche permet de réduire au maximum l’impact du bus de communication sur le métier. Il s’agit d’un composant d’infrastructure sans intelligence particulière. Cette intelligence business est concentrée aux extrémités, dans les (micro-)services. Les critères de sélection d’une solution technique sont ainsi simplifiés : le bus doit faire peu de choses mais bien. Parmi ces critères retenons la capacité de débit, la garantie d’acheminement, la latence ou la typologie réseau.

Pour qu’un service reçoive des messages, il doit simplement écouter sur le bus ceux qui l’intéressent. Les services sont ainsi réellement isolés du reste du système. Leurs seuls liens vers l’extérieur étant la typologie des messages (reçus ou émis), ainsi que le bus.

L’utilisation d’un bus de messages induit un nouveau concept : tout ce qui se passe dans le système est asynchrone. En effet, une fois un message envoyé sur le bus, il n’y a aucune garantie qu’il sera lu et surtout, quand il sera lu.

Ceci oblige à penser l’architecture de manière totalement asynchrone, ce qui peut être déroutant au départ. Néanmoins, cet asynchronisme présente plusieurs avantages :

Le système devient résistant à la lenteur. Une fois le message envoyé, le service n’attend pas de réponse immédiate. Si le service répondant devient lent, l’appelant n’est pas impacté
Le système devient tolérant à la panne. Si un service tombe, tous les messages qui lui sont destinés sont gardés dans le bus. Il pourra reprendre le travail une fois redémarré, sans perte.
Le système devient facilement scalable. N’importe qui peut lire dans le bus, qu’il y ait une ou dix instances du même service ne change rien pour le bus et cela répartit la charge entre chaque instance

Ce type d’architecture permet de réellement découpler les services et de les faire évoluer de manière totalement indépendante. Tant que la typologie des messages ne change pas, l’évolution des services sera transparente pour le système.

Comme vous l’avez sans doute remarquer, le point sensible des architectures avec communication par messages est le bus lui même. Il devient le goulot d’étranglement du système.

Heureusement, aujourd’hui, les technologies de bus supportent particulièrement bien la charge, permettent de scaler le bus facilement et si besoin de rejouer les messages. Le rejeu d’anciens messages conduit vers le principe d’event sourcing.

Les bus se doivent également d’être des « dumb pipe ». Autrement dit, un bon bus ne porte aucune intelligence de routage. Il reçoit des messages que d’autres services viennent lire. Le bus est un élément technique du système, pas un élément fonctionnel. C’est un point essentiel qui permet d’éviter d’avoir des bus très complexes contenant des tables de routages encore plus complexes.

Un autre point également très important dans ce type d’architecture est le monitoring. L’ensemble étant totalement asynchrone et découplé, un service peut tomber sans que cela soit visible immédiatement. Les autres services continueront d’envoyer leurs messages sans ralentissement visible. Il est donc primordial de penser à un bon système de monitoring. Que les données collectées soient business ou techniques, il est impératif de savoir ce qui se passe dans le système. Mettre en place des dashboards simples et visuels permettront de détecter rapidement les problèmes.
Conclusion

Nous venons de voir deux types d’architectures microservices. La première avec communication par service REST est certainement la plus simple à mettre en oeuvre. Elle est efficace lorsqu’il s’agit de migrer depuis une application monolithique.

La deuxième architecture avec le bus de messages est la plus intéressante. L’asynchronisme rend plus robuste et plus tolérant le système. Le bus permet de scaler horizontalement tout type de services simplement et efficacement.

Il est par ailleurs tout à fait envisageable de mixer les deux types d’architecture afin d’obtenir une architecture hybride. Ceci peut par exemple être envisagé lors d’une migration progressive vers du tout asynchrone.

Ces architectures sont surtout utilisées dans des environnements complexes devant absorber une charge importante.

Le lot de nouveautés des microservices apporte également de nouvelles problématiques : sécurité du bus de messages, gestion de l’asynchronisme, débuggage de systèmes complexes. Le monitoring de ces systèmes est un élément très important à prendre en compte. Sans un bon monitoring, les systèmes complexes peuvent devenir un enfer à gérer en production.

Heureusement, de nouveaux outils sont apparus pour aider à la mise en place d’une architecture microservices. Des frameworks comme vert.X avec un bus intégré ou encore Spring Boot permettent de créer rapidement de nouveaux services.

L’orchestration du système est également un point essentiel. Le nombre de services se multipliant, il devient important d’automatiser les déploiements et la gestion de l’ensemble. Là aussi, des outils font leur apparition comme Marathon et Mesos. Les microservices se marient idéalement avec Docker.

Dans notre prochain article, nous verrons plus en détails les pièges possibles des microservices et comment les éviter.

Source of this article :
http://blog.xebia.fr/2015/03/09/microservices-des-architectures/

Leave a Reply

Post Navigation