Architecture logicielle

Microservices : frontières de service, déploiement indépendant et coût réel

Intermédiaire13 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Les microservices échangent une complexité interne contre une complexité distribuée, et l'échange n'est rentable que si l'organisation a besoin de livrer de façon indépendante. Ce guide définit ce que le style exige réellement, montre comment trouver des frontières qui résistent aux vraies fonctionnalités, compare les styles de communication et dresse un bilan réaliste de la facture opérationnelle et du chemin de sortie d'un monolithe.

Ce que vous allez apprendre

  • Énoncer les propriétés qu'un système doit avoir avant de pouvoir honnêtement se dire microservices
  • Tracer les frontières de service à partir des capacités métier plutôt que des couches techniques
  • Choisir entre communication synchrone et communication par événements pour une interaction donnée
  • Estimer l'investissement opérationnel qu'exige une plateforme microservices avant de s'y engager
  • Planifier une extraction progressive depuis un monolithe avec l'approche strangler fig

On présente généralement les microservices comme une manière de construire du logiciel ; il vaut mieux les comprendre comme une manière d'organiser la livraison. La description technique (de petits services, chacun dans son propre processus, qui communiquent par le réseau) est la partie facile, et elle s'applique à quantité de systèmes qui n'en tirent aucun bénéfice. Ce qui rend le style intéressant, c'est une propriété sans rapport avec la taille : chaque service peut être modifié, testé, livré et restauré à sa version précédente sans se coordonner avec les équipes qui possèdent les autres.

Cette propriété coûte cher. Elle exige des pipelines automatisés par service, la tolérance aux pannes partielles chez chaque appelant, une observabilité qui traverse les processus et une conception des données où deux services ne touchent jamais les mêmes lignes. Les équipes qui adoptent le vocabulaire sans payer pour la propriété se retrouvent avec un monolithe distribué : tous les sauts réseau des microservices, toute la coordination de livraison d'un monolithe, et un débogage pire que dans l'un ou l'autre. Ce guide explique comment mériter honnêtement cette propriété, et dans quels cas un unique livrable bien modularisé est la meilleure réponse d'ingénierie.

Ce que le style exige réellement

Cinq engagements distinguent les microservices de « plusieurs applications qui se parlent ».

Une unité de déploiement par service, livrée à son propre rythme. Si livrer une modification du service de tarification impose une livraison synchronisée du service catalogue, les deux forment une seule unité de changement déguisée en deux conteneurs. La capacité à livrer indépendamment est le test décisif, et c'est elle qu'il faut mesurer.

Des données privées. Un service possède son état persistant et il est le seul processus à le lire ou à l'écrire. Les autres services ne voient cet état qu'à travers l'interface du service ou les données qu'il publie délibérément. C'est la règle la plus souvent enfreinte, et celle dont la violation est la plus difficile à réparer.

Un contrat qui survit à son implémentation. Les consommateurs dépendent d'une interface publiée (une API HTTP, un schéma de message), pas d'une bibliothèque partagée de classes d'entités. L'évolution rétrocompatible est une exigence de conception, pas une aspiration.

L'isolation des pannes. Tout appel distant peut être lent, échouer ou renvoyer quelque chose d'inattendu. Les appelants fixent des timeouts, plafonnent la concurrence et se dégradent proprement. Sans cela, un système de douze services disponibles chacun à 99,9 % est moins fiable que le monolithe qu'il a remplacé.

Une équipe qui en est responsable de bout en bout. Les mêmes personnes conçoivent, construisent, déploient et maintiennent le service. Avec la loi de Conway, c'est moins une préférence qu'une prédiction : une organisation dont les équipes sont découpées par couche technique produira des services découpés par couche technique, quoi qu'en dise le schéma d'architecture.

Trouver des frontières qui tiennent

Les mauvaises frontières sont l'échec caractéristique des projets microservices, et elles sont presque toujours tracées selon des lignes techniques : un « service base de données », un « service de validation », un « service d'orchestration ». Chacun d'eux participe à tous les cas d'usage : chaque fonctionnalité touche donc tous les services, et rien ne peut être livré seul.

Les frontières qui tiennent sont tracées autour des capacités métier : ce que fait l'entreprise, qui porte un nom, a un responsable et suit son propre cycle de vie. Commandes, tarification, stock, facturation, suivi des expéditions, identité client. Le vocabulaire et la technique de modélisation viennent du domain-driven design, où un bounded context est exactement l'unité qu'un service doit implémenter.

Trois tests pratiques aident à juger une frontière candidate :

TestQuestionÀ quoi ressemble une mauvaise réponse
Cohésion du changementQuand une fonctionnalité typique arrive, combien de services changent ?Presque tous, à chaque fois
Propriété des donnéesCe service peut-il répondre à ses requêtes principales depuis son propre stockage ?Il a besoin d'une jointure entre deux autres services
VocabulaireLes termes employés à l'intérieur ont-ils un sens unique et cohérent ?« Commande » désigne quatre choses différentes dans un seul service

Le test de cohésion du changement est le plus parlant, car il se mesure a posteriori. Prenez les vingt dernières fonctionnalités terminées et comptez combien de services chacune a touchés. Dans une distribution saine, la plupart des fonctionnalités atterrissent dans un ou deux services. Si la médiane est de cinq, les frontières sont mauvaises et ajouter des services ne fera qu'aggraver la situation.

Les frontières ont aussi une dimension temporelle. Le bon découpage pour un produit à 30 000 utilisateurs est rarement le bon à 3 millions, et il arrive qu'on fusionne des services. Traitez une frontière comme une hypothèse avec une date d'expiration plutôt que comme une décision définitive, et maintenez des interfaces étroites pour que fusionner deux services reste peu coûteux.

Des données par service, et que faire des requêtes

Donner à chaque service son propre schéma est simple jusqu'au premier rapport qui a besoin des données de quatre d'entre eux. Toutes les réponses possibles sont des compromis, et un mauvais choix ici est précisément ce qui ramène les équipes vers une base de données partagée.

La composition d'API. Un appelant (souvent la gateway ou un agrégateur dédié) interroge plusieurs services et fait la jointure en mémoire. C'est simple, correct et suffisant pour une poignée d'enregistrements. Cela se dégrade fortement dès qu'il faut filtrer ou trier entre plusieurs services, car on ne peut pas paginer une jointure que l'on calcule soi-même.

Les modèles de lecture répliqués. Chaque service publie des événements quand ses données changent ; les services intéressés en conservent une copie locale et dénormalisée, taillée pour leurs requêtes. Les lectures deviennent rapides et locales ; le prix à payer est la cohérence à terme et le code qui maintient la projection. C'est la forme générale du pattern CQRS décrit dans le guide sur les patterns de conception des microservices.

Un stockage analytique séparé. Le reporting et la business intelligence ne devraient jamais s'exécuter contre les services opérationnels. Diffusez les changements vers un entrepôt ou un data lake et laissez les analystes y faire toutes les jointures qu'ils veulent.

La règle qui garde tout cela gérable : un service peut publier ses données, mais il ne laisse jamais un autre service accéder directement à son stockage. Un réplica en lecture ouvert aux autres équipes n'est qu'une base de données partagée avec des étapes en plus : le schéma devient public, et il se fige.

Styles de communication

Deux familles, chacune adaptée à des interactions différentes.

La requête/réponse synchrone (HTTP ou gRPC) est le bon choix quand l'appelant ne peut pas continuer sans la réponse : vérifier qu'un client existe avant de créer une commande, valider un jeton. Elle est simple à appréhender et facile à tracer. Son danger est le couplage temporel (la disponibilité de l'appelant devient le produit de celle de chaque service de la chaîne) et une latence qui s'accumule à chaque saut. Gardez les chaînes peu profondes : deux niveaux de profondeur, c'est gérable ; cinq, c'est une stack trace distribuée qui n'attend que son timeout.

Les événements asynchrones publient un fait et laissent les parties intéressées réagir. « OrderPlaced » est émis une seule fois ; le stock, la facturation et les notifications font chacun leur part à leur propre rythme. L'émetteur ne sait pas qui écoute et ne s'en soucie pas : ajouter un consommateur ne demande aucun changement en amont, et un consommateur indisponible retarde le travail au lieu de le faire échouer. Les coûts sont la cohérence à terme, un débogage de bout en bout plus difficile et l'obligation pour les consommateurs de gérer les livraisons en double. Le fonctionnement des brokers, l'ordre des messages et les garanties de livraison sont traités dans patterns de messagerie et garanties.

Une heuristique utile : les appels synchrones pour les requêtes, les événements asynchrones pour les changements d'état qui se propagent. Les commandes qui doivent être confirmées à un utilisateur qui attend restent synchrones ; tout ce que l'utilisateur n'a pas besoin d'attendre devient un événement.

// Synchronous: the order cannot be priced without this answer.
CustomerTier tier = customerClient.tierFor(customerId);   // timeout: 300 ms, fallback: STANDARD

// Asynchronous: nobody waits for stock, invoicing or email.
events.publish(new OrderPlaced(order.id(), order.lines(), order.total(), Instant.now()));

Quel que soit le style, le versionnement compte davantage que le choix du protocole. Ajoutez des champs, ne les détournez jamais de leur sens ; tolérez les champs inconnus côté consommateur ; et gardez une ancienne forme de message lisible au moins aussi longtemps qu'il faut au plus lent des consommateurs pour se mettre à jour.

La facture opérationnelle

L'effort d'ingénierie qu'exige une plateforme microservices est régulièrement sous-estimé, car il n'apparaît pas sur le schéma d'architecture. Avant de vous engager, budgétez-le en entier.

  • Un pipeline par service. Build, tests, analyse de sécurité, déploiement dans chaque environnement, rollback. Multipliez par le nombre de services ; si créer un nouveau service coûte une semaine de plomberie de pipeline, les équipes éviteront d'en créer et entasseront les fonctionnalités dans les services existants. C'est pourquoi les pipelines de livraison continue sont un prérequis, pas un projet à mener après coup.
  • Traçage distribué et logs corrélés. Sans identifiant de trace propagé à chaque saut, diagnostiquer « le checkout est lent » revient à lire douze fichiers de logs à la main.
  • Métriques et alertes centralisées par service, avec des objectifs de niveau de service (SLO) qui décrivent le comportement visible par l'utilisateur plutôt que le CPU.
  • Plateforme d'exécution. Orchestration de conteneurs, découverte de services, gestion des secrets, rotation des certificats, politiques réseau.
  • Tests de contrat. Les tests de contrat pilotés par le consommateur interceptent le changement incompatible avant qu'il n'atteigne un environnement où son diagnostic coûte une journée.
  • Astreinte et runbooks par équipe, parce que l'équipe qui livre est celle que l'on réveille.

Un rapide test de bon sens : si l'organisation ne sait pas déployer son monolithe actuel en production à la demande, en moins d'une heure, avec des tests automatisés auxquels elle fait confiance, elle n'est pas prête à exploiter quinze services. Les microservices amplifient la maturité d'un processus de livraison ; ils ne la fournissent pas.

Quand le monolithe est la bonne réponse

Un unique livrable est le bon choix plus souvent que ne le laissent entendre les conférences. Il s'impose quand l'équipe est assez petite pour tenir dans un seul stand-up, quand le domaine n'est pas encore assez bien compris pour tracer des frontières stables, quand le trafic du produit croît uniformément plutôt que par points chauds, et quand la cohérence transactionnelle sur l'ensemble du modèle est une exigence réelle plutôt qu'une habitude.

La version à viser est le monolithe modulaire : un processus, un déploiement, mais des modules internes aux frontières appliquées. Chaque module a sa propre arborescence de packages, son propre schéma (ou au moins ses propres tables, sans clé étrangère entre modules), une interface publique réduite à une poignée de types et une vérification qui fait échouer le build lorsqu'un module accède aux entrailles d'un autre. Le système de modules de Java, des règles ArchUnit ou simplement des modules de build séparés font tous l'affaire.

PréoccupationMonolithe modulaireMicroservices
DéploiementUn pipeline, un artefactUn pipeline par service
Refactoring qui traverse les frontièresAssisté par le compilateur, quelques minutesLivraisons coordonnées, plusieurs semaines
CohérenceTransactions localesSagas et cohérence à terme
Mise à l'échelleToute l'application à la foisPar service
Rayon d'impact d'une panneTout le processusUn seul service, s'il est isolé
Autonomie des équipesTrain de livraison partagéIndépendante
DébogageUne seule stack traceTrace distribuée

Le monolithe modulaire est aussi le point de départ honnête d'un système que vous comptez découper plus tard, car les modules vous montrent où se trouvent les coutures. Découper un monolithe bien modularisé est un travail mécanique ; découper un monolithe enchevêtré relève de l'archéologie.

Migrer avec le strangler fig

La réécriture d'un grand système échoue pour des raisons bien connues : le métier continue de faire évoluer l'ancien système pendant qu'on construit le nouveau, et la bascule concentre un risque énorme en un seul point. L'approche strangler fig évite ces deux écueils en extrayant une capacité à la fois, en routant le trafic vers la nouvelle implémentation et en laissant l'ancien système rétrécir jusqu'à ce qu'on puisse le retirer.

  1. Placer une couche de routage en frontal

    Installez une gateway ou un reverse proxy entre les clients et le monolithe afin de pouvoir rediriger le trafic chemin par chemin sans toucher aux clients. Voir gestion d'API et gateways.
  2. Choisir une capacité qui vaut la peine d'être extraite

    Prenez-en une dont la frontière est nette, qui cause une vraie douleur (elle change souvent, ou ne se met pas à l'échelle comme le reste) et dont les données sont peu enchevêtrées. Ne commencez ni par la plus difficile ni par la plus centrale.
  3. Découper d'abord les données

    Identifiez les tables que possède la capacité, supprimez du monolithe les jointures et clés étrangères qui traversent les schémas, et remplacez-les par des appels ou des événements publiés. C'est généralement l'étape la plus longue, et celle qui décide du succès.
  4. Construire le service et faire tourner les deux

    Implémentez le nouveau service, puis exécutez-le à côté de l'ancien code (trafic miroir, ou double écriture avec comparaison) jusqu'à ce que les comportements concordent.
  5. Basculer le trafic, puis supprimer

    Déplacez la route, surveillez les métriques, gardez le rollback disponible pendant une semaine, puis supprimez l'ancien code. C'est en sautant cette suppression que les organisations finissent par maintenir deux implémentations pour toujours.

Suivez deux chiffres tout au long de la migration : la part du trafic servie par les services extraits, et le nombre de fonctionnalités qui exigent encore une modification du monolithe. Si le second ne baisse pas, l'extraction produit des services sans produire d'autonomie.

Erreurs fréquentes

Découper par couche technique. Un « service UI », un « service métier » et un « service de données » recréent les couches d'une seule application à travers le réseau, et garantissent que chaque fonctionnalité touche les trois.

Démarrer avec trop de services. Les équipes qui lancent un nouveau produit avec vingt services devinent des frontières avant d'avoir compris le domaine. Commencez par deux ou trois services à gros grain, ou par un monolithe modulaire, et découpez quand une contrainte précise l'exige.

Des bibliothèques partagées contenant de la logique métier. Un JAR commun qui contient l'entité Order lie tous les services à un même cycle de livraison. Partagez des utilitaires s'il le faut ; ne partagez jamais le modèle.

Des chaînes synchrones pour tout. Cinq services sur le chemin d'une requête multiplient la latence et divisent la disponibilité. Basculez le travail vers des événements partout où l'utilisateur n'a pas besoin d'attendre.

Aucune discipline de versionnement. Renommer un champ JSON casse des consommateurs que vous n'avez jamais rencontrés. Uniquement des changements additifs, avec des périodes de dépréciation explicites.

Ignorer la facture de la plateforme. Opter pour les microservices sans financer pipelines, traçage et astreinte produit des services que personne ne sait exploiter et que tout le monde a peur de déployer.

Points clés à retenir

  • La propriété qui définit les microservices est le déploiement indépendant ; si des services doivent être livrés ensemble, la distribution n'apporte rien et coûte très cher.
  • Tracez les frontières autour des capacités métier et validez-les en comptant combien de services une fonctionnalité typique touche.
  • Chaque service possède ses données en privé ; les requêtes transverses passent par la composition, des modèles de lecture répliqués ou un stockage analytique séparé, jamais par un schéma partagé.
  • Utilisez des appels synchrones quand l'appelant a besoin d'une réponse et des événements pour les changements d'état qui se propagent, et faites du versionnement des interfaces une préoccupation de conception à part entière.
  • L'investissement opérationnel (un pipeline par service, traçage, métriques, tests de contrat, astreinte) est le vrai coût, et il doit être financé avant le premier découpage.
  • Un monolithe modulaire apporte l'essentiel du bénéfice de conception sans la complexité distribuée, et constitue le meilleur point de départ pour une future extraction par strangler fig.

Sources et lectures complémentaires

Questions fréquentes

Quelle taille doit faire un microservice ?
La taille n'est pas le bon critère. Un service doit porter entièrement une capacité métier, données comprises, et rester assez petit pour que l'équipe qui en a la charge puisse le réécrire en quelques semaines si nécessaire. Compter les lignes de code ou imposer un nombre maximal de fichiers produit des services incapables de répondre à une question sans appeler quatre voisins, ce qui est bien pire qu'un service qui se trouve être gros.
Plusieurs microservices peuvent-ils partager une même base de données ?
Techniquement oui, et le résultat est un monolithe distribué. Chaque évolution de schéma devient une livraison coordonnée, la requête lente d'un service prive un autre de ses ressources, et plus personne ne sait qui écrit quoi. Si deux services ont réellement besoin de la même table, c'est un indice fort qu'ils n'en forment qu'un. Partagez plutôt les données via des interfaces publiées ou des modèles de lecture répliqués.
Les microservices rendent-ils un système plus rapide ?
Presque jamais à l'échelle d'une requête. Transformer un appel de méthode en mémoire en appel réseau ajoute sérialisation, transport et gestion des erreurs, soit généralement une milliseconde ou plus à chaque fois. Ce que les microservices apportent, c'est la mise à l'échelle indépendante des composants (les trois endpoints qui consomment le plus de CPU peuvent recevoir plus de capacité sans payer pour toute l'application) et la vitesse de livraison d'équipes qui ne font plus la queue les unes derrière les autres.
Le monolithe modulaire est-il un retour en arrière ?
C'est souvent la bonne destination. Un unique livrable aux frontières de modules strictement appliquées, avec un schéma par module et aucun SQL entre modules, offre l'essentiel du bénéfice de conception sans aucun coût de distribution. Il laisse aussi l'option ouverte : des modules aux frontières nettes pourront être extraits plus tard, lorsqu'une contrainte précise (mise à l'échelle indépendante, cadence de livraison distincte, technologie différente) le justifiera.