L’histoire se répète. Tout ce qui est ancien est à nouveau nouveau et je suis là depuis assez longtemps pour voir des idées abandonnées, redécouvertes et revenir triomphalement pour dépasser la mode. Ces dernières années, SQL a fait un formidable retour d’entre les morts. Nous aimons à nouveau les bases de données relationnelles. Je pense que le monolithe aura à nouveau son moment d’odyssée spatiale. Les microservices et le sans serveur sont des tendances poussées par les fournisseurs de cloud, conçues pour nous vendre plus de ressources de cloud computing. Les microservices ont très peu de sens financièrement pour la plupart des cas d’utilisation. Oui, ils peuvent ralentir. Mais lorsqu’ils se développent, ils en paient les coûts sous forme de dividendes. L’augmentation des coûts d’observabilité remplit à elle seule les poches des fournisseurs de « gros nuages ».
J’ai récemment dirigé un panel de conférence qui a couvert le sujet des microservices par rapport aux monolithes. Le consensus au sein du panel (même avec la personne pro-monolithe), était que les monolithes ne s’adaptent pas aussi bien que les microservices.
C’est probablement vrai pour les monstrueux monolithes d’autrefois qu’Amazon, eBay, et al. remplacé. C’étaient en effet d’énormes bases de code dans lesquelles chaque modification était pénible et leur mise à l’échelle était difficile. Mais ce n’est pas une comparaison juste. Les nouvelles approches battent généralement les anciennes approches. Mais que se passe-t-il si nous construisons un monolithe avec des outils plus récents, obtiendrons-nous une meilleure évolutivité ?
Quelles seraient les limites et à quoi ressemble un monolithe moderne ?
Modulithe
Pour avoir une idée de la dernière partie, vous pouvez consulter le Projet Spring Modulith. C’est un monolithe modulaire qui nous permet de construire un monolithe en utilisant des pièces isolées dynamiques. Avec cette approche, nous pouvons séparer les tests, le développement, la documentation et les dépendances. Cela aide avec l’aspect isolé du développement de microservices avec peu de frais généraux impliqués. Il supprime la surcharge des appels distants et la réplication des fonctionnalités (stockage, authentification, etc.).
Le Spring Modulith n’est pas basé sur la modularisation de la plate-forme Java (Jigsaw). Ils appliquent la séparation pendant les tests et pendant l’exécution, il s’agit d’un projet Spring Boot régulier. Il dispose de capacités d’exécution supplémentaires pour l’observabilité modulaire, mais il s’agit principalement d’un exécuteur de «meilleures pratiques». Cette valeur de cette séparation va au-delà de ce à quoi nous sommes normalement habitués avec les microservices, mais comporte également certains compromis.
Donnons un exemple. Un monolithe Spring traditionnel comporterait une architecture en couches avec des packages comme celui-ci :
com.debugagent.myapp
com.debugagent.myapp.services
com.debugagent.myapp.db
com.debugagent.myapp.rest
Ceci est précieux car cela peut nous aider à éviter les dépendances entre les couches ; par exemple, la couche DB ne devrait pas dépendre de la couche service. Nous pouvons utiliser des modules comme celui-ci et forcer efficacement le graphe de dépendance dans une direction : vers le bas. Mais cela n’a pas beaucoup de sens à mesure que nous grandissons. Chaque couche se remplira de classes de logique métier et de complexités de base de données.
Avec un Modulith, on aurait une architecture qui ressemblerait plutôt à ça :
com.debugagent.myapp.customers
com.debugagent.myapp.customers.services
com.debugagent.myapp.customers.db
com.debugagent.myapp.customers.rest
com.debugagent.myapp.invoicing
com.debugagent.myapp.invoicing.services
com.debugagent.myapp.invoicing.db
com.debugagent.myapp.invoicing.rest
com.debugagent.myapp.hr
com.debugagent.myapp.hr.services
com.debugagent.myapp.hr.db
com.debugagent.myapp.hr.rest
Cela ressemble assez à une architecture de microservice appropriée. Nous avons séparé tous les éléments en fonction de la logique métier. Ici, les dépendances croisées peuvent être mieux contenues et les équipes peuvent se concentrer sur leur propre zone isolée sans se marcher sur les pieds. C’est une grande partie de la valeur des microservices sans les frais généraux.
Nous pouvons en outre imposer la séparation de manière approfondie et déclarative à l’aide d’annotations. Nous pouvons définir quel module utilise lequel et forcer les dépendances à sens unique, de sorte que le module des ressources humaines n’aura aucun rapport avec la facturation. Le module client non plus. Nous pouvons imposer une relation à sens unique entre les clients et la facturation et communiquer en retour à l’aide d’événements. Les événements au sein d’un Modulith sont triviaux, rapides et transactionnels. Ils découplent les dépendances entre les modules sans tracas. Cela est possible avec les microservices mais serait difficile à appliquer. Supposons que la facturation doive exposer une interface à un module différent. Comment empêchez-vous les clients d’utiliser cette interface ?
Avec les modules, nous le pouvons. Oui. Un utilisateur peut modifier le code et fournir un accès, mais cela nécessiterait de passer par une révision du code et cela présenterait ses propres problèmes. Notez qu’avec les modules, nous pouvons toujours nous appuyer sur des éléments de base courants des microservices tels que les drapeaux de fonctionnalités, les systèmes de messagerie, etc. Vous pouvez en savoir plus sur Spring Modulith dans la documentation et sur le blog de Nicolas Fränkel.
Chaque dépendance dans un système de modules est cartographiée et documentée dans le code. L’implémentation de Spring inclut la possibilité de tout documenter automatiquement avec des graphiques à jour pratiques. Vous pourriez penser que les dépendances sont la raison d’être de Terraform. Est-ce le bon endroit pour un tel design « de haut niveau » ?
Une solution Infrastructure as Code (IaC) comme Terraform pourrait toujours exister pour un déploiement Modulith, mais elle serait beaucoup plus simple. Le problème est la répartition des responsabilités. La complexité du monolithe ne disparaît pas avec les microservices comme vous pouvez le voir dans l’image suivante (extraite de ce fil). Nous venons de renvoyer cette boîte de Pandore à l’équipe DevOps et nous avons rendu leur vie plus difficile. Pire encore, nous ne leur avons pas donné les bons outils pour comprendre cette complexité, ils doivent donc gérer cela de l’extérieur.
C’est pourquoi les coûts d’infrastructure augmentent dans notre industrie, où traditionnellement les prix devraient tendre à la baisse. Lorsque l’équipe DevOps rencontre un problème, elle y consacre des ressources. Ce n’est pas la bonne chose à faire dans tous les cas.
Autres modules
Nous pouvons utiliser les modules de plate-forme Java standard (Jigsaw) pour créer une application Spring Boot. Cela a l’avantage de décomposer l’application et une syntaxe Java standard, mais cela peut parfois être gênant. Cela fonctionnerait probablement mieux lorsque vous travaillez avec des bibliothèques externes ou que vous divisez une partie du travail en outils communs.
Qu’en est-il de l’échelle ?
Nous pouvons utiliser la plupart des outils de mise à l’échelle des microservices pour mettre à l’échelle nos monolithes. Une grande partie de la recherche liée à la mise à l’échelle et au regroupement a été développée en pensant aux monolithes. C’est un processus plus simple puisqu’il n’y a qu’une seule partie mobile : l’application. Nous reproduisons des instances supplémentaires et les observons. Il n’y a pas de service individuel qui échoue. Nous avons des outils de performance précis et tout fonctionne comme une seule version unifiée.
Je dirais que la mise à l’échelle est plus simple que les microservices équivalents. Nous pouvons utiliser des outils de profilage et obtenir une approximation raisonnable des goulots d’étranglement. Notre équipe peut facilement (et à moindre coût) mettre en place des environnements de test pour exécuter des tests. Nous avons une vue unique de l’ensemble du système et de ses dépendances. Nous pouvons tester un module individuel de manière isolée et vérifier les hypothèses de performance.
Les outils de traçage et d’observabilité sont formidables. Mais ils affectent également la production et produisent parfois du bruit. Lorsque nous essayons de donner suite à un goulot d’étranglement de mise à l’échelle ou à un problème de performances, ils peuvent nous envoyer dans le mauvais terrier.
Nous pouvons utiliser Kubernetes avec des monolithes aussi efficacement qu’avec des microservices. La taille de l’image serait plus grande mais si nous utilisons des outils comme GraalVM, elle pourrait ne pas être beaucoup plus grande. Avec cela, nous pouvons répliquer le monolithe dans toutes les régions et fournir le même comportement de basculement que nous avons avec les microservices. De nombreux développeurs déploient des monolithes sur Lambdas. Je ne suis pas fan de cette approche car cela peut coûter très cher, mais cela fonctionne.
Le goulot d’étranglement
Mais il reste un point où un monolithe se heurte à un mur d’échelle : la base de données. Les microservices atteignent une grande échelle grâce au fait qu’ils ont intrinsèquement plusieurs bases de données distinctes. Un monolithe fonctionne généralement avec un seul magasin de données. C’est souvent le véritable goulot d’étranglement de l’application. Il existe des moyens de mettre à l’échelle une base de données moderne. Le clustering et la mise en cache distribuée sont des outils puissants qui nous permettent d’atteindre des niveaux de performances qu’il serait très difficile d’atteindre dans une architecture de microservices.
Il n’y a pas non plus d’exigence pour une seule base de données dans un monolithe. Il n’est pas inhabituel d’avoir une base de données SQL tout en utilisant Redis pour le cache. Mais nous pouvons également utiliser une base de données distincte pour les séries chronologiques ou les données spatiales. Nous pouvons également utiliser une base de données distincte pour les performances, bien que d’après mon expérience, cela ne se soit jamais produit. Les avantages de conserver nos données dans la même base de données sont énormes.
Les avantages
Le fait que nous puissions conclure une transaction sans compter sur la « cohérence éventuelle » est un avantage incroyable. Lorsque nous essayons de déboguer et de répliquer un système distribué, nous pouvons avoir un état intermédiaire très difficile à répliquer localement ou même à comprendre pleinement à partir de l’examen des données d’observabilité.
Les performances brutes suppriment une grande partie de la surcharge du réseau. Avec une mise en cache de niveau 2 correctement réglée, nous pouvons encore supprimer 80 à 90 % des E/S lues. Ceci est possible dans un microservice mais serait beaucoup plus difficile à réaliser et ne supprimera probablement pas la surcharge des appels réseau.
Comme je l’ai mentionné précédemment, la complexité de l’application ne disparaît pas dans une architecture de microservice. Nous venons de le déplacer vers un autre endroit. D’après mon expérience jusqu’à présent, ce n’est pas une amélioration. Nous avons ajouté de nombreuses pièces mobiles dans le mélange et augmenté la complexité globale. Revenir à une architecture unifiée plus intelligente et plus simple a plus de sens.
Pourquoi utiliser les microservices
Le choix du langage de programmation est l’un des premiers indicateurs d’affinité avec…