Chapitre 17
Les microservices

Diviser pour mieux régner.
Temps de lecture : 5 minutes


Dans les chapitres précédents, nous avons appelé "brique logicielle" un lot de fonctionnalités autour d'un domaine métier. Elle assure un service en toute indépendance, masquant pour l'extérieur la complexité de ce qu'elle fait.

  • "Brique" est un terme générique, la solution technique peut prendre différentes formes : bibliothèque tierce à inclure dans son projet, module distant nécessitant une communication par le réseau, ou au contraire module interne appelable directement dans notre code.
  • Grâce à son API, une brique peut recevoir des données en entrée et émettre des données en sortie : requête HTTP, appel direct via le code, événement via une file de messages. Tous ces échanges sont cadrés par des contrats.
  • Ces briques et leurs API sont conçues suivant le principe du Domain Driven Design (DDD) : l’approche de conception du code visant à calquer le domaine métier de la vie réelle.

Dans ce chapitre, nous allons aborder les microservices, un type particulier de brique logicielle qui combine tous ces points.

Avant ça, parlons de leur nemesis : le monolithe.

Le monolithe

Revenons dans les années 2000, à l'époque :

  • Le cloud computing est émergent. La plupart des entreprises administrent leur propre parc de serveurs. Les machines virtuelles permettent d’optimiser l’usage des machines physiques en exécutant plusieurs systèmes d’exploitation isolés, mais leur gestion reste lourde et fastidieuse.
  • Les réseaux sont moins performants. La fibre est peu répandue. Les protocoles, notamment HTTP, sont moins optimisés. Multiplier les appels réseau entre briques logicielles alourdit les performances.
  • Les pratiques et outils permettant des tests et déploiements sur les différents environnements ne sont pas encore généralisés. Avoir une seule application facilite la mise en production et réduit les risques d’échec.
  • Les navigateurs web sont moins standardisés, les requêtes asynchrones commencent à peine à se démocratiser et JavaScript est limité. On est à l'aube du web 2.0. Le backend (PHP, Java, .NET) génère directement le code frontend (HTML, CSS, JavaScript) qui sera interprété par le navigateur. Le métier de développeur frontend est assez nouveau, les développeurs sont pour la plupart polyvalents.

C'est pourquoi les directions informatiques, notamment dans les entreprises du web, centralisaient l'intégralité du code dans un seul projet.

Backend, frontend, gestion de compte utilisateur, rapports financiers... tout est fait au même endroit.
C'est ce qu'on appelle un monolithe, le cauchemar du développeur, la définition même de "code legacy" :

  • Même les monolithes les mieux organisés ont été pervertis par les dizaines de développeurs qui se sont succédé. Deadline trop courte ou manque de compétences, les développeurs ont bien souvent succombé à la facilité et ignorés les bonnes pratiques de test et de conception. Ces projets sont devenus d'énormes plats de spaghetti, où tout est entremêlé. Ils sont très fragiles à chaque changement.
  • Ils sont difficiles à faire évoluer techniquement. Dès qu'on veut changer un outil ou une version du langage, il faut repasser absolument partout. Ça prend des mois de travail. Autant dire qu'en général, on essaie de ne pas le faire, c'est pourquoi ces monolithes tournent souvent sur des technologies obsolètes. C'est une catastrophe niveau sécurité et bonne chance pour recruter de nouveaux développeurs.
  • Les développeurs travaillent souvent sur les mêmes fichiers, les conflits sont légion.

À ça, s'ajoutent quelques contraintes techniques. En particulier, c'est compliqué de faire monter un monolithe en charge. Un monolithe tourne généralement sur une seule machine et est relié à une seule base de données. Si demain le nombre de clients double, il faut soit augmenter la puissance de la machine, soit dupliquer l'intégralité du système.

Un monolithe, tous ceux qui ont lancé leur plateforme numérique dans les années 2000 en ont un. Deezer, L'Équipe, Rue du Commerce, dans chacune des entreprises par lesquelles je suis passé, le monolithe jouait un rôle central et gérait toujours des pans entiers du métier.
Toutes ont mis en place un plan pour casser ce monolithe en plus petites briques.

Les microservices

L'essor technologique des années 2010 a ouvert aux développeurs une nouvelle façon de concevoir leurs architectures. Ils appliquent les principes du Domain Driven Design pour définir des frontières claires à leurs domaines métiers afin de les exporter dans des projets indépendants.
Exemple : un projet paiement, compte utilisateur, anti-fraude...

Note : ce découpage fait apparaître parfois des briques non pas basées sur le métier, mais sur une problématique technique majeure. Exemple : authentification, sécurisation... Dans la suite, je ne parlerai que de découpage métier.

À la place d'un monolithe, nous avons donc maintenant un ensemble de briques autonomes avec :

  • Des sources de données dédiées : base de données, cache...
  • Un déploiement en production indépendant.
  • La capacité de répliquer uniquement cette partie en cas de montée en charge.
  • La liberté d'utiliser des technologies et des langages différents.
  • Et la majorité du temps, une infrastructure isolée.

Dans un monde idéal, ces briques suivent le principe de responsabilité unique : elles se concentrent sur une seule partie du métier et exposent un service bien défini.
Elles sont ainsi appelées "microservice", ou "ms".

Tous ces microservices communiquent grâce à leurs API respectives, la plupart du temps à travers HTTP ou via la diffusion d'événements.

D'un point de vue maintenabilité, les microservices semblent parfaits à première vue :

  • Le code n'étant plus dans le même projet, ça complique la vie des développeurs les moins consciencieux qui veulent prendre des raccourcis.
  • Si un développeur fait une mauvaise conception, elle est restreinte à un microservice sans impacter le reste.
  • La dette technique est plus facilement et rapidement remboursable. On peut faire évoluer l'ensemble ms par ms.

Mais ce système a aussi ses défauts :

  • Les communications passent par le réseau : c'est lent, il y a des risques d'échecs, la surveillance et le débogage sont plus difficiles.
  • Si le développement d'une fonctionnalité impacte plusieurs microservices, il faut développer la fonctionnalité elle-même, mais aussi les communications entre ms : c'est chronophage et il faut faire attention à l'ordre de mise en production.

Depuis les années 2020, avec le recul des architectures en microservices conçues pendant la dernière décennie, nous commençons à voir leur problème principal : le découpage est bien souvent mauvais.

Le "micro" de "microservices" a, à tort, poussé les devs à produire des briques aussi petites que possible. Plus il y a de briques, plus les communications augmentent, ce qui allonge le temps de développement et accroît la latence réseau. Mais surtout, avec le temps, le développement des nouvelles fonctionnalités crée des adhérences entre microservices : ils finissent tous par s'entre-appeler.

Exemple : ms-utilisateur demande à ms-paiement "Hey, donne-moi les paiements de l'utilisateur n°42". Le paiement va chercher les infos dans différents systèmes, pour obtenir les paiements Google, il a besoin de l'identifiant Google de l'utilisateur. Il demande donc "Hey ms-utilisateur, donne-moi les détails du client n°42". Ces deux microservices sont inter-dépendants !

On a retrouvé là notre plat de spaghetti avec plein de choses qui s'entremêlent. C'est même encore pire qu'avant car, maintenant, il est éclaté dans différentes briques à travers le réseau, c'est beaucoup plus compliqué de suivre le fil pour tout détricoter. Quel enfer ! On appelle ça un monolithe distribué.

Cette situation apparaît quand les développeurs essaient de trop bien faire. Ils sur-découpent en pensant décupler les effets positifs des microservices. Ils oublient qu'ils décuplent en même temps le négatif. Si demain un nouveau besoin apparaît et change le scope d'un microservice, ça peut foutre en l'air toute leur conception.

Exemple vécu : j'ai créé un microservice dédié à la gestion des abonnements, en me disant que c'est un domaine métier suffisamment compliqué pour légitimer qu'il soit indépendant. Ce ms fait des appels aux partenaires de paiement. L'entreprise veut maintenant sortir l'achat à l'acte de son monolithe historique : carte cadeau... Les partenaires de paiement sont les mêmes. Est-ce que j'intègre ça dans ms-abonnement alors que ce ne sont pas des abonnements ? Est-ce que je crée un nouveau ms-paiement pour centraliser ma relation avec les partenaires, qui serait alors utilisé par ms-abonnement ?
Le choix n'est pas facile et impactera l'entreprise sur le long terme.

Depuis quelques années, on entend de plus en plus parler d'un juste milieu entre notre bon vieux monolithe et les microservices.

Le monolithe modulaire

Partons directement sur un exemple :

Mon entreprise est un média en ligne qui propose des articles payants.
Différentes offres permettent au client de s'abonner à différents niveaux de service : premium, super premium.
Ces niveaux de service me débloquent différents droits : lire les articles, partager mon compte, poster un commentaire.

Cet exemple met en avant la gestion de plusieurs concepts métiers : article, compte, offre, niveau de service, abonnement, droit, commentaire.

Dans une architecture en microservices, nous aurions probablement créé un microservice dédié à la gestion de chacun de ces concepts : article, abonnement, compte, offre, droit, commentaire.
Ils sont tous liés :

  • Un compte accepte une offre.
  • Cette acceptation démarre un abonnement.
  • L'abonnement donne des droits au compte.
  • Un compte est donc lié à des droits.
  • Droits qui déverrouillent un article.
  • Un compte lit un article.
  • Un compte poste un commentaire.
  • Ce commentaire est ainsi associé à la fois au compte et à l'article.

L'idée du monolithe modulaire, c'est de réduire la complexité en rassemblant dans le même projet les concepts qui sont fortement dépendants.

On peut imaginer ici que les abonnements sont responsables à 90% de la mise en place et de la suppression des droits. Ça semble une bonne idée de les réunir.
Étudions le négatif d'abord : dans notre système, les seules fonctionnalités à haute charge qui relient le compte et l'abonnement sont le parcours d'achat et la page de gestion des abonnements sur l'espace client. Par contre, les droits d'un compte sont fréquemment utilisés : à la connexion, lors de l'affichage d'un article...
En mode microservices, nous aurions donné plus de puissance à ms-droit, tandis que ms-abonnement peut faire sa vie tranquillement sur des machines moins performantes. Une réunification des deux coupera cette distinction.

Il va donc falloir faire des choix. Parfois, ils sont évidents. Bien souvent, on accepte de perdre d'un côté pour gagner de l'autre.
On peut décider de réunir seulement certains concepts, le monolithe devient une sorte de macroservice.
Ou alors, le choix est fait de tout regrouper dans un seul et unique projet : terminé les microservices. Ça arrive de plus en plus dans les premières phases de lancement d'un produit, afin de le confronter le plus rapidement possible aux utilisateurs.

Mais du coup, on retombe sur tous les travers des années 2000 !

Dans "monolithe modulaire", il y a "module", et c'est là toute la différence avec l'ancien monolithe.

Certes, on réunit plusieurs concepts au même endroit, mais on introduit une frontière claire entre eux pour qu'ils restent indépendants. On fait tout pour que, si besoin, on puisse facilement extraire ce module et le transformer en microservice.

En gros, là où avant, j'avais des microservices ms-abonnement et ms-droit, j'ai maintenant un macroservice avec des modules "abonnement" et "droit".
Les microservices communiquaient via leurs API en passant par le réseau, perdant jusqu'à 100 millisecondes à chaque communication.
Les modules communiquent désormais via leurs API par des appels directs dans le code, cadrés par tous les contrats mis en place. On parle ici de microsecondes.

Dans un monde idéal, les modules ont, eux aussi, des sources de données indépendantes : base de données, cache... Si ce n'est pas le cas, il faut faire attention à segmenter ces sources pour assigner un responsable à chaque partie.
Exemple : le module "utilisateur" est responsable du cache associé au compte. Après un parcours d'achat, le module "abonnement" ne doit surtout pas supprimer ce cache de lui-même. Il doit communiquer "hey, l'utilisateur 42 vient de s'abonner" au module "compte" qui se chargera de la logique autour de cet événement, provoquant la mise à jour du cache.

En théorie, ce système modulaire réduit le risque de recréer des spaghettis. Dans la pratique, les langages ne disposent pas tous de fonctionnalité pour empêcher un dev de court-circuiter un contrat. On verra le résultat dans quelques années.


La plupart des développeurs fuient les monolithes historiques comme la peste. Mais pour certains, comme moi, ils représentent une opportunité. Extraire les fonctionnalités d'un monolithe, ce n'est pas juste faire un copier-coller, il faut retravailler le besoin actuel et proposer des améliorations fonctionnelles et techniques. Il faut la plupart du temps repenser une architecture de zéro et mettre en place un plan de migration pour que le nouveau système prenne la main au fur et à mesure sans perturber la production.
C'est passionnant !