1. Introduction
1.1 Les débuts de la programmation
Pour commencer, retournons à la période où il n’y avait pas de micro-services. Au début des apparitions des ordinateurs, les applications contenaient toutes les instructions nécessaires pour l’exécutions des programmes. Et le tout se compilait et s’installait sur la machine, comme les premiers éditeurs de texte ou les premiers jeux vidéo. Ensuite, les applications devenaient de plus en plus riches en fonctionnalités et, par conséquent, plus complexes. Ainsi, la maintenance de toute l’application dans un seul block de code était pénible, ce qui a poussé les développeurs à adopter des “best practices”? Notamment écrire des petits modules chargés de faire quelques fonctionnalités, et construire l’application à partir de ces modules. Un module indépendant peut être utilisé dans une autre application et ainsi de suite. La gestion de la complexité n’était plus un problème. Mais, on devait quand même tout compiler et installer sur une seule machine.
1.2 L’arrivée des applications web et l’architecture monolithique
On peut dire que les applications web ont révolutionné la façon dont on construit les programmes. La mémoire de stockage et la puissance de calcul d’un seul ordinateur étant limité, les gens se penchaient de plus en plus vers les applications web, où seul le navigateur internet doit être installé sur l’ordinateur. Il n’y avait donc pas de problème de stockage. Juste l’envoi et la réception du code html sont faits sur l’ordinateur, donc juste une faible performance CPU est demandée à l’utilisateur. Les développeurs devaient donc écrire les applications en plusieurs modules indépendants. Ensuite, ils compilaient et déployaient le tout sur un serveur que l’utilisateur peut joindre avec son navigateur. C’est ce qu’on appelle l’architecture monolithique. Tout semble bien jusqu’à maintenant, mais peut-on faire mieux ?
Les difficultés qu’on peut rencontrer avec l’architecture monolithique :
- Complication du déploiement
- Comme tous les modules sont déployés en une seule fois sur le serveur, tout changement dans n’importe quelle module de l’application nécessite le redéploiement de toute l’application et menace, par conséquent, son fonctionnement intégral.
- Scalabilité non optimisée
- La seule façon d’accroître les performances d’une application conçue en architecture monolithique, suite à une augmentation de trafic par exemple, est de la redéployer plusieurs fois sur plusieurs serveurs. Or, dans la majorité des cas, on a besoin d’augmenter les performances d’une seule fonctionnalité de l’application. Mais en la redéployant sur plusieurs serveurs, on accroîtra les performances de toutes les fonctionnalités, ce qui peut être non-nécessaire et gaspiller les ressources de calcul.
Ces deux difficultés principales ont donné naissance à l’architecture micro-services que l’on détaillera dans cet article.
2. Qu’est-ce qu’un micro-service ?
Un micro-service est une petite application destinée à faire une seule fonctionnalité. Par exemple, une application qui envoie un texte à une adresse mail peut être un micro-service. C’est une seule brique de l’application globale, une brique indépendante ayant une seule responsabilité indépendante. Une personne, voire une équipe peut travailler de façon libre et autonome sur un micro-service, le concevoir à leur choix, le coder avec un langage de programmation qu’ils sélectionnent eux-mêmes et le déploient sur le serveur qu’ils veulent. Puis ils fournissent le micro-service en tant qu’API par exemple.
L’application globale sera par la suite la combinaison et l’intégration de tous les micro-services.
3. Les bénéfices d’une telle architecture
3.1 Séparation des responsabilités
Chaque équipe peut travailler sur un micro-service séparément sans se soucier de l’architecture globale. Cela permet aux nouveaux arrivés de s’intégrer facilement car ils n’auront qu’à lire et comprendre le micro-service sur lequel ils travailleront au lieu de se documenter sur toute l’application. Cela pouvant être pénible s’il s’agit d’une application de grande taille. L’ajout des nouvelles fonctionnalités devient très rapide ainsi que la localisation des bugs.
3.2 Scalabilité
L’architecture micro-services permet d’augmenter les performances des applications d’une façon efficace. Par exemple, si on a besoin qu’une fonctionnalité soit plus performante, il suffit de répliquer le micro-service correspondant et le déployer sur plusieurs serveurs tout seul. Ainsi, on évitera de répliquer toute l’application à chaque fois qu’un de ces composants est en manque de ressources.
4- Les difficultés
Bien que les architectures micro-services nous permettent d’optimiser notre façon de développer, de déployer et d’utiliser les ressources physiques, elles présentent quelques difficultés à prendre en considération lors du design.
4.1- Tests plus compliqués
Comme tout micro-service doit être indépendant et séparé du reste de l’application, les tests doivent l’être aussi. Cela signifie que tout appel à un autre micro-service doit être moqué avec du code se comportant comme tel. Les moques doivent ainsi être maintenus au même comportement du micro-service tiers, et toute différence avec celui-ci peut laisser passer des bugs inaperçus en production.
4.2- Latence du réseau
Les appels entre micro-service se font à travers le réseau ; des appels HTTP par exemple, ce qui est beaucoup plus lent que des appels de fonction dans des modules. Parfois on peut même faire face à des appels qui n’aboutissent pas ou qui échouent, cela est impossible en monolithique.
4.3- Partage des données
En architectures micro-services, il faut faire très attention à la synchronisation des données entre les différents réplicas d’un micro-service, car il y aura toujours un accès concurrent aux données.
Par exemple, deux instances du service modifient une donnée en même temps, ou alors un changement de configuration traité par une instance doit être communiqué à tous les autres réplicas.
4.4- Monitoring
Le monitoring est plus facile en monolithique qu’en micro-services. Il faut mettre en place un agent de monitoring sur chaque serveur hébergeant un micro-service et surveiller le trafic de chacun d’entre eux. En passe de la surveillance d’un seul processus monolithe à plusieurs.
5- Bonnes pratiques
Les difficultés que nous avons évoquées ci-dessus nous poussent à adopter de bonnes pratiques afin de faciliter et optimiser le développement en micro-services :
5.1- Commencer en monolithique
Il est très recommandé de commencer d’abord par un design monolithique et de séparer les micro-services petit à petit, en fonction de notre vision des futures fonctionnalités et du trafic de la production. Cela permet de réduire le nombre de micro-services et de bien choisir les services à séparer.
5.2- Vérifier l’indépendance des micro-services
Pour minimiser la dépendance entre micro-services et par suite le partage des données entre eux, chaque micro-service doit avoir une seule responsabilité bien définie et ne contenir que les fonctionnalités liées à celle-ci.
5-3- Stabiliser le contrat d’interface
L’interface API de chaque micro-service doit changer le moins possible afin de garder l’indépendance et ne pas perturber le travail sur les micro-services dépendant de celui-ci. Alors si on se trouve obliger de faire évoluer l’interface API, il est très recommandé de garder la compatibilité avec l’ancienne version ou alors de lancer et synchroniser les deux versions en parallèle pendant une durée suffisante. Cela permettra aux autres équipes de migrer vers la nouvelle version.
6- Exemple
Dans cette partie, on met en place une architecture en micro-service d’une application basique afin d’illustrer l’utilité de cette architecture et ses bénéfices. On suivra aussi les bonnes pratiques mentionnées ci-dessus.
6.1- Use case
Imaginons qu’on souhaite développer une application qui joue le rôle d’un marché de vente de produits en ligne. Pour simplifier, les produits sont disponibles chez des fournisseurs, et leur manipulation s’effectue par des appels API sur leurs serveurs. Notre application (appelons la speedBuy) doit exposer les produits aux clients par appel d’API de voir les caractéristiques d’un produit et pouvoir l’acheter. On aura donc besoin de fournir aux clients les trois end-points suivants :
- GET speedBuy/catalog : lister le catalogue de tous les produits disponibles
- GET speedBuy/details/<id> : lister les caractéristiques du produit numéro <id>
- POST speedBuy/buy/<id> : acheter le produit numéro <id>
La majorité des fournisseurs acceptent les mêmes end-points pour exposer et vendre leurs propres produits avec json comme format de données. On décide donc de fournir notre API en json aussi. Cependant le reste des fournisseurs n’acceptent que le format xml, les end-points sont comme suit : “fournisseurX/xmCatalog” , “fournisseurX/xmlDetails/<id>” , “fournisseurX/xmlBuy /<id>”.
Exemples de réponses :
Requête : GET speedBuy/catalog
Réponse :
{ "1": { "nom": "Produit1", "description": "caractéristique du produit 1", }, "2": { "nom": "Produit2", "description": "caractéristique du produit 2", }, "3": { "nom": "Produit3", "description": "caractéristique du produit 3", }, ... }
Requête : GET speedBuy/details/3
Réponse :
{ "nom": "Produit3", "fournisseur": "fournisseur10" "description": "caracteristique du produit 1", "prix": 50, "devise": "EUR", "note" : 4.5, "nombre_avis" : 500 }
Requête : GET speedBuy/buy/3
“body” : { "nomAcheteur": "Bob Donut", "Payment": "CB 9999 9999 9999 9999", "adresse" : "123 rue Pierre Paul … " }
Réponse :
{ "Status": "CONFIRME", "DelaiLivraison": 2 }
Pour les fournisseurs n’acceptant que xml comme format, la dernière réponse par exemple sera comme suit :
<?xml version="1.0" encoding="UTF-8"?> <root> <nom>Produit1</nom> <description>caracteristique du produit 1</description> <fournisseur>fournisseur3</fournisseur> <prix>50</prix> <devise>EUR</devise> <note>4.5</note> <nombre_avis>500</nombre_avis> </root>
6.2- Architecture
6.2.1 Monolithique :
Si on commence par une architecture monolithique, notre application aura la forme suivante :
6.2.2 Le premier micro-service :
Après plusieurs itérations et en suivant les bonnes pratiques de développement (refactoring et tests …) on arrive à avoir une version en production qui fonctionne.
On remarque que le module faisant la transformation de json vers xml, vue sa simplicité, était stable depuis le début et on a dû à chaque fois le redéployer et lancer ses tests avec tout le déploiement de notre application. On décide donc de le séparer de notre application principale pour se contenter de gérer la logique du business sans se soucier de ce module. Commençons par séparer notre premier micro-service, appelons le xmlRouter. Il va être chargé de gérer tout ce qui est lié à la transformation vers xml et doit permettre à speedBuy de se comporter comme si tous les fournisseurs acceptaient json comme format et qu’il est l’un d’entre eux. Il fournira donc les mêmes trois APIs d’un fournisseur json et se chargera d’agréger dans sa réponse toutes les réponses des fournisseurs xml :
6.2.2 Plus de micro-services
Après quelques mois en production, on reçoit de plus en plus de demandes de nouvelles fonctionnalités et le trafic d’appel API augmente tellement que notre serveur n’arrivera plus à absorber dans les mois prochains. Certes on peut répliquer notre application sur plusieurs serveurs et répartir les charges entre eux, mais, en analysant le trafic on remarque que le end-point “catalog” est appelé trois fois plus que “details/<id>” lui-même appelé deux fois plus que “buy/<id>”. On peut donc faire mieux que de juste redéployer l’application sur plusieurs serveurs. En séparant chaque end-point en un micro-service indépendant on peut répliquer chacun autant de fois que le demande son trafic.
6.2.3 Plus d’optimisation
On peut optimiser encore plus l’utilisation des ressources physiques en rendant le nombre de réplications de chaque service élastique. Cela veut dire que les serveurs se multiplient au moment du peak et se libèrent quand le trafic diminue.
7. Conclusion
Pour conclure, l’architecture micro-service consiste donc à décomposer l’application globale en plusieurs composants responsables d’une seule fonctionnalité et communiquant en réseau entre eux ainsi qu’avec les applications tierces. Elle permet un déploiement facile et une utilisation optimisée des ressources de calcul. C’est un modèle d’architecture adopté de plus en plus par les entreprises et continue à évoluer chaque jour, parmi les évolutions on compte le modèle Function as a Service ou encore Serverless.