samedi 23 mai 2020

Introduction aux Microservices: partie 2


On a vu, dans la première partie de cette introduction aux microservices, qu’il n’existe pas de définition formelle du style architectural des microservices. Toutefois, plusieurs auteurs et praticiens s’accordent sur un ensemble de caractéristiques communes aux architectures qui répondent à l’étiquette des microservices. Comme pour toute définition qui décrit des caractéristiques communes, les architectures de microservices n'ont pas toutes ces caractéristiques, mais on peut s’attendre à ce que la plupart présentent plusieurs de ces caractéristiques.
Dans cet article, je présente neuf caractéristiques communes aux architectures de microservices.

Basée sur les composants via les services


Un composant logiciel est défini comme une unité logicielle qui est remplaçable et évolutive indépendamment. Par définition, une architecture microservice est essentiellement une architecture de composants. Les monolithes s’appuient aussi souvent sur des composants, mais ces derniers sont des librairies plutôt que des services. La différence est fondamentale, car une librairie s’exécute à l’intérieur du processus qui l’appelle, alors que le service s’exécute dans son propre processus et communique avec le processus appelant par des mécanismes standardisés tels que les services web ou les appels distants de procédure (RPC). 
L'une des principales raisons pour lesquelles les services sont utilisés comme composants (plutôt que les librairies) est que les services peuvent se déployer indépendamment. Dans une application composée de plusieurs librairies, une modification d’un composant entraîne le redéploiement de l'application toute entière. Mais si cette application est plutôt composée de services, on peut s’attendre à ce qu’une modification à un seul service nécessite seulement le redéploiement de ce service. Toutefois, certains changements affectent aussi les interfaces de service, et dans ce cas une coordination est nécessaire. Le but d'une bonne architecture des microservices est de minimiser de tels changements en concevant des services aux frontières bien établies et en mettant en place des mécanismes d'évolution dans les contrats de service. 
D’autre part, un service en tant que composant offre une interface plus explicite. La plupart des langages de programmation ne disposent pas d'un bon mécanisme pour définir une interface explicite. Ce n’est qu’en lisant la documentation et en restant disciplinés que les clients de certaines librairies évitent de briser l'encapsulation des composants, ce qui aboutit à un couplage trop fort entre les composants. En utilisant des mécanismes d’appel distant bien explicités, les services permettent d’éviter ces situations.

Organisée autour des capacités d’affaires


Pour décomposer une grosse application en plusieurs parties, on avait souvent tendance à se concentrer sur les couches technologiques. Comme résultat, on avait par exemple une équipe dédiée à l'interface utilisateur, une équipe pour la logique des traitements et une équipe pour les bases de données. Lorsque les équipes sont séparées ainsi, même un simple petit changement peut conduire à un projet interéquipe qui est coûteux en temps et en ressources. 
L'approche microservices de la décomposition est différente. D’abord, les services sont organisés autour des capacités d’affaires de l’entreprise. Ensuite, la mise en œuvre complète d’un service est confiée à une seule équipe. Par conséquent, les équipes sont pluridisciplinaires et on y trouve l'ensemble des compétences requises pour concevoir et livrer un service: expérience utilisateur, base de données, gestion de projet, etc. Chaque équipe est responsable de tout le cycle de développement du service dont elle a la responsabilité, de la conception jusqu’à la livraison. Enfin, les équipes ont une certaine autonomie dans le choix des outils et des méthodes de travail. En fonctionnant de façon indépendante et en travaillant dans un environnement hautement collaboratif, les équipes peuvent développer et livrer plus rapidement.

Des Produits et non des Projets


La plupart des efforts de développement d'applications utilisent un modèle de projet. L'objectif est de fournir un logiciel qui est ensuite considéré comme terminé. La réalisation terminée, l'équipe de projet est dissoute et le logiciel est remis à une autre structure de l’organisation qui prendra en charge la maintenance.
Les partisans des microservices ont tendance à éviter ce modèle, préférant plutôt l'idée qu'une équipe devrait posséder un produit pendant toute sa durée de vie. Une inspiration commune à cela est la notion de chez Amazon: « you build, you run it », qui peut être traduit en français par: « vous construisez, vous l'exécutez », et où une équipe de développement assume l'entière responsabilité du logiciel en production. Cela met les développeurs en contact quotidien avec le comportement de leurs logiciels en production, et puisqu’ils doivent assumer au moins une partie de la charge du support, ça augmente les contacts avec leurs utilisateurs. 
Cette mentalité du produit se rattache au fait que les microservices sont organisés autour des capacités d’affaires. Plutôt que de considérer le logiciel comme un ensemble de fonctionnalités à compléter, il existe une relation permanente où la question est de savoir comment le logiciel peut aider ses utilisateurs à améliorer les capacités de l'entreprise. Il n'y a aucune raison pour laquelle cette même approche ne peut pas être adoptée avec des applications monolithiques, mais la petite granularité des services peut faciliter la création de relations personnelles entre les développeurs de services et leurs utilisateurs.

Points d’extrémité intelligents et mécanismes de communication simple


Cette caractéristique concerne les communications entre les microservices. 
Dans la mise en oeuvre des 1ères génération de SOA, l'accent a été mis sur le fait d'intégrer de manière significative beaucoup d’intelligence dans le mécanisme de communication entre les services. C’est ainsi qu'a été inclus dans les produits ESB des mécanismes sophistiqués pour le routage des messages, la chorégraphie, la transformation et l'application de règles d’affaires. 

La communauté des microservices privilégie une approche alternative, qui se dit mieux en anglais par : « smart endpoints and dumb pipes », ce qui peut se traduire en français par « Des terminaux intelligents et des canaux stupides ». 
Pour mieux comprendre ce slogan, examinons le mécanisme de communication entre les microservices. 

Les deux protocoles de communication les plus utilisés par les microservices sont :

    1. Approche requête-réponse HTTP avec les APIs

La communication requête-réponse entre les microservices est utilisée partout où un service envoie une requête et s'attend qu’une réponse lui soit retournée, sous forme d’une ressource ou d'un accusé-réception. Le moyen le plus simple d'implémenter ce modèle est d'utiliser HTTP, en suivant idéalement les principes REST. Dans cette approche, les équipes de microservices utilisent les principes et les protocoles sur lesquels repose l’Internet. Les ressources souvent utilisées peuvent être mises en cache avec très peu d'efforts de la part des développeurs.

    2. Approche par messagerie sur un bus de messages léger

Cette approche est aussi appelée le modèle « Observateur ». 
La communication avec les observateurs est essentielle à l'extension des microservices. Toutes les communications ne nécessitent pas une réponse ou un accusé de réception. En fait, dans de nombreux flux de travail, il existe au moins quelques éléments de logique qui devraient être entièrement asynchrones et non bloquants. La manière standard de distribuer ce type de charge de travail consiste à transmettre des messages à l'aide d'un bus de messages léger. C'est-à-dire un service de courtier ou un répartiteur de message, idéalement celui qui implémente une file d'attente. RabbitMQ, ZeroMQ, Kafka ou même Redis peuvent tous être utilisés comme des canaux stupides qui permettent à un microservice de publier un événement, tout en permettant à d'autres microservices de s'abonner à une ou plusieurs classes d'événements. 

Dans les deux approches, l’intelligence reste dans les deux d'extrémités de la chaîne de communication, occupées par les services qui produisent et consomment des messages.  On peut remarquer que, contrairement à l’ESB, les répartiteurs de message dans le modèle observateur sont réduits à leur fonction de base, celle d’acheminer les messages. Ce sont des canaux stupides.

Gouvernance décentralisée


Avant l'avènement des microservices, la gouvernance autour des systèmes logiciels est essentiellement centralisée. 
L'une des conséquences de la gouvernance centralisée est la tendance à la normalisation d’une ou de quelques plateformes technologiques. L'expérience montre que cette approche est contraignante à la longue. 
Avec les microservices, la gouvernance devient fortement décentralisée. 
En divisant les composants du monolithe en services, on a la possibilité de choisir des technologies différentes lors de la construction de chacun d'eux. On peut par exemple :
  • utiliser Node.js pour afficher une simple page de rapports,
  • utiliser C ++ pour un composant particulièrement stimulant et qui doit réagir temps quasi réel,
  • utiliser une version différente de la base de données qui convient mieux au comportement de lecture d'un composant,
  • etc.
Bien sûr, ce n'est pas parce que vous pouvez faire quelque chose que vous devriez le faire, mais partitionner votre système ainsi vous donne du moins droit à ces options. 
Aussi, les équipes des microservices préfèrent également une approche différente des standards existants. Plutôt que d'utiliser un ensemble de normes définies et écrites quelque part, elles préfèrent l'idée de produire des outils utiles dont d'autres développeurs peuvent se servir pour résoudre des problèmes similaires à ceux auxquels elles se sont confrontées. Ces outils sont généralement collectés à partir des implémentations et partagés avec des groupes plus larges, parfois, mais pas exclusivement, à l'aide d’un modèle "open sources" interne. Maintenant que git et github sont devenus le système de contrôle de version de facto, les pratiques open sources sont de plus en plus courantes en interne. Netflix est un bon exemple d'une organisation qui suit cette philosophie. 
Le besoin de centraliser la gestion des contrats s’amenuise. En effet, de nombreux outils simples existent permettant de définir le contrat avant même que le code du nouveau service ne soit écrit. Le service n'est alors construit qu'au point où il satisfait le contrat. Ces techniques et les outils qui les entourent limitent le besoin d’une gestion centralisée des contrats la dépendance (couplage) temporelle entre services. 
L'apogée de la gouvernance décentralisée est peut-être encore le fameux modèle « build it / run it » vulgarisé par Amazon. Comme nous l’avons mentionné précédemment, les équipes sont responsables de tous les aspects du logiciel qu'elles construisent. Ce n’est certainement pas la norme, mais on voit de plus en plus d'entreprises pousser la responsabilité vers les équipes de développement. Netflix est une autre organisation qui a adopté cette philosophie. 
Ces idées sont à peu près aussi éloignées que possible du modèle traditionnel de gouvernance centralisé.

Gestion centralisée des données


La décentralisation de la gestion des données se présente de différentes manières. D’abord au niveau le plus abstrait, c'est-à-dire que le modèle conceptuel est différent d'une approche à l'autre. Il s'agit d'un problème courant lors de l'intégration dans une grande entreprise, par exemple, un client dans la facette vente ne se présente pas comme dans la facette support du modèle conceptuel. Ce même client peut avoir des attributs différents entre les deux facettes ou, pire encore, des attributs qui ont des sémantiques différentes entre les deux facettes. 
Ce problème est courant entre les applications, mais peut également survenir à l’intérieur d’une même application, en particulier lorsque celle-ci est divisée en composants. Une manière utile de penser à ce sujet est la notion de « Bounded Context » de la conception orientée domaine (DDD). En DDD, on divise un domaine complexe en plusieurs contextes délimités et on marque les relations entre eux. Ce processus est utile autant pour les architectures monolithiques que pour les microservices, mais il existe une corrélation naturelle entre les frontières de service et celles des contextes, qui aide à clarifier et à renforcer les séparations. 
En plus de décentraliser les décisions concernant les modèles conceptuels, les microservices décentralisent aussi les décisions de stockage des données. Dans les architectures monolithiques, on a l'habitude d’avoir une base de données pour une application, parfois pour une gamme d’applications. 
Les microservices préfèrent laisser chaque service gérer sa propre base de données. Parfois ce sont des instances différentes d’un même SGBD ou des SGBD entièrement différents, voire des bases de données SQL pour certains et NoSQL pour d’autres. 
Dans la cartographie de l’architecture exemple pour une application de commerce électronique, on peut voir que :
  • le service des comptes a sa propre base de données,
  • le service des inventaires a sa propre base de données,
  • le service des livraisons a sa propre base de données.
L'utilisation d'une base de données par service présente les avantages suivants: 
  • aide à garantir que les services sont couplés de manière lâche. Les modifications apportées à la base de données d'un service n'ont aucun impact sur les autres services
  • chaque service peut utiliser le type de base de données le mieux adapté à ses besoins. Par exemple, un service qui effectue des recherches de texte pourrait utiliser ElasticSearch. Un service qui manipule un graphe social pourrait utiliser Neo4j.
L'utilisation d'une base de données par service présente les inconvénients suivants: 
  • la mise en œuvre de transactions couvrant plusieurs services n'est pas simple. Il est préférable d'éviter les transactions distribuées. De plus, de nombreuses bases de données modernes (NoSQL) ne prennent pas en charge les transactions distribuées
  • l'implémentation de requêtes combinant des données qui se trouvent dans plusieurs bases de données est difficile. Cette difficulté est accrue lorsque les données sont distribuées entre des bases de données SQL et NoSQL 
Il existe plusieurs solutions (patrons de conception) pour implémenter des transactions et des requêtes qui impliquent plusieurs services. Pour les transactions distribuées, le pattern Saga est actuellement la solution la mieux évaluée. Quant aux requêtes distribuées, deux solutions sont le plus souvent utilisées : la composition des APIs et le modèle CQRS.

Automatisation de l’infrastructure 


Dans cette caractéristique, il s’agit des infrastructures de livraison continue, d’intégration continue, d'automatisation des tests et de gestion des microservices en production. Les techniques d'automatisation des infrastructures ont énormément évolué au cours des dernières années. L'évolution du cloud a réduit la complexité opérationnelle du développement, du déploiement et de l'exploitation des microservices. La plupart des équipes de microservices possèdent une vaste expérience de la livraison continue et de son précurseur qu'est l'intégration continue. Des lots de tests sont exécutés automatiquement ainsi que les déploiements vers différents environnements. Ce sont autant de techniques d'automatisation des infrastructures. Certaines équipes ont d’ailleurs étendu cette automatisation à la gestion des microservices en production. 

Comme conséquence, l’automatisation accrue, en raison de la livraison et du déploiement continu, a favorisé la création d'un ensemble d’outils utiles pour aider les développeurs et les équipes d’exploitation. Les outils de création d'artefacts, les outils de gestion des codes sources et les outils de mise en place de services simples comme l'ajout des mécanismes de surveillance et de journalisation standard; sont désormais assez courants. Le meilleur exemple sur le Web est probablement l'ensemble d'outils open source de Netflix, mais il y en a d'autres, qui sont largement utilisés. 

Tolérant aux pannes (Résilience) 


L'utilisation des services comme composants va avec l’exigence que les applications doivent être conçues de manière à pouvoir tolérer la défaillance des services. Puisque chaque service s’exécute dans un processus à part, tout appel de service pourrait échouer en raison de l'indisponibilité du fournisseur. Le client doit y répondre le plus gracieusement possible. Ceci est un inconvénient par rapport à une conception monolithique, car il introduit une complexité supplémentaire pour le gérer. La conséquence est que les équipes de microservices réfléchissent constamment à la façon dont les défaillances de service affectent l'expérience utilisateur. Étant donné que les services peuvent cesser de fonctionner à tout moment, il est important de pouvoir détecter les pannes rapidement et, si possible, de restaurer automatiquement le service. 

Les équipes de microservices mettent beaucoup l'accent sur le monitoring en temps réel de l'application, vérifiant à la fois les éléments architecturaux (comme le nombre de requêtes que la base de données reçoit par seconde) et les mesures pertinentes pour l'entreprise (comme le nombre de commandes reçues par minute). Le monitoring sémantique peut fournir un système d'alerte précoce en cas de problème, qui déclenche le suivi et l'enquête par les équipes de développement.

Conception évolutive  


Les praticiens de microservices sont généralement issus d'un contexte de conception évolutive et considèrent la décomposition des services comme un outil supplémentaire pour permettre aux développeurs de contrôler les changements dans leurs applications, sans ralentir ces changements. Contrôler les changements ne signifie pas nécessairement de réduire leur fréquence ou leur portée, car avec les bonnes pratiques et les bons outils, on peut apporter des modifications fréquentes, rapides et bien contrôlées au logiciel.

Quand on essaye de diviser un système logiciel en composants, on est confronté à la décision de comment découper les pièces. Autrement dit, quels sont les principes sur lesquels on décide de découper notre application? La propriété clé d'un composant est la notion de remplacement indépendant et d'évolutivité. Chaque composant doit pouvoir évoluer ou être remplacé indépendamment des autres. Ceci implique qu’on recherche des points où on peut imaginer réécrire un composant sans affecter ses collaborateurs. En effet, de nombreux groupes de microservices vont plus loin en s'attendant explicitement à ce que des services soient supprimés plutôt  que d'évoluer à plus long terme. Cette emphase sur la « remplaçabilité » est un cas particulier d'un principe plus général de conception modulaire. L’idée est de garder les choses qui changent en même temps dans le même module. Les parties d'un système qui changent rarement devraient être dans des services différents de ceux qui subissent des ajustements fréquents. Si on se retrouve à changer plusieurs services ensemble à plusieurs reprises, c'est un signe qu'ils doivent être fusionnés.

La facilité de remplacer ou de supprimer un composant est pratique pour les fonctionnalités qui ne vont pas demeurer en permanence au sein de l’application. Par exemple, les pages spécialisées pour gérer un événement ponctuel. Une telle partie du site Web peut être rapidement réalisée à l'aide des langages de développement rapide, et supprimée une fois l'événement terminé. On a vu des approches similaires dans une institution financière où de nouveaux services sont ajoutés pour une opportunité de marché et abandonnés après quelques mois, voire quelques semaines.

Utiliser les services comme composant d’application offre une opportunité pour une planification plus granulaire des versions. On a vu qu’avec les microservices, il suffit de redéployer le ou les services qu’on a modifiés. Cela peut simplifier et accélérer le processus de livraison. L'inconvénient est qu’il faut se soucier, et même s’en inquiéter, des changements à un service qui impactent le fonctionnement de ses consommateurs. L'approche d'intégration traditionnelle consiste à essayer de résoudre ce problème en utilisant la gestion des versions. La préférence dans le monde des microservices est de n'utiliser la gestion des versions qu'en dernier recours. Il est possible de réduire le besoin de recourir à l’utilisation des versions en concevant des services aussi tolérants que possible aux changements de leurs services fournisseurs. 


Conclusion 

Les microservices offrent une façon unique de moduler une application; ils rendent faciles les grandes solutions applicatives, augmentent la productivité, offrent une flexibilité dans le choix des technologies et sont parfaits pour les équipes réparties. Cependant, comme toute approche architecturale, les microservices ont leurs inconvénients. Parfois, l'utilisation de langages, bibliothèques, Frameworks et technologies de stockage de données différents peut être intimidante et paralysante pour les organisations. De plus, ce ne sont pas toutes les équipes qui sont capables de gérer l'autonomie et l'indépendance qu’offrent cette approche. Mais si vous avez un grand projet, avez besoin d'une livraison rapide et autonome, envisagez de faire évoluer votre solution ou devez fréquemment mettre à jour des parties distinctes de votre système, les microservices sont votre meilleur choix.

Pour démarrer avec les microservices, je vous recommande ce cours sur Udemy: Les bases des architectures de microservices

Références:

Aucun commentaire:

Enregistrer un commentaire