Le coût des microservices (2020)
(robertovitillo.com)- Les backends démarrent généralement sous forme de monolithe, mais lorsque plusieurs équipes fonctionnelles commencent à travailler sur la même base de code, le couplage peut augmenter et le rythme des changements ralentir
- Les microservices consistent à découper les services par fonctionnalité métier afin d’obtenir un déploiement indépendant et l’autonomie de petites équipes
- Si les services sont trop petits, la charge opérationnelle peut dépasser les bénéfices, et le terme « micro » peut même brouiller le jugement
- Une adoption complète s’accompagne de coûts importants : standardisation, provisionnement des ressources, protection des appels distants, CI/CD, opérations, jusqu’à la cohérence éventuelle
- En général, il est plus sûr d’apprendre suffisamment les frontières au sein d’un monolithe, puis d’en extraire les parties une par une lorsque les douleurs de croissance deviennent fortes ; une approche microservices-first convient lorsque l’expérience et la plateforme sont prêtes
Les limites qui apparaissent quand un monolithe grossit
- Le backend d’une application JavaScript moderne de type single-page commence généralement comme un service web unique et sans état, qui fournit une API HTTP RESTful et utilise une base de données relationnelle
- Ce service est composé de composants ou de bibliothèques qui implémentent différentes fonctionnalités métier
- Plus le nombre d’équipes fonctionnelles contribuant à la même base de code augmente, plus le couplage entre composants tend à s’accroître et plus les zones de travail des équipes risquent de se chevaucher
- La productivité baisse et il devient difficile de prévoir le périmètre d’impact des changements
- Quand la base de code devient suffisamment complexe, plus personne ne la comprend entièrement, ce qui allonge le temps nécessaire pour implémenter de nouvelles fonctionnalités et corriger les bugs
- Même si le backend est divisé en bibliothèques par équipe, une modification de bibliothèque finit tout de même par entraîner un redéploiement du service
- Si un bug comme une fuite mémoire est introduit, tout le service peut être affecté
- Revenir en arrière après un mauvais build affecte la vitesse de toutes les équipes, pas seulement celle qui a introduit le bug
L’autonomie apportée par les frontières de services
- Une manière de réduire les douleurs de croissance d’un backend monolithique consiste à le diviser en un ensemble de services déployables indépendamment
- Les services communiquent via des API, et ces API créent des frontières plus difficiles à franchir que les limites entre composants au sein d’un même processus
- Cette architecture est appelée architecture microservices, mais le terme
micropeut prêter à confusion- Les services n’ont pas nécessairement besoin d’être petits
- Un service qui offre trop peu de fonctionnalités peut créer plus de charge opérationnelle que de bénéfices
- Un nom plus approprié pourrait être service-oriented architecture, même si ce terme porte lui aussi un héritage ancien
La vitesse créée par les petites équipes et les services indépendants
- Découper le backend en services par fonctionnalité métier permet à chaque service d’être développé et opéré par une petite équipe unique
- Les petites équipes sont plus efficaces, car le coût de communication augmente au carré avec la taille de l’équipe
- Lorsque chaque équipe définit son propre calendrier de release et contrôle sa base de code, la coordination entre équipes diminue et les décisions sont prises plus vite
- Une base de code de service plus petite est plus facile à comprendre pour les développeurs et réduit aussi le temps d’adaptation des nouvelles recrues
- Une petite base de code limite les ralentissements de l’IDE, ce qui aide aussi la productivité des développeurs
- Grâce à des frontières de services fortes, les développeurs n’ont besoin de comprendre qu’une petite partie du backend, et non l’ensemble, lorsqu’ils en modifient une portion
- Chaque service peut scaler indépendamment et choisir une stack technique adaptée à ses propres besoins
- Les consommateurs de l’API n’ont pas à se préoccuper de la façon dont la fonctionnalité est implémentée en interne
- Il est plus facile d’expérimenter et d’évaluer de nouvelles technologies sans affecter d’autres zones du système
- Chaque microservice peut avoir son propre modèle de données et son propre stockage
- Les développeurs peuvent modifier le schéma sans affecter les autres services
Les coûts réels ajoutés par les microservices
- Les microservices ajoutent davantage de pièces mobiles à l’ensemble du système, et ce coût n’est pas gratuit
- Le coût d’une adoption complète des microservices ne vaut la peine que s’il peut être amorti de manière distribuée sur des dizaines d’équipes de développement
-
Expérience développeur
- Il est possible d’utiliser des langages, bibliothèques et stockages de données différents pour chaque service, mais cela peut rendre la maintenance de l’application difficile
- Si les stacks logicielles sont complètement différentes, il devient difficile pour un développeur de passer d’une équipe à une autre
- Pour fournir des fonctionnalités communes nécessaires à tous les services, comme la journalisation, il faut maintenir des bibliothèques pour chacun des langages adoptés
- Un certain niveau de standardisation est nécessaire
- On peut encourager de manière souple l’usage de certaines technologies en offrant une bonne expérience développeur aux équipes qui suivent le portefeuille recommandé de langages et de technologies
-
Provisionnement des ressources
- Pour prendre en charge de nombreux services indépendants, il faut pouvoir créer facilement des serveurs, des stockages de données et d’autres ressources génériques
- Il ne faut pas laisser chaque équipe inventer sa propre manière de créer des ressources
- Les ressources provisionnées doivent ensuite être configurées, ce qui exige une automatisation importante
-
Communication
- Les appels distants sont coûteux et introduisent de nouveaux modes de défaillance du système
- Pour se préparer aux pannes, il faut des mécanismes de défense comme les timeouts, les retries et les circuit breakers
- Pour réduire la perte de performance due aux communications réseau, il faut aussi exploiter le traitement asynchrone et le batch
- Ces éléments augmentent la complexité du système
- Les monolithes peuvent connaître des problèmes similaires à cause de l’accès de clients distants et de l’utilisation d’API tierces, mais à une échelle plus réduite
-
Intégration, livraison et déploiement continus
- L’intégration continue garantit que les changements de code sont fusionnés dans la branche principale après l’exécution de builds et de tests automatisés
- Une fois les changements de code fusionnés, ils doivent être publiés et déployés automatiquement dans un environnement proche de la production
- Dans cet environnement, des tests d’intégration et de bout en bout sont exécutés pour vérifier que les microservices ne cassent pas les autres services qui dépendent d’eux
- Tester un microservice individuel n’est pas plus difficile que tester un monolithe, mais les tests d’intégration de tous les microservices sont beaucoup plus difficiles
- Lorsque les services individuels interagissent, des comportements très subtils et inattendus peuvent apparaître
-
Opérations
- Contrairement à un monolithe, affecter une équipe d’exploitation distincte à chaque équipe responsable d’un service coûte beaucoup plus cher
- L’équipe qui développe un service assure généralement aussi l’astreinte de ce service
- À chaque sprint, l’équipe doit décider si elle donne la priorité au développement ou à la charge opérationnelle, ce qui crée des frictions
- Le débogage des pannes système devient aussi plus difficile
- Il n’est pas possible de lancer toute l’application sur une machine locale et de l’exécuter pas à pas avec un debugger
- Avec plus de pièces mobiles, il existe aussi davantage de modes de défaillance
- Une bonne journalisation et un bon monitoring à tous les niveaux deviennent essentiels
-
Cohérence éventuelle
- Lorsque l’application est divisée en plusieurs services, le modèle de données n’existe plus uniquement dans un stockage de données unique
- Mettre à jour atomiquement des enregistrements situés dans différents stockages et garantir une cohérence forte est lent, coûteux et difficile à implémenter
- Ce type d’architecture doit généralement accepter la cohérence éventuelle
Quand faut-il séparer ?
- Découper une application en services augmente fortement la complexité globale du système
- En général, il vaut mieux commencer par un monolithe et ne le diviser que lorsqu’il existe une raison suffisante de le faire
- Comme il est difficile de définir correctement les frontières entre services, il est plus simple de déplacer les frontières à l’intérieur du monolithe pour trouver les bons points de découpage
- Lorsque le monolithe est suffisamment mature et que les douleurs de croissance commencent à devenir importantes, on peut extraire les microservices un par un
- Une approche microservices-first ne devrait être lancée que si l’on dispose déjà d’expérience et que l’on a construit la plateforme nécessaire, ou que l’on a pris en compte le temps nécessaire pour la construire
1 commentaires
Avis sur Hacker News
Je suis globalement d’accord avec l’article, mais à l’inverse, il y a aussi des cas où le coût peut se justifier : on peut isoler dans un service séparé les problèmes de compatibilité avec d’anciennes dépendances tierces et éviter ainsi une refactorisation douloureuse ou une réimplémentation maison ; empêcher que les contraintes de déploiement d’un système critique à haute disponibilité ne bloquent les déploiements d’autres systèmes ; ou encore isoler des contraintes de conception qu’un gros client ne peut pas changer.
Ça ne veut pas dire que c’est gratuit, mais que c’est un coût qui peut valoir la peine pour éviter de transformer un monolithe simple en tas d’ordures, de le fragiliser avec des déploiements risqués, ou de se retrouver lié à un partenaire commercial doté d’une pile technique encore pire.
Les microservices, ce n’est pas plutôt une approche où les composants internes sont par défaut des services, plutôt que des bibliothèques ou des classes ?
Si l’on écrit un service d’inférence de machine learning en Python, une UI rendue côté serveur en Rails, et un service très orienté entrées/sorties et concurrence en Go, le surcoût consistant à les séparer en trois services peut se justifier.
J’aime plutôt les microservices, mais l’optimisation prématurée et le passage à l’échelle prématuré peuvent être aussi néfastes que la dette technique créée lorsqu’il faut finalement optimiser dans une direction complètement différente.
Le moment d’extraire quelque chose en service séparé, c’est quand un problème apparaît et qu’il ne peut pas être résolu de manière raisonnable autrement.
Accroître la complexité architecturale pour forcer des frontières ne peut pas être une solution au manque de discipline organisationnelle, même si les entreprises tech de taille moyenne la traitent constamment comme telle. Si des frontières de domaine faibles posent problème, extraire des services a de fortes chances de ne pas vraiment résoudre le problème.
Citation : https://www.oreilly.com/library/view/software-architects-han...
La CI/CD qui bloque une PR tant que les tests ne passent pas est aussi une solution technique coûteuse à un problème de discipline organisationnelle.
Tout le monde reconnaît qu’il est pertinent que la résolution de noms de domaine soit assurée par un service externe, et presque personne n’intègre un résolveur DNS récursif et son cache dans un monolithe. Pourtant, cette séparation de responsabilités, pourtant ancienne, est rarement acceptée comme exemple.
Le problème semble être que plus un employé est « bon », plus il a envie de montrer sa valeur, ce qui conduit à la surconception, et finit par devenir un enfer.
Microservices
grug se demande pourquoi les gros cerveaux prennent le problème le plus difficile, celui de décomposer correctement un système, et y ajoutent en plus des appels réseau.
pour grug, cela semble très déroutant.
https://grugbrain.dev/#grug-on-microservices
« Pour des raisons SCALE_REASONS, nous voulons moderniser notre approche de FOO_TABLE en passant de MySQL à DynamoDB, mais 32 des 59 équipes accèdent directement à FOO_TABLE ou appellent directement des méthodes private de nos classes. Leurs priorités étant différentes, elles ne peuvent pas migrer vers FOO_SERVICE ni modifier leurs requêtes pour utiliser des tables shardées. Désormais, faire évoluer FOO_TABLE devient un chantier sur plusieurs trimestres, et il faut laisser aux équipes le temps de se mettre à jour progressivement. Dans un an ou deux, on pourra peut-être retirer l’ancienne approche qui est actuellement en feu. En attendant, profitez de l’astreinte. »
Avec des microservices, l’équipe se rend compte que la table ne passe pas à l’échelle, mais les données sont fournies via une API. Elle planifie et exécute la migration au sprint suivant, et les utilisateurs de l’API ont simplement l’impression que tout est devenu beaucoup plus rapide.
En ouvrant le REPL d’un Ruby moderne, on obtient déjà une bonne partie de ce qu’on attendrait d’un IDE pour langage statiquement typé en matière d’autocomplétion et de vérification syntaxique.
L’isolation n’est pas à prendre à la légère : c’est l’une des fonctionnalités les plus puissantes que l’on puisse ajouter à un logiciel. Elle peut améliorer les performances, créer des limites de défaillance et fournir des frontières de sécurité.
C’est le même concept de base que dans le modèle d’acteurs. Au lieu que deux composants partagent et modifient la mémoire l’un de l’autre, deux systèmes isolés, comme des acteurs ou des microservices, ne communiquent que par un protocole défini.
Je pense que beaucoup comprennent mal la modularité. La modularité est importante, mais j’en suis venu à penser qu’il existe un autre principe architectural important : le principe du tourbillon unique.
Dans un système logiciel, un tourbillon est une boucle dans le flux de données. Si vous envoyez des données quelque part et récupérez le résultat traité, ou si vous êtes affecté d’une manière ou d’une autre par ces données, il y a un tourbillon. Une variable mutable est un exemple de tout petit tourbillon.
Le principe du tourbillon unique signifie qu’idéalement, un système logiciel ne devrait avoir qu’un seul tourbillon, ou, autrement dit, que tous les composants devraient savoir dans quel sens tourne leur propre tourbillon.
Quand on fusionne des modules qui forment deux tourbillons, si leurs directions correspondent, la composition est facile ; si elles sont opposées, c’est plus délicat, car il faut décider du sens du nouveau tourbillon. Il vaut donc mieux que les tourbillons de tous les modules aient le même sens et n’en forment qu’un seul.
Ce principe généralise des idées comme le pattern Flux, CQRS, l’event sourcing et l’immutabilité.
Ou bien un événement inattendu dans A peut bloquer C. Idéalement, un parent ne devrait appeler que ses enfants et ne pas dépendre d’eux ; si l’on échoue à cela, l’architecture a elle aussi échoué.
Quand on regarde un diagramme d’architecture, la moitié du travail consiste à comprendre ce qui est asynchrone/synchrone, push/pull, ainsi que la direction et l’ordre des flux.
Les bus de services d’entreprise et les files de messages ressemblent à des solutions pour créer un tourbillon là où les deux directions ne s’accordent pas.
Il y a un passage qui dit que le problème est « le nombre de bibliothèques à maintenir — une par langage adopté — pour fournir les fonctionnalités communes nécessaires à tous les services, comme la journalisation ».
C’est la principale raison pour laquelle nous avons abandonné les microservices. Avec l’outillage de 2023, surtout dans des environnements purement cloud native ou FaaS à mise à l’échelle infinie, consacrer de la bande passante mentale à ce genre de préoccupation est un pur gaspillage. C’est encore plus vrai avec des clients comme les banques et les institutions financières, qui passent au crible chaque dépendance tierce.
Aujourd’hui, nous exploitons une distribution binaire .NET monolithique unique d’environ 250 Mo gzipée, et il n’y a aucun signe de fissure. Si, avec une distribution SaaS de 10 à 100 Mo, vous vous inquiétez minutieusement du fait que « mon exe ne tient plus dans le L2 », vous pouvez vous rassurer : votre voyage dans le logiciel monolithique n’a même pas encore commencé.
Si vous devez un jour réécrire l’un de ces tas de déchets, n’est-il pas beaucoup plus simple que tous les commits soient en un seul endroit, globalement cohérents ?
Microservices ne veut pas dire Far West où chaque équipe peut agir sans tenir compte de l’ensemble de l’organisation. Il peut, et il doit, y avoir des règles pour maintenir un certain niveau de cohérence entre les équipes.
Le meilleur code est celui qu’il est facile de jeter et de réécrire. Dans la pratique, le vieux code passe entre davantage de mains, se dégrade, et surtout finit par ne plus être maintenu par personne avec envie.
Les partisans des microservices disent qu’il est plus facile de modifier ou de réécrire un service parce qu’il existe un contrat clair sur son comportement attendu et que sa base de code est beaucoup plus petite.
Les partisans du monolithe disent qu’il est plus facile de modifier ou de réécrire parce que tout forme une grosse pelote, et que pour changer un fil il suffit de savoir où il s’entrelace avec les autres.
Je ne sais pas qui a raison. Pour les anciens qui ont passé des années à examiner la base de code au microscope, le monolithe est probablement préférable ; pour les autres, les microservices le sont peut-être.
Du point de vue de la gestion des releases, passer d’un monolithe aux microservices se fait souvent pour de mauvaises raisons.
La seule raison réellement valable de changer me semble être un problème de scalabilité dû à un goulot d’étranglement de performance. Le reste ne fait que déplacer la complexité du développement logiciel vers la maintenance système.
Bien sûr, les développeurs apprécieront d’avoir moins de contraintes fortes de coordination avec les autres équipes. Mais clarifier quand et comment les fonctionnalités sont implémentées, correctement testées à travers les microservices, activées en production et pilotées de manière centralisée devient beaucoup plus difficile si la communication entre équipes de développement n’est pas suffisamment mature. Et cette immaturité est souvent la vraie raison pour laquelle on découpe le monolithe au départ.
À l’inverse, avec peu de développeurs et un contexte bien défini, gérer plusieurs microservices est pénible.
Dire que « le reste ne fait que déplacer la complexité du développement logiciel vers la maintenance système » paraît plausible si le logiciel est en développement actif. Le développement coûte cher. Le coût de maintenance d’un système distribué peut être inférieur au coût de développement d’un énorme monolithe par une grande équipe ; au final, tout dépend du contexte.
Où est l’article « le coût du monolithe » ? Je ne le vois pas ici. On ne voit que ce problème parce que tout le monde implémente des microservices sans réfléchir
Si tout le monde implémentait des monolithes sans réfléchir, on verrait une quantité énorme d’articles « les monolithes, c’est mal »
Les gens ne comprennent pas que les deux systèmes finissent par créer la même quantité de problèmes. C’est la différence entre faire travailler un éléphant et faire travailler 1 000 souris. Les deux ont des problèmes, simplement des problèmes différents
Choisir l’un ou l’autre ne fait pas disparaître les problèmes. Dans tous les cas, il y aura des problèmes, et il faudra choisir une option et proposer des solutions. Si, dans une entreprise, « quelle architecture ? » est le plus gros obstacle, j’aimerais y travailler
Si un monolithe est bien décomposé, qu’est-ce qui le distingue de microservices colocalisés ? Probablement seulement l’interface. Un appel de fonction devient du RPC, et on accepte un peu d’overhead pour obtenir les avantages de traiter les composants individuellement, par exemple pour appliquer des correctifs
Qu’est-ce qui distingue des microservices distribués de microservices au même emplacement ? Le déploiement est plus complexe, mais on peut placer intelligemment les processus sur du matériel plus adapté. Les modes de défaillance se multiplient, mais on obtient une meilleure tolérance aux pannes
Il n’y a pas de réponse universelle. Si vous avez besoin des bénéfices, vous payez le coût. À mon avis, ces débats microservices contre monolithe viennent du fait qu’un pattern a été mal appliqué, que les outils qui simplifieraient la vie manquaient, ou, le plus souvent, que des logiciels vieux de 10 ans n’ont jamais été continuellement refactorisés et repensés, au point de devenir pénibles à faire évoluer indépendamment de leur architecture initiale
Dire « un appel de fonction devient du RPC » paraît simple, mais dès que c’est le cas, l’application cliente doit gérer tous les chemins d’exécution suivants : serveur DNS inaccessible, échec de résolution du nom d’hôte, hôte inaccessible, connexion refusée, authentification refusée, certificat TLS non fiable, limite de débit dépassée, 301 Moved Permanently, résultat pas encore prêt, délai d’attente dépassé
Même une application bien décomposée devra probablement être repensée pour gérer cela de façon robuste : files asynchrones, limitation du débit des requêtes, cache des résultats, retries avec backoff et journalisation, identifiants client, magasins de confiance et URL de ressources configurables, journalisation des échecs
Là où un appel de fonction local disposait de données dans le contexte, il faudra aussi ajouter des fonctions RPC et des paramètres pour les fournir. De plus, une UI monolithique devra parfois signaler à l’utilisateur des latences et des échecs réseau qui n’existaient pas auparavant
Même le meilleur monolithe demandera un effort conséquent pour être refactorisé en microservices
Peut-on relancer cet appel RPC en toute sécurité ? Et le backoff exponentiel ? Le load balancer d’API fonctionne-t-il correctement ? Dans ce cas, il faut aussi du monitoring. Comment tracer ce qui se passe entre microservices ? Il faut maintenant quelque chose comme OpenTelemetry
À quel point est-il plus difficile de déboguer avec des points d’arrêt dans une architecture de microservices ? Comment annuler une transaction de base de données entre deux microservices ? Un simple appel de fonction devient soudain beaucoup plus complexe
Chaque fois qu’on défend ce concept, on oublie de compter la fragmentation créée par l’exigence que les services soient très petits. Plus précisément, si vous avez un monolithe et un microservice, ce ne sont pas des microservices. Le terme microservices implique que tout est découpé en petits services et qu’il n’y a pas de monolithe
La plupart des arguments en faveur des microservices s’effondrent dès qu’on comprend qu’ils doivent nécessairement être « micro ». Et si l’on abandonne cette exigence, il ne reste plus rien de nouveau ni de profond dans la proposition
Du point de vue des opérations, dire que « le déploiement est plus complexe » est un énorme euphémisme. Chaque package est un objet à gérer séparément, avec ses propres caractéristiques et problèmes
Un peu comme les développeurs front-end qui pensent qu’on ne peut pas créer une « application web moderne » sans framework ou bibliothèque front-end comme React/Vue/Svelte, il semble aujourd’hui circuler l’idée qu’un « monolithe » équivaut à exécuter en une seule instance un énorme amas de goudron effrayant, et que donc « ça ne scale pas » ; ça n’a aucun sens
Autre chose que l’on observe : le volume total de code devient bien plus important, et la plupart des services se composent de 20 % de code métier/domaine et 80 % de gestion d’envoi et de réception de messages réseau. Peu importe à quel point on le cache, c’est bien là, et il faut gérer le réseau d’une manière ou d’une autre
Comme la folie du front-end, ce culte des microservices ne prendra fin que lorsque l’économie s’effondrera et qu’il n’y aura plus d’argent pour soutenir ces tours de Babel vouées à la ruine
Cela dit, les microservices ont aussi leur place. Ils sont pertinents dans certaines entreprises qui en tirent plus de valeur que leur coût
Il existe des avantages techniques, comme le scaling indépendant par service, mais le principal bénéfice est de définir des frontières de propriété claires et d’éviter un couplage excessif inutile
Si une petite équipe construit un seul produit, il est probable que le problème organisationnel résolu par les microservices n’existe pas encore, donc ils ne sont peut-être pas nécessaires. Dans ce cas, les microservices sont une optimisation prématurée, et on paie un coût pour résoudre un problème qui n’existe pas encore
Plus largement, la complexité du front-end est entièrement dans votre tête : https://bower.sh/front-end-complexity
Le texte semble partir du principe que l’organisation de développement est complètement dysfonctionnelle et hors de contrôle. Du genre : ne pas savoir quoi faire pendant un sprint, introduire en douce des bibliothèques tierces et des langages inconnus, ou déployer discrètement en production des changements non compatibles.
Si les développeurs ne sont pas idiots, les microservices semblent aussi devoir être plus faciles.
Je suis convaincu que, dans une équipe composée de développeurs intelligents et compétents, les microservices seraient plus faciles. Évidemment, dans ce cas, tout serait plus facile.