/ Grande Aventure

Que faire si Node.Js consomme trop en ressources (RAM / CPU) ?

Que faire si Node.Js consomme trop en ressources (RAM / CPU) ?

Afin d'illustrer comment j'ai résolu mon problème de consommation de ressources avec Node.js, je vais vous raconter une "petite histoire" pour vous donner le contexte, mais aussi mon raisonnement me permettant de trouver l'origine du problème ainsi que la sélection de la solution.

La petite histoire

Un jours (quelques mois après mon arrivée dans ma nouvelle société) un membre du staff me remonte un problème de lenteur / déconnexion intempestive.

À ce moment la je n'avais encore jamais eu le temps de regarder les différents environnements présents dans la chaîne de développement (sauf l'env de Dev bien entendu :) ).
On m'a seulement donnée à mon arrivée la liste des environnements :

  • Dev
  • Staging
  • Production

Même si je suis certain que vous connaissez le but de ces environnements, je préfère préciser, histoire qu'il n'y a pas d'ambiguïté.

L'environnement de Dev permet de créer de nouvelles features, de corriger des bug, de créer des POC et j'en passe. C'est l'environnement qui est utilisé par le développeur généralement directement sur son poste de travail. C'est aussi par définition l'environnement le plus en avance sur la Production.

L'environnement de Staging (ou Pré-Prod) est un environnement ISO Production, tout doit être identique (l'architecture serveur(s), quota de ressources, contexte, données, etc...), son principale but et de vérifier le comportement de la mise à jour avant que cette dernière arrive sur l'environnement de production.

L'environnement de Production est le "live serveur", c'est le seul environnement qui est en contacte avec tous les utilisateurs. C'est donc cet environnement qui délivre le service attendu par les utilisateurs.


Assez de théorie, revenons à la réalité :)

Ici, l'environnement de Staging n'est pas ISO Prod, et comme si cela n'étais pas déjà suffisant, il utilisé uniquement pour des tests. A savoir qu'il existe un environnement d'intégration, mais complètement à l'abandon...

Alors j'ai demandé les accès à tous les environnements, pour faire un état des lieux et mettre en place un workflow de déploiement !
Essentiel si on veut pouvoir garantir que ce que l'on à développer fonctionne comme attendu une fois arrivé en Production :)

Autre surprise, seul l'environnement de Production bénéficie de métrics (donné directement par l'hébergeur Heroku), donc impossible de savoir avant de balancer en Production si la mise à jour empire ou améliore la consommation des ressources...

Je ne m'éttandrais pas plus sur le nombre de problématiques, outils manquants, manque de procédures, de tests, etc... J'ajouterais juste que PM2 est utilisé uniquement pour l'environnement de Dev (certainement le seul environnement qui n'en n'a pas besoin :p ).

Bon, je remonte mes manches

Je décide d'installer PM2 en Production.
Comme je dis toujours, un médecin ne peux prescrire des soins sans avoir occulté le patient ?
Ici, c'est pareil !

Il faut savoir tout de même que les métrics d'Héroku montrait une consommation plutôt constante de ~1Go de RAM (on à même eu des pics à 1.5Go... 1.5Go !!!).
Je ne sais pas si vous vous en rendez compte, mais pour une application Node.js, 1Go de RAM, c'est juste énorme !
J'ai déjà vu des applications Node.js complexe avec plusieurs millions de connexions simultané à moins de 600Mo.

Bref, une fois PM2 installé et relié à Keymetrics, je pensais me pencher sur une étude poussée de la Heap, du comportement du Garbage Collector, etc... Je n'ai pas eu besoin de regarder bien loin, une simple lecture des courbes du CPU et de la RAM (voir ci-dessous) sont sans appel !

Consommation-de-Node.JS-sur-la-RAM---CPU-sur-l-environnement-de-Production-1
(Consommation de la RAM / CPU sur l'environnement de Production)

Quelque chose vient remplir la RAM du serveur et stresse énormément le CPU de manière cyclique. Après une petite heure de collecte de données, j'ai pu déterminer sans difficulté que toutes les 10min le problème de lenteur (qui interviennent généralement avec une déconnexion) était de retour.

La coïncidence est bien entendu trop grosse pour que ce soit le hasard, le graph ci-dessus montre une évolution de la RAM qui dépasse légèrement le Giga Octet toutes les 10 minutes !

La dernière petite information importante que je n'ai pas encore donnée, c'est que l'hébergement Héroku pour l'environnement de la Production nous donne le droit d'utiliser 1Go de RAM Max.
Je ne serais pas expliquer ce qu'il se passe en détails sur le serveur, lorqu'on dépasse le quota, je sais qu'on peut aller jusqu'à 1.5Go avant d'avoir un Reboot du serveur.

Ceci dit, j'ai quand même l'impression que quelque chose se passe dès qu'on dépasse le quota autorisé, car l'application ralentit énormément et génère des erreurs puis une déconnexion...


Comment trouver le coupable ?

Ma nouvelle tache à était de trouver ce qui pouvais lancer des traitements toutes les 10 minutes précisément !
J'ai directement regardé du côté des batch. Un en particulier qui contient un grand nombre de lignes de codes :)

Jackpot ! Après une analyse rapide, il se lance toutes les 10 minutes !
Par acquis de conscience, j'ai regardé les temps de lancement de tous les autres batch et seul le plus gros des batch se lance toutes les 10 minutes :)
Après une longue lecture et quelques saignements des yeux, j'ai compris comment les créateurs de l'application ont réussi à consommer autant !!


Mais finalement, à quoi il peut bien servir ce méga batch ?

Pour faire simple, le but du batch était de :

  • Déterminer si une commande doit être annulée automatiquement au bout d'un certain temps
  • Déterminer si une mission est terminée
  • Déterminer si il faut envoyer la notation de la mission
  • Déterminer si la commande est terminée
  • Déterminer si la commande est annulée

Sauf qu'on parle de quelques dizaines de milliers de missions qui sont éligibles. Chaque mission contient énormément de données (la structuration de la BDD est vraiment pas top). A chacun des points cité, ci-dessus on récupère l'ensemble des missions éligibles, on effectue tout un ensemble de boucles imbriquées les unes dans les autres pour réaliser tout un ensemble de traitements, pour au final uniquement déterminer une information simple.
Est ce que la mission est terminée ?
Est ce que la commande est terminée ?
Est ce que la mission doit être annulée ?
Est ce que la commande doit être annulée ?

J'ai trouvé pas moins de 6 solutions pour grandement améliorer les performances... Ces dires à quel point, la construction de ce batch à était maladroite...


La solution élue

Une des 6 solutions était obligatoirement ma préférée. Node.js étant Event-Driven, il est logique de privilégier l'événementiel, après tout, c'est toute la puissance de Node.js :)

Pour cela, j'ai cherché un module me permettant d'enregistrer un Event et de le déclencher au moment voulu, après quelques recherche, j'ai trouvé un bon candidat. Le seul problème étant qu'il n'était plus supporté depuis 2 ans et qu'en plus ne permettait pas de stocker plusieurs Event sous le même nom, qui est problématique lorsqu'on doit gérer plusieurs missions en même temps :)

J'ai donc décidé de créer mon propre module à partir de celui que j'ai trouvé.
J'en ai profité pour mettre à jour toutes les dépendances et j'ai bien entendu effectué toutes les modification pour mon besoin.
J'ai décidé de nommer mon module comme celui que j'ai repris par respect pour l'auteur du module original : mongo-scheduler-more


Premier test

Il est venu le temps de mettre en place mon premier module mongo-scheduler-more et de le Tester :)
Mais avant ça, il faut que je reproduise le problème histoire d'être sur de l'avoir résolu.

Pour cela, j'ai utilisé l'environnement d'Intégration, j'ai pris une mesure des courbes RAM / CPU sans aucune modification (ni code, ni BDD), me permettant de comparer avant / après et de déterminer avec précision l'impact de ma solution.
Voir l'image ci-dessous:

Consommation-de-Node.JS-sur-la-RAM---CPU-sur-l-envionnement-d-Int-gration
(Consommation de la RAM / CPU sur l'envionnement d'Intégration)

Bon, on remarque une activité presque normal, et un petit pic au niveau du CPU toutes les 10 min, mais rien d'alarmant, car les données (nombre de missions) sont trop faible pour avoir un réel impact.

Aller, il est temps de reproduire le problème en Intégration, je remplace la BDD d'Intégration par celle de la Production:

Consommation-de-Node.JS-sur-la-RAM---CPU-sur-l-envionnement-d-Int-gration-avec-la-BDD-de-Production
(Consommation de la RAM / CPU sur l'environnement d'Intégration avec la BDD de Production)

Hummm, ce graph ressemble étrangement à celui de la Production, vous ne trouvez pas ?

Ok, maintenant qu'on a reproduit le problème de lenteur, on peut remplacer le méga batch par mon module :

Consommation-de-Node.JS-sur-la-RAM---CPU-sur-l-envionnement-d-Int-gration-avec-la-BDD-de-Production-et-mongo-scheduer-more
(Consommation de la RAM / CPU sur l'environnement d'Intégration avec la BDD de Production et mon premier module mongo-scheduler-more)

Wow, plutôt impressionnant ;)
Le CPU est presque toujours au repos (sauf pour quelque traitement qui restent lourd même avec mon module), mais surtout la RAM ne dépasse pas les 200 Mo.


Conclusion

Finalement la solution du problème été simple à partir du moment qu'on utilise les bons outils !
Si j'ai un message que vous devez retenir ce cet article, c'est de savoir quels outils utiliser pour analyser la problématique, sans ça, vous perdrez du temps inutilement pour possiblement ne pas résoudre le problème.

Je ne peux que vous conseiller PM2, c'est un merveilleux outil capable de faire bien des choses. Cependant, il y a d'autres outils moins versatile, mais plus pointu...

Dans de futurs posts, je vous donnerais les clefs pour trouver les causes de lenteurs de votre application.

Qu'avez-vous pensé de cette histoire ?
Avez-vous déjà rencontré ce type de problématiques ?

Dites moi dans les commentaires ;)

DarkTerra

DarkTerra

Est un Developpeur spécialisé dans le Web et plus précisement coté Back-End. Ayant une attirance pour la performance et les nouvelles technologies, Node.JS correspond parfaitement à ses attentes.

Read More