
Il y a une semaine, j'ai pris la parole lors d'une réunion Node.JS et j'ai promis à beaucoup de publier un enregistrement de la performance. Plus tard, j'ai réalisé que je ne pouvais pas tenir compte de certains faits intéressants dans une demi-heure réglementée. Oui, et je préfère moi-même lire, plutôt que regarder et écouter, j'ai donc décidé de poster ma présentation sous la forme d'un article. Cependant, la vidéo sera également à la fin de l'article dans la section des liens.
J'ai décidé de vous parler d'un point sensible - la vie dans un monolithe. Il y a déjà des centaines d'articles à ce sujet sur le hub, des milliers d'exemplaires sont cassés dans les commentaires, la vérité est morte depuis longtemps dans la controverse, mais ... Le fait est que nous avons une expérience très spécifique dans OneTwoTrip, contrairement à beaucoup de gens qui écrivent sur certains modèles architecturaux dans vide:
- Tout d'abord, notre monolithe a déjà 9 ans.
- Deuxièmement, il a passé toute sa vie sous forte charge (il est maintenant de 23 millions de demandes par heure).
- Et dans NaN, nous écrivons notre monolithe sur Node.JS, qui a changé au-delà de la reconnaissance au cours des 9 dernières années. Oui, nous avons commencé à écrire sur le nœud en 2010, nous chantons une chanson à la frénésie des braves!
Nous avons donc beaucoup de spécificité et d'expérience réelle. Intéressant? C'est parti!
Délais de non-responsabilité
Cette présentation ne reflète que l'opinion privée de son auteur. Il peut coïncider avec la position de OneTwoTrip ou ne pas coïncider. Voilà comment la chance. Je travaille en tant qu’expert technique de l’une des équipes de l’entreprise et je ne prétends pas être objectif ou exprimer une opinion autre que la mienne.
Disclaimer Two
Cet article décrit les événements historiques, et pour le moment tout est complètement faux, alors ne vous inquiétez pas.
0. Comment est-ce arrivé
La tendance des requêtes du mot "microservice" dans google:

Tout est très simple - il y a neuf ans, personne ne connaissait les microservices. Nous avons donc commencé à écrire, comme tout le monde - dans un monolithe.
1. Douleur dans le monolithe
Je décrirai ici les situations problématiques que nous avons connues au cours de ces 9 années. Certains d'entre eux ont été résolus, certains ont été contournés par des hacks, certains ont tout simplement perdu leur pertinence. Mais leur souvenir, comme les cicatrices de bataille, ne me quittera jamais.
1.1 Mise à jour des composants connectés

C'est le cas lorsque la synergie est mauvaise. Parce que n'importe quel composant a été réutilisé plusieurs centaines de fois, et s'il était possible de l'utiliser de façon tordue, il n'a pas été perdu. Toute action peut provoquer des effets complètement imprévisibles, et tous ne sont pas suivis par des unités et des tests d'intégration. Rappelez-vous l'histoire des vadrouilles, d'un ventilateur et d'un ballon? Sinon, google. Elle est la meilleure illustration du code dans le monolithe.
1.2 Migration vers les nouvelles technologies
Voulez-vous Express? Linter? Un autre framework pour les tests ou moks? Mettre à jour le validateur ou au moins lodash? Mettre à jour Node.js? Je suis désolé. Pour ce faire, vous devez modifier des milliers de lignes de code.
Beaucoup parlent de l'avantage du monolithe, c'est que toute révision est un commit atomique . Ces gens sont silencieux sur une chose - cette révision ne sera jamais faite .
Connaissez-vous la vieille blague sur le versioning sémantique?
la vraie sémantique du versioning sémantique:
majeur = un changement de rupture
mineur = un changement de rupture mineur
patch = un changement de rupture minuscule
Imaginez maintenant que tout changement de rupture minuscule apparaîtra presque certainement dans votre code. Non, il est possible de vivre avec, et nous avons régulièrement rassemblé des forces et migré, mais c'était vraiment très difficile. Très.
1.3 Versions
Ici, je dois dire quelques spécificités de notre produit. Nous avons un grand nombre d'intégrations externes et diverses branches de la logique métier qui apparaissent assez rarement séparément. J'envie vraiment les produits qui exécutent réellement toutes les branches de leur code en 10 minutes en production, mais ce n'est pas le cas ici. Par essais et erreurs, nous avons trouvé pour nous-mêmes un cycle de publication optimal qui minimisait le nombre d'erreurs qui atteignent les utilisateurs finaux:
- la sortie se passe et les tests d'intégration passent en une demi-journée
- puis il est sous surveillance attentive sur scène pendant une journée (pour 10% des utilisateurs)
- puis une autre journée de production sous une surveillance encore plus attentive.
- Et seulement après cela, nous lui donnons le feu vert dans le maître.
Puisque nous aimons nos collègues et que nous ne sortons pas le vendredi, cela signifie finalement que la sortie va au maître environ 1,5 à 2 fois par semaine. Ce qui conduit au fait que la version peut avoir 60 tâches ou plus. une telle quantité provoque des conflits de fusion, des effets synergiques soudains, une charge de travail complète d'AQ sur l'analyse des journaux et d'autres peines. En général, il nous a été très difficile de libérer un monolithe.
1.4 Juste beaucoup de code
Il semblerait que la quantité de code ne devrait pas être d'une importance fondamentale. Mais ... pas vraiment. Dans le monde réel, c'est:
- Seuil d'entrée plus élevé
- D'énormes artefacts de construction pour chaque tâche
- Processus CI longs, y compris les tests d'intégration, les tests unitaires et même le linting de code
- Ralentissement du travail IDE (à l'aube du développement de Jetbrains, nous les avons choqués avec nos journaux plus d'une fois)
- Recherche contextuelle sophistiquée (n'oubliez pas, nous n'avons pas de saisie statique)
- Difficulté à trouver et à supprimer le code inutilisé
1.5 Propriétaires de code manquants
Très souvent, les tâches surgissent avec une sphère de responsabilité incompréhensible - par exemple, dans les bibliothèques connexes. Et le développeur d'origine pouvait déjà passer à une autre équipe, voire quitter l'entreprise. La seule façon de se rendre responsable dans ce cas est l'arbitraire administratif - de prendre et de nommer une personne. Ce qui n'est pas toujours agréable tant pour le développeur que pour celui qui le fait.
1.6 Difficulté de débogage
La mémoire coule-t-elle? Augmentation de la consommation du processeur? Vous vouliez construire des graphiques de flammes? Je suis désolé. Dans un monolithe, tant de choses se produisent en même temps qu'il devient incroyablement difficile de localiser un problème. Par exemple, pour comprendre laquelle des 60 tâches lors du déploiement en production entraîne une consommation accrue de ressources (bien que cela ne soit pas reproduit localement dans les environnements de test et de transfert), il est presque irréaliste.
1.7 Une pile
D'une part, c'est bien quand tous les développeurs «parlent» le même langage. Dans le cas de JS, il s'avère que même les développeurs Backend with Frontend se comprennent. Mais ...
- Il n'y a pas de solution miracle, et pour certaines tâches, vous souhaitez parfois utiliser autre chose. Mais nous avons un monolithe et nous n'avons nulle part où coller d'autres développeurs.
- Nous ne pouvons pas simplement prendre une bonne équipe sur la recommandation, qui nous est venue sur les conseils d'amis - nous n'avons nulle part où la mettre.
- Au fil du temps, nous nous reposons sur le fait que le marché n'a tout simplement pas assez de développeurs sur la bonne pile.
1.8 De nombreuses équipes avec des idées différentes sur le bonheur

Si vous avez deux développeurs, vous avez déjà deux idées différentes sur le meilleur framework, les normes à respecter, les bibliothèques, etc.
Si vous avez dix équipes, chacune ayant plusieurs développeurs, alors ce n'est qu'un désastre.
Et il n'y a que deux façons de le résoudre - soit «démocratique» (chacun fait ce qu'il veut), soit totalitaire (les normes sont imposées d'en haut). Dans le premier cas, la qualité et la standardisation souffrent, dans le second - les personnes qui ne sont pas autorisées à réaliser leur idée du bonheur.
2. Les avantages du monolithe
Bien sûr, le monolithe présente certains avantages qui peuvent être différents selon les piles, les produits et les équipes. Bien sûr, il y en a beaucoup plus que trois, mais je ne serai pas responsable de tout ce qui est possible, seulement de ceux qui nous concernent.
2.1 Facile à déployer
Lorsque vous avez un service, augmenter et tester est beaucoup plus simple qu'une douzaine de services. Certes, cet avantage n'est pertinent qu'au stade initial - par exemple, vous pouvez augmenter l'environnement de test et utiliser tous les services, à l'exception de ceux développés, à partir de celui-ci. Ou à partir de conteneurs. Ou quoi que ce soit d'autre.
2.2 Pas de surcharge de transfert de données
Un avantage assez douteux, si vous n'avez pas de charge élevée. Mais nous avons un tel cas - par conséquent, le coût du transport entre les microservices est perceptible pour nous. Peu importe comment vous essayez de le faire rapidement, stockez et transférez tout dans la RAM plus rapidement que toute autre chose - c'est évident.
2.2 Un assemblage
Si vous avez besoin de revenir en arrière à un moment donné de l'histoire, alors le faire avec un monolithe est vraiment simple - prenez-le et reculez. Dans le cas des microservices, il est nécessaire de sélectionner des versions de services compatibles qui ont été utilisées les unes avec les autres à un moment donné, ce qui ne peut pas toujours être simple. Certes, cela est également résolu à l'aide de l'infrastructure.
3. Avantages imaginaires d'un monolithe
Ici, j'ai pris toutes ces choses qui sont généralement considérées comme des avantages, mais de mon point de vue, elles ne le sont pas.
3.1 Code - c'est la documentation
Souvent entendu cette opinion. Mais généralement, il est suivi par des développeurs novices qui n'ont pas vu de fichiers dans des dizaines de milliers de lignes de code écrites par des gens qui sont partis il y a des années. Eh bien, pour une raison quelconque, le plus souvent, cet article est ajouté en faveur des partisans du monolithe - ils disent que nous n'avons besoin d'aucune documentation, nous n'avons aucun moyen de transport ou API - tout est dans le code, c'est facile et clair. Je ne contesterai pas cette affirmation, je dirai simplement que je n'y crois pas.
3.2 Il n'y a pas de versions différentes des bibliothèques, des services et des API. Il n'y a pas de référentiels différents.
Oui Mais non. Parce qu'au deuxième coup d'œil, vous comprenez que le service n'existe pas dans le vide. Et parmi un grand nombre d'autres codes et produits avec lesquels il s'intègre - à partir de bibliothèques tierces, en continuant avec les versions de logiciels de serveur et sans se terminer par des intégrations externes, une version IDE, des outils CI, etc. Et dès que vous comprenez combien de choses versionnées différentes votre service inclut indirectement, il devient immédiatement clair que ce plus n'est que de la démagogie.
3.3 Surveillance plus facile
Plus simple. Parce que vous avez à peu près un tableau de bord au lieu de quelques dizaines. Mais c'est plus compliqué, et parfois même impossible, car vous ne pouvez pas décomposer vos graphiques en différentes parties du code, et vous n'avez que la température moyenne à l'hôpital. En général, j'ai déjà tout dit dans le paragraphe sur la complexité du débogage, je précise simplement que la même complexité s'applique à la surveillance.
3.4 Il est plus facile de se conformer aux normes communes
Oui Mais, comme je l'ai déjà écrit dans le paragraphe sur beaucoup d'équipes avec l'idée du bonheur - les normes sont soit imposées de manière totalitaire, soit affaiblies presque jusqu'à leur absence.
3.5 Moins de risques de duplication de code
L'étrange opinion est que le code n'est pas dupliqué dans le monolithe. Mais je l'ai rencontré assez souvent. Dans ma pratique, il s'avère que la duplication de code dépend uniquement de la culture de développement de l'entreprise. Si c'est le cas, le code général est alloué à toutes sortes de bibliothèques, modules et microservices. S'il n'est pas là, alors il y aura un copier-coller vingt fois dans le monolithe.
4. Avantages des microservices
J'écrirai maintenant ce que nous avons obtenu après la migration. Encore une fois, ce sont de vraies conclusions d'une situation réelle.
4.1 Vous pouvez créer une infrastructure hétérogène
Nous pouvons maintenant écrire du code sur une pile qui est optimale pour résoudre un problème spécifique. Et il est rationnel d’utiliser tous les bons développeurs qui sont venus chez nous. Par exemple, voici un exemple de liste de technologies que nous avons actuellement:

4.2 Vous pouvez effectuer de nombreuses versions fréquentes
Maintenant, nous pouvons faire de nombreuses petites versions indépendantes, elles sont plus simples, plus rapides et pas douloureuses. Autrefois, nous n'avions qu'une seule équipe, mais maintenant il y en a déjà 18. Il y aurait eu une pause s'ils étaient tous restés dans le monolithe. Ou les personnes qui en sont responsables ...
4.3 Plus facile de faire des tests indépendants
Nous avons réduit le temps pour les tests d'intégration, qui ne testent désormais que ce qui a vraiment changé, et en même temps nous n'avons pas peur des effets d'une synergie soudaine. Bien sûr, j'ai dû commencer à contourner le râteau - par exemple, apprendre à créer des API rétrocompatibles - mais au fil du temps, tout s'est arrangé.
4.4 Plus facile à implémenter et à tester de nouvelles fonctionnalités
Nous sommes maintenant ouverts à l'expérimentation. Tous les frameworks, piles, bibliothèques - vous pouvez tout essayer, et en cas de succès, passez à autre chose.
4.5 Vous pouvez tout mettre à jour
Vous pouvez mettre à jour la version du moteur, les bibliothèques, mais n'importe quoi! Dans le cadre d'un petit service, trouver et corriger tous les changements de rupture est une question de minutes. Et pas des semaines, comme avant.
4.6 Et vous ne pouvez pas mettre à jour
Curieusement, c'est l'une des fonctionnalités les plus intéressantes des microservices. Si vous avez un code de travail stable, vous pouvez simplement le geler et l'oublier. Et vous n'aurez jamais à le mettre à jour, par exemple, pour exécuter le code produit sur un nouveau moteur. Le produit lui-même fonctionne sur un nouveau moteur et le microservice continue de vivre comme il a vécu. Les mouches avec des côtelettes peuvent enfin être mangées séparément.
5 Inconvénients des microservices
Bien sûr, une mouche dans la pommade n'était pas complète, et une solution parfaite pour simplement s'asseoir et être payé n'a pas fonctionné. Qu'avons-nous rencontré:
5.1 Besoin d'un bus pour l'échange de données et la journalisation claire.
L'interaction des services sur HTTP est un modèle classique, et en général même fonctionnel, à condition qu'il y ait des couches de journalisation et d'équilibrage entre elles. Mais il vaut mieux avoir un pneu plus distinct. En outre, vous devez réfléchir à la façon de collecter et de combiner les journaux entre eux - sinon vous n'aurez que de la bouillie sur les mains.
5.2 Gardez une trace de ce que font les développeurs.
En général, cela devrait toujours être fait, mais dans les microservices, les développeurs ont évidemment plus de liberté, ce qui peut parfois donner lieu à des choses dont Stephen King aurait la chair de poule. Même si extérieurement, il semble que le service fonctionne - n'oubliez pas qu'il devrait y avoir une personne qui surveille ce qui est en lui.
5.3 Vous avez besoin d'une bonne équipe DevOps pour tout gérer.
Presque n'importe quel développeur peut déployer un monolithe d'une manière ou d'une autre et télécharger ses versions (par exemple, via FTP ou SSH, j'ai vu cela). Mais avec les microservices, il existe toutes sortes de services centralisés pour collecter les journaux, les métriques, les tableaux de bord, les chefs pour gérer les configurations, les volts, les jenkins et d'autres bonnes choses, sans lesquelles vous pouvez généralement vivre - mais c'est mauvais et incompréhensible pourquoi. Donc, pour gérer les microservices, vous devez avoir une bonne équipe DevOps.
5.4 Vous pouvez essayer d'attraper un battage médiatique et de vous tirer une balle dans le pied.
C'est peut-être le principal inconvénient de l'architecture et son danger. Très souvent, les gens suivent aveuglément les tendances et commencent à introduire l'architecture et la technologie sans les comprendre. Après quoi tout tombe, ils se confondent dans le gâchis qui en résulte, et écrivent un article sur le Habr "comment nous sommes passés des microservices à un monolithe", par exemple. En général, ne bougez que si vous savez pourquoi vous faites cela et quels problèmes vous allez résoudre. Et lesquels vous obtenez.
6 Kaki dans le monolithe
Certains des hacks qui nous ont permis de vivre dans un monolithe sont légèrement meilleurs et un peu plus longs.
6.1 Peluches
L'introduction d'un linter dans un monolithe n'est pas une tâche aussi simple qu'il y paraît à première vue. Bien sûr, vous pouvez faire des règles strictes, ajouter une config, et ... Rien ne changera, tout le monde désactive simplement le linter, car la moitié du code devient rouge.
Pour l'introduction progressive du linting, nous avons écrit un module complémentaire simple pour eslint - slowlint , qui vous permet de faire une chose simple - pour contenir une liste de fichiers temporairement ignorés. En conséquence:
- Tout code incorrect est mis en évidence dans l'IDE
- De nouveaux fichiers sont créés selon les règles du peluchage, sinon CI ne les manquera pas
- Les anciens gouvernent progressivement et s'éloignent des exceptions
Au cours de l'année, il a été possible de rassembler environ la moitié du code monolithique dans un seul style, c'est-à-dire presque tout le code activement ajouté.
6.2 Améliorations des tests unitaires
Une fois les tests unitaires effectués avec nous pendant trois minutes. Les développeurs ne voulaient pas attendre autant de temps, donc tout était vérifié uniquement dans CI sur le serveur. Après un certain temps, le développeur a découvert que les tests étaient tombés, maudits, ouvert une branche, retourné au code ... En général, il a souffert. Qu'avons-nous fait avec ça:
- Pour commencer, nous avons commencé à exécuter des tests multithread. Yandex a une variante du moka multi-thread, mais avec nous, cela n'a pas décollé, alors ils ont eux-mêmes écrit un simple wrapper. Les tests ont commencé à être effectués une fois et demie plus rapidement.
- Ensuite, nous sommes passés de 0,12 nœuds au 8e (oui, le processus lui-même dessine dans un rapport séparé). Aussi étrange que cela puisse paraître, cela n'a pas donné un gain fondamental de productivité sur la production, mais les tests ont commencé à être effectués 20% plus rapidement.
- Et puis nous nous sommes assis pour déboguer les tests et les optimiser individuellement. Ce qui a donné la plus grande augmentation de vitesse.
En général, à l'heure actuelle, les tests unitaires s'exécutent dans le crochet de préparation et fonctionnent en 10 secondes, ce qui est assez confortable et vous permet de les exécuter sans interruption de la production.
6.3 Alléger le poids de l'artefact
L'artefact monolithique a finalement pris 400 mégaoctets. Compte tenu du fait qu'il est créé pour chaque commit, le volume total s'est avéré assez important. Le module de la diarrhée , une fourchette du module modclean , nous a aidé avec cela. Nous avons supprimé les tests unitaires de l'artefact et l'avons nettoyé de divers déchets tels que les fichiers Lisez-moi, les tests à l'intérieur des packages, etc. Le gain était d'environ 30% du poids!
6.4 Mise en cache des dépendances
Auparavant, l'installation de dépendances à l'aide de npm prenait tellement de temps que vous pouviez non seulement boire du café, mais aussi, par exemple, cuire des pizzas. Par conséquent, au début, nous avons utilisé le module npm-cache , qui était fourchu et un peu terminé. Il vous a permis de stocker des dépendances sur un lecteur réseau partagé, à partir duquel toutes les autres générations le prendraient ensuite.
Nous avons ensuite réfléchi à la reproductibilité des assemblages. Lorsque vous avez un monolithe, le changement des dépendances transitives est le fléau de Dieu. Étant donné que nous étions très en retard sur la version du moteur à ce moment-là, la modification d'une dépendance de cinquième niveau a facilement cassé tout notre ensemble. Nous avons donc commencé à utiliser npm-shrinkwrap. C'était déjà plus facile avec lui, même si la fusion de ses changements était un plaisir pour les forts d'esprit.
Et puis package-lock et l'excellente commande npm ci
finalement apparus - qui fonctionnaient à une vitesse légèrement inférieure à celle de l'installation des dépendances à partir du cache de fichiers. Par conséquent, nous avons commencé à l'utiliser uniquement et avons cessé de stocker des assemblys de dépendances. Ce jour-là, j'ai apporté au travail plusieurs boîtes de beignets.
6.5 Distribution de l'ordre des sorties.
Et c'est plus un hack administratif, pas technique. Au départ, j'étais contre lui, mais le temps a montré que le deuxième expert technique avait raison et était généralement bien fait. Lorsque les versions ont été distribuées à tour de rôle entre plusieurs équipes, il est devenu clair où exactement les erreurs sont apparues, et plus important encore, chaque équipe a senti sa responsabilité en matière de vitesse et a essayé de résoudre les problèmes et de les déployer le plus rapidement possible.
6.6 Suppression du code mort
Dans un monolithe, il est très effrayant de supprimer le code - vous ne savez jamais où vous pourriez vous y retrouver. Par conséquent, le plus souvent, il ne reste plus qu'à s'allonger sur le côté. Au fil des ans. Et même le code mort doit être pris en charge, sans parler de la confusion qu'il introduit. Par conséquent, au fil du temps, nous avons commencé à utiliser require-analyse pour une recherche superficielle de code mort, et des tests d'intégration plus le lancement en mode de vérification de couverture pour une recherche plus approfondie.
7 Coupe monolithe
Pour une raison quelconque, beaucoup de gens pensent que pour passer aux microservices, vous devez abandonner votre monolithe, écrire un tas de microservices près de zéro, l'exécuter tout à la fois - et il y aura du bonheur. Mais ce modèle ... Hmm ... Il est lourd du fait que vous ne ferez rien et que vous passerez juste beaucoup de temps et d'argent à écrire du code que vous devrez jeter.
Je propose une autre option, qui me semble plus fonctionnelle, et qui a été mise en place avec nous:
- Nous commençons à écrire de nouveaux services dans les microservices. Nous gérons la technologie, sautons sur le râteau, nous comprenons si nous voulons le faire du tout.
- Nous extrayons le code dans des modules, des bibliothèques ou tout ce qui y est utilisé.
- Nous distinguons les services d'un monolithe.
- Nous distinguons les microservices des services. Sans hâte et un à la fois.
8 Et enfin

Finalement, j'ai décidé de laisser la chose la plus importante.
N'oubliez pas:
- Vous n'êtes pas google
- Vous n'êtes pas Microsoft
- Tu n'es pas facebook
- Vous n'êtes pas Yandex
- Vous n'êtes pas netflix
- Vous n'êtes pas OneTwoTrip
Si quelque chose fonctionne dans d'autres entreprises, ce n'est absolument pas un fait que cela vous sera bénéfique. Si vous essayez de copier aveuglément l'expérience d'autres entreprises avec les mots «ça marche pour eux», cela se terminera très probablement mal. Chaque entreprise, chaque produit et chaque équipe est unique. Ce qui fonctionne pour certains ne fonctionnera pas pour d'autres. Je n'aime pas dire des choses évidentes, mais trop de gens commencent à construire un culte du fret autour d'autres entreprises, copiant aveuglément les approches, et s'enterrent sous de fausses décorations pour arbres de Noël. Ne le faites pas. Expérimentez, essayez, développez les solutions qui vous conviennent le mieux. Et alors seulement, tout ira bien.
Liens utiles: