Chapitre 7.5Monolithes et microservices

Du plat de spaghetti au monolithe modulaire.

7 minutes de lecture

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 qui vise à 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 némésis, 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 back-end (PHP, Java, .NET) génère directement le code front-end (HTML, CSS, JavaScript) qui sera interprété par le navigateur. Le métier de développeur front-end 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.

Back-end, front-end, 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é 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é, puisque les versions dépassées ne reçoivent plus de correctif, et bonne chance pour recruter de nouveaux développeurs. C'est un exemple parfait de dette technique.
  • 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. Il s'appuie généralement sur une seule base de données, et tout le code est déployé d'un bloc. Si demain le nombre de clients double, il faut soit augmenter la puissance de la machine, soit dupliquer l'intégralité du système. Même si seul le moteur de recherche sature, on est obligé de dupliquer aussi la facturation, les rapports financiers et le reste, ce qui coûte cher pour rien.

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. Le cloud permet de louer des machines à la demande, les conteneurs rendent le déploiement reproductible, et le déploiement continu automatise les mises en production.
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, envoi d'emails... 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 dans une file de messages.

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, donc c'est plus lent, il y a des risques d'échec, et la surveillance comme le débogage deviennent plus difficiles. Suivre une commande client qui traverse six briques demande des outils dédiés, comme nous l'avons vu dans le chapitre sur le monitoring.
  • 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.
  • Les données sont éparpillées. Impossible de faire une simple requête qui joint la table des commandes et celle des utilisateurs, elles ne vivent plus dans la même base. Il faut recomposer l'information par des appels successifs, et accepter que deux briques ne soient pas parfaitement synchronisées à la milliseconde près.
  • L'infrastructure devient un métier à part entière. Quarante briques à héberger, surveiller, sécuriser et déployer, ça demande une équipe et des outils que toutes les entreprises n'ont pas.

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, un découpage 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, ce qui coûte quelques millisecondes à chaque échange dans le meilleur des cas, bien plus si l'appel traverse plusieurs briques ou plusieurs continents.
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 tombe alors sous la microseconde.

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és permettant d'empêcher un dev de court-circuiter un contrat. On verra le résultat dans quelques années.

Si tu dois retenir une seule chose, c'est que le bon découpage ne se décide pas sur un tableau blanc en une après-midi. Il suit les frontières du métier et, très souvent, celles des équipes. Une brique par équipe, c'est bien plus sain que quarante briques pour six développeurs. Commencer simple et découper le jour où la douleur devient réelle reste, de mon expérience, la stratégie la moins risquée.

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 !

PrécédentLa programmation objet Tous les chapitres