Points Clés
- Les microservices sont un moyen, pas un objectif
- Être distribué ne garantit pas d'être découplé
- Les tests de contrat sont une partie importante de toute architecture de microservices
- La décomposition doit se produire dans les couches front-end, back-end et d'intégration, ainsi que dans la couche métier
- Si une entreprise n'a pas la capacité de publier des microservices rapidement et de manière indépendante, de nombreux avantages des microservices seront perdus
Lors de QCon Plus en novembre dernier, j'ai présenté certaines des façons dont les microservices peuvent mal tourner. Je suis consultante pour IBM, et une partie de mon travail consiste à aider les entreprises à adopter le cloud natif. Ces problèmes sont basés sur mon expérience – que, malheureusement, je vois de manière répétée sur le terrain.
Le premier problème que je vois, c'est que parfois nous ne savons même pas quel est le problème. Nous pensons que nous devrions faire des microservices, mais nous n'avons pas vraiment passé assez de temps à définir pourquoi nous faisons des microservices.
Quel problème essayons-nous de résoudre ? Qu'est-ce qui nous fait mal maintenant ? Qu'est-ce qui va être mieux après avoir fait des microservices ? C'est une tendance humaine tout à fait naturelle, surtout pour nous en tant que techniciens. Nous voulons passer à la solution. Nous voulons jouer avec les nouveautés. Malheureusement, même si c'est vraiment très important, déterminer quel problème nous essayons de résoudre est beaucoup moins amusant que de le résoudre.
Les conteneurs aggravent cette tendance naturelle à passer vers des solutions, car les conteneurs sont une technologie quasi magique, ce qui en fait une excellente solution. Ils sont si légers. Ils sont si portables. Ils rendent tellement de choses tellement meilleures. Nous finissons par décider « Parce que j'ai ces conteneurs, ce serait un terrible gaspillage de la capacité du conteneur d'exécuter mon application dans un seul conteneur. Je devrais l'exécuter dans autant de conteneurs que possible !" Malheureusement, « ne pas avoir suffisamment de conteneurs » n'est pas un énoncé de problème valide.
Le développement piloté par le CV
Un autre problème que je vois est le développement piloté par le CV. Nous regardons notre CV, et il y a un grand espace vide où il devrait dire « microservices ». Ce n'est pas bon, alors nous disons : "Je peux résoudre ce problème en réorganisant la pile de mon entreprise". Vous pensez peut-être : « Non, c'est tout simplement trop cynique, Holly. Personne ne prendrait vraiment de décisions architecturales en se basant sur son CV ? » Il s'avère … qu'ils le feraient.
Red Hat a récemment réalisé une enquête portant sur le principaux moteurs du développement basé sur les conteneurs. La progression de carrière était le moteur numéro un. La progression de carrière est une meilleure façon de dire le développement piloté par le CV.
Un vide concernant les microservices sur un CV est un gros problème, car pour le moment, les microservices sont presque une nouvelle orthodoxie. Même si nous ne participons pas à la grande démission, même si nous ne cherchons pas de nouveau travail en ce moment, nous ne voulons pas être les intrus. Lorsque nous regardons autour de nous, il semble que tout le monde fait des microservices. La pensée naturelle est, s'ils font des microservices, qu'est-ce qui ne va pas avec moi si je ne fais pas de microservices ? J'appelle cela "l'envie des microservices".
Les microservices ne sont pas l'objectif
L'envie des microservices est un problème, car les microservices ne sont pas le genre de chose que nous devrions envier. L'un de nos consultants a une heuristique selon laquelle si un client continue de parler de Netflix et de demander des microservices, il sait que l'engagement est en difficulté. Il est presque certain qu'ils ne passent pas aux microservices pour la bonne raison. Si la conversation est un peu plus profonde et couvre des choses comme le couplage et la cohésion, alors il sait qu'ils sont dans le bon esprit.
L'ambition de départ d'une transformation des microservices ne devrait jamais être les microservices eux-mêmes. Les microservices sont le moyen d'atteindre un objectif de niveau supérieur d'agilité ou de résilience de l'entreprise ou équivalent. En fait, les microservices ne sont même pas les seuls moyens ; ils sont un moyen.
Monolithe distribué
Il est important de se demander "avez-vous des microservices, ou avez-vous un monolithe réparti sur des centaines de dépôts Git ?" C'est malheureusement ce que l'on voit souvent. C'est un monolithe distribué, et c'est une chose terrible. C'est difficile de raisonner dessus. Il est plus sujet aux erreurs que son équivalent monolithique. Avec un monolithe conventionnel où tout est contenu dans un environnement de développement unique, vous bénéficiez d'avantages tels que la vérification au moment de la compilation et la prise en charge de la refactorisation par l'IDE. Parce que vous exécutez toujours dans un seul processus, vous obtenez l'exécution garantie de la fonction. Vous n'avez pas à vous soucier de vous souvenir des sophismes de l'informatique distribuée et de la découverte de services et de la gestion des cas où la chose que vous essayez d'appeler a cessé d'exister. Les choses sont plus sûres. Si, d'un autre côté, nous enlevons la sécurité du monolithe mais quittons le couplage, nous nous retrouvons avec des spaghettis cloud-native.
Distribué n'est pas équivalent à découplé
Il y a quelques années, j'ai été appelé dans un projet troublé pour une mission de sauvetage. Quand je suis arrivée, l'une des premières choses que l'équipe m'a dites était "à chaque fois que nous changeons un microservice, un autre casse". Si vous avez prêté attention à la promesse des microservices, vous savez que c'est exactement le contraire de ce qui est censé se produire. Les microservices sont censés être indépendants les uns des autres, découplés. Cependant, le découplage n'est pas gratuit si vous distribuez votre système. Tout simplement parce que "distribué" et "découplé" commencent tous les deux par D, ce n'est pas la même chose.
Il est tout à fait possible d'avoir un système hautement distribué avec toute la douleur qui vient sur le fait d'être distribué tout en étant toujours entièrement intriqué et couplé. C'est ce qui s'était passé dans ce cas. Lorsque j'ai commencé à explorer la base de code, je revoyais le même code dans chaque référentiel. Le modèle objet de l'application était assez élaboré. Il y avait environ 20 classes, et certaines de ces classes avaient 70 champs. C'est un schéma complexe.
L'un des principes du développement des microservices est de laisser tomber DRY et d'éviter les bibliothèques communes, car elles sont source de couplage. Dans ce cas, pour éviter le couplage d'une bibliothèque d'objets centrale, chaque microservice disposait d'un copier-coller du modèle objet dans son code. Mais si le schéma de domaine est toujours partagé, il y a toujours couplage. La duplication du code des objets n'élimine pas le couplage, elle supprime simplement la possibilité de vérification au moment de la compilation. Si un nom de champ change, cela casse toujours tout le monde, mais la rupture ne se produit qu'au moment de l'exécution.
Cette triste histoire démontre l'importance des principes de conception basés sur le domaine dans les microservices. L'idéal que nous recherchons est que chaque microservice corresponde parfaitement à un domaine. Un effet secondaire de cela, et un signe que vous le faites bien, est que les interfaces avec vos microservices sont petites. Si nous divisons selon des frontières techniques, plutôt que selon des frontières de domaine, nous nous retrouvons avec une situation comme celle que j'ai vue ; chaque microservice avait une interface énorme et fragile. Le résultat est un désordre de spaghetti fragmenté.
Le Mars Climate Orbiter
Bien qu'il s'agisse techniquement d'un vaisseau spatial, plutôt que d'une plate-forme de microservices, le Mars Climate Orbiter montre bien la distinction entre être distribué et être découplé. La NASA a lancé Mars Climate Orbiter en 1998, avec pour mission d'étudier le climat martien. Malheureusement, l'Orbiter n'a pas réussi à orbiter autour de Mars ; au lieu de cela, la sonde s'est écrasée sur Mars. L'autopsie de la NASA a révélé que le problème provenait de la relation entre deux systèmes de contrôle différents, construits par des équipes différentes. La plupart du temps, la direction était assurée par un système sur l'Explorer lui-même. Tous les quelques jours, lorsque l'Orbiter arrivait en vue de la Terre, un système de contrôle de supervision en Floride envoyait des corrections de cap. C'est à peu près aussi distribué qu'un système peut l'être ; une partie était dans l'espace. Mais le domaine est en fait similaire entre ces deux systèmes : tous deux traitaient des calculs de poussée moteur. Les deux équipes n'avaient pas été assez claires dans leur communication sur ce à quoi ressemblait l'interface, et elles ont donc fini par utiliser des unités différentes. La partie dans l'espace utilisait des unités métriques, la partie sur Terre utilisait des unités impériales, donc un désastre s'est produit. Nous pouvons dire en toute sécurité que dans ce cas, le système était très distribué, et être distribué n'a pas aidé.
Tests de contrat axés sur le consommateur
Ce genre de problème de communication subtil se produit tout le temps lorsque plusieurs équipes sont impliquées. Heureusement, il existe une bonne atténuation : les tests de contrats axés sur le consommateur. Dans les systèmes où l'IDE ne nous aide pas avec les vérifications de type, nous devons tester nos intégrations, mais nous voulons limiter au minimum les tests d'intégration complets. Les tests d'intégration sont lourds, coûteux à exécuter, fragiles et intrinsèquement couplant. Si nous avons investi dans le développement de microservices, nous ne voulons pas revenir en arrière et créer un gros monolithe intégré au moment du test. Alors, comment pouvons-nous avoir confiance que nous construisons quelque chose qui fonctionne réellement ?
Les mocks sont une solution courante, mais les mocks en eux-mêmes ont un problème. Pour mettre en place des mocks, l'équipe de production et l'équipe de consommation ont une conversation au début du développement sur ce à quoi ressemble l'interface. Ils parviennent à un accord, et les consommateurs s'en vont et essaient d'écrire un mock qui ressemble à leur compréhension de ce à quoi l'équipe de production a dit que leur code ressemblait. Dans le cas idéal, ils réussissent. Le problème est qu'ils intègrent leurs hypothèses dans le mock parce qu'ils écrivent le mock, et ils ne sont peut-être pas la meilleure personne pour savoir à quoi ressemble l'autre code ou comment il se comporte, car ce n'est pas leur code.
Dans le cas heureux, ils réussissent. Les tests unitaires passent tous, et les choses continuent de passer dans la phase d'intégration. Tout est bon. Malheureusement, ce n'est pas toujours ce qui se passe. Parfois, la mise en œuvre réelle finit par être différente de ce que l'équipe consommatrice avait compris, soit parce que l'équipe productrice a changé d'avis, soit parce que quelqu'un quelque part a fait une hypothèse qui était incorrecte. Dans ce cas, les tests passeront quand même. Cependant, lorsque nous intégrons réellement les vrais services, cela échouera. Le problème est que le comportement du mock n'est pas validé par rapport au vrai service. L'équipe de production ne verra probablement même jamais les mocks qui ont été créées.
Une meilleure option est d'avoir un test de contrat axé sur le consommateur. La beauté d'un test de contrat, et pourquoi il est différent d'un mock, est que les deux parties interagissent avec le test de contrat. Pour le consommateur, le test du contrat agit comme un mock pratique.
D'autre part, le test de contrat sert de test fonctionnel pratique pour le fournisseur. C'est une validation plus approfondie qu'une simple vérification OpenAPI pour la syntaxe. Un test de contrat vérifie également la sémantique et le comportement. Cela fait gagner du temps à l'équipe chargée de rédiger des tests fonctionnels.
Si tout est compatible et fonctionnel, tous les tests contractuels passent. C'est un regain de confiance rapide, car ils sont bon marché et légers à utiliser. Si l'équipe qui fournit casse quelque chose, ses tests échoueront et fourniront une alerte précoce, avant que le changement cassant ne s'échappe dans l'environnement d'intégration. Si l'API change, une nouvelle version du contrat est déployée des deux côtés (ou à un broker de connexion).
Il existe plusieurs systèmes de tests contractuels différents. Spring Contract fonctionne très bien si vous êtes dans l'écosystème Spring. Si vous êtes un peu plus polyglotte, alors j'aime beaucoup Pact. Il a des liaisons pour presque toutes les langages que vous pourriez utiliser.
La boule de poils de l'entreprise (Enterprise Hairball)
Bien sûr, même si nous organisons tous nos tests, et même si nous avons un bel ensemble de microservices découplés au niveau de la couche métier, le succès n'est pas garanti. Il y aura de nombreux autres éléments dans notre système que nous n'avions peut-être pas pris en compte lorsque nous avons élaboré notre architecture de microservices vraiment propre. Nous sommes vraiment enthousiasmés par la couche métier, et nous oublions le recto et le verso, et toute la colle. La colle est particulièrement probable, et collante, dans les architectures d'entreprise. L'un de nos architectes appelle cela la boule de poils de l'entreprise.
Si nous concentrons tous nos efforts de décomposition fonctionnelle sur la couche métier, nous nous retrouvons souvent avec un tas de microservices parfaitement découplés, pris en sandwich entre un frontal monolithique et une couche de base de données monolithique. Le changement sera difficile dans ce genre de systèmes. Cependant, en tant qu'industrie, nous nous améliorons dans la décomposition des bases de données afin qu'elles correspondent à des microservices individuels, et nous développons des micro-front-ends.
Mais nous n'en avons pas fini avec la décomposition. Si le système n'est pas trivial, nous aurons une couche d'intégration. Il peut s'agir de messagerie ou d'une autre solution d'intégration qui rassemble le système complexe. Même après la modernisation du reste de l'architecture, la couche d'intégration est souvent encore monolithique et inflexible. L'équipe elle-même peut être sous une charge importante - un « sandwich paniqué» comme l'a dit mon collègue. Parce que la couche d'intégration est monolithique, ils doivent planifier soigneusement tous les changements, ce qui bloque tout le monde.
Cela peut causer beaucoup de frustration, en particulier pour l'équipe d'intégration. De l'extérieur, ils peuvent sembler insensibles et lents, même s'ils travaillent dur. Pour déméler le couplage, nous devons adopter des patterns d'intégration modulaires.
Que se passe-t-il si nous ne découpons pas les couches d'intégration, de base de données et front-end ? Il est presque certain que nos microservices n'atteindront pas ce que nous voulons. Les dépendances entre les parties de la boule de poils empêcheront toute partie de progresser rapidement. Les microservices de la couche métier ne pourront pas être déployés indépendamment et le rythme de déploiement sera décidément non continu.
Des freins qui entravent les releases
Combien d'entre vous reconnaissent ce scénario ? Vous travaillez très dur, vous avez créé quelque chose d'incroyable. Vous savez que les utilisateurs vont l'adorer, mais ce n'est pas encore entre leurs mains. La valeur est mise sur l'étagère, et votre chose incroyable ne peut pas être publiée. Même si vous avez une architecture de microservices, vous avez également un comité de release. Tous les autres microservices doivent être publiés simultanément, car ils doivent être testés ensemble et c'est trop cher de le faire, sauf dans un gros lot. Même remplir la liste de contrôles de releases coûte cher. L'entreprise a peur de publier, car elle a été échaudée par des releases de mauvaise qualité dans le passé. La liste de contrôle de releases et le plan de releases, ainsi que les tests à un seul thread et les autres incantations de release sont toutes des tentatives pour réduire le risque perçu. Étant donné que les délais de publication sont communs à toute l'organisation, nous finissons par devoir nous précipiter pour intégrer des fonctionnalités avant la date limite. Cela, bien sûr, rend la publication plus risquée. Quelqu'un quelque part suit un tableaur avec toutes les dépendances entre les microservices qui sont plus couplées qu'elles ne devraient l'être. Et, bien sûr, la lune doit être dans la bonne phase. Ce n'était pas ce à quoi nous nous sommes inscrits lorsque nous avons choisi les microservices ! Tous ces processus bien intentionnés sont des freins qui empêchent la valeur d'atteindre les utilisateurs et augmentent souvent les risques.
L'automatisation des tests
Comment est-ce arrivé ? Habituellement, la raison pour laquelle nous avons si peur de publier est qu'il y a une tonne de travail manuel impliqué dans une release. En particulier, les tests qui nous donnent réellement confiance ne sont pas automatisés, nous devons donc même faire beaucoup de travail pour déterminer si notre application fonctionne. Lorsque je rends visite à un client et que j'entends "nos tests ne sont pas automatisés", ce que j'entends est "nous n'avons aucune idée si notre code fonctionne pour le moment". Cela peut fonctionner. Cela a fonctionné la dernière fois que nous avons effectué une QA manuelle ; nous espérons que cela fonctionne toujours. C'est une situation triste.
Si vous vous souciez de cela, automatisez les - et la qualité est quelque chose dont vous devriez vous soucier. Surtout si l'architecture a dérivé vers des spaghettis et que le couplage s'est insinué, des cassures sont probables. La dé-spaghettification est difficile, nous voulons donc être dans une boucle de rétroaction rapide, où nous détectons les cassures le plus tôt possible. Si vous allez être des spaghettis, testez au moins les spaghettis.
Le cycle de releases
Les tests manuels ne sont qu'une partie du processus manuel impliqué dans une release. Dans les industries réglementées ou axées sur la conformité, il y a presque toujours une pile de travaux manuels de conformité. La conformité est quelque chose qui nous tient à cœur - nous devrions donc l'automatiser.
Avec tous ces processus manuels et tous ces ralentissements, cela signifie vraiment que même si nous déployons dans le cloud, nous n'obtenons pas la promesse du cloud. Nous utilisons le cloud comme si ce n'était pas un cloud. L'ironie est que dans le cloud, les choses que nous avions l'habitude de faire, qui étaient autrefois une bonne idée, qui nous protégeaient, nous nuisent en fait. La gouvernance à l'ancienne dans le cloud ne fonctionne pas. Il n'atteint pas les résultats métiers que nous espérions et perd une grande partie des avantages métiers du cloud.
Il est facile de déterminer si une entreprise tient la promesse du cloud en examinant les cycles de releases. Il y a quelques années, un de mes collègues a eu un appel commercial avec une grande banque traditionnelle. Leur déjeuner était mangé par les fintechs et les banques challengers nouvellement venues. L'entreprise pouvait comprendre pourquoi elle perdait : elle ne pouvait pas agir assez rapidement pour suivre le rythme. Ils sont venus nous voir et nous ont expliqué qu'ils disposaient d'un grand parc COBOL, et que c'était ce qui les ralentissait. (C'était très probablement vrai.) Ils ont ensuite ajouté qu'ils avaient clairement besoin de se débarrasser de tout ce COBOL et de passer aux microservices parce que tout le monde faisait des microservices. Ensuite, ils ont ajouté que leur comité de releases ne se réunissait que deux fois par an. À ce moment-là, le cœur de mon collègue s'est serré. Si votre comité de releases ne se réunit que tous les six mois, vous savez que votre cadence de release sera tous les six mois. Peu importe le nombre de microservices déployables indépendamment dont vous disposez. Vous n'obtiendrez pas l'agilité.
L'aide dont cette banque avait besoin n'était pas vraiment une aide technique ; ils devaient changer leur façon de penser le risque et la façon dont ils effectuaient les opérations, et leur planification des releases avait besoin d'une refonte complète, et ils avaient besoin de tout un tas d'automatisation. C'est le manque de discipline en matière de livraison continue qui les retenait, pas le COBOL.
"Je veux être décomposé" est une demande courante des clients, mais décomposé a plus d'un sens. Lorsqu'on souhaite une application décomposée, cela ne garantit pas la modularité. Parfois, cela signifie simplement que le désordre se propage plus largement. S'il y a des contraintes externes, comme des plans de releases et des processus obsolètes, qui nous font reculer, jusqu'à ce que nous les corrigions, peu importe à quel point nous sommes décomposés.