Maison >interface Web >js tutoriel >Suivi de l'utilisation élevée de la mémoire dans Node.js

Suivi de l'utilisation élevée de la mémoire dans Node.js

Patricia Arquette
Patricia Arquetteoriginal
2024-12-17 00:40:25817parcourir

Dans cet article, je partagerai mon approche pour détecter et corriger l'utilisation élevée de la mémoire dans Node.js.

Contenu

  • Contexte
  • Approche
    • Comprendre le code
    • Répliquer le problème de manière isolée
    • Capturez les profils des services de préparation
    • Vérifiez le correctif
  • Résultats
  • Conclusion

Contexte

Récemment, j'ai reçu un ticket avec le titre "Résoudre un problème de fuite de mémoire dans la bibliothèque x". La description comprenait un tableau de bord Datadog montrant une douzaine de services souffrant d'une utilisation élevée de la mémoire et finissant par planter avec des erreurs MOO (mémoire insuffisante), et ils avaient tous la bibliothèque x en commun.

J'ai découvert la base de code assez récemment (<2 semaines), ce qui a rendu la tâche difficile et méritait également d'être partagée.

J'ai commencé à travailler avec deux informations :

  • Il existe une bibliothèque utilisée par tous les services qui entraîne une utilisation élevée de la mémoire et qui implique Redis (redis était inclus dans le nom de la bibliothèque).
  • La liste des services qui ont été concernés.

Ci-dessous le tableau de bord qui était lié au ticket :

Tracking down high memory usage in Node.js

Les services fonctionnaient sur Kubernetes et il était évident qu'ils accumulaient de la mémoire au fil du temps jusqu'à ce qu'ils atteignent la limite de mémoire, tombent en panne (récupèrent de la mémoire) et redémarrent.

Approche

Dans cette section, je partagerai comment j'ai abordé la tâche à accomplir, en identifiant le coupable de l'utilisation élevée de la mémoire et en le corrigeant plus tard.

Comprendre le code

Comme j'étais relativement nouveau dans la base de code, je voulais d'abord comprendre le code, ce que faisait la bibliothèque en question et comment elle était censée être utilisée, en espérant qu'avec ce processus, il serait plus facile d'identifier le problème. Malheureusement, il n'y avait pas de documentation appropriée, mais en lisant le code et en recherchant comment les services utilisaient la bibliothèque, j'ai pu en comprendre l'essentiel. Il s'agissait d'une bibliothèque englobant des flux Redis et exposant des interfaces pratiques pour la production et la consommation d'événements. Après avoir passé une journée et demie à lire le code, je n'ai pas pu saisir tous les détails ni comment les données circulaient en raison de la structure et de la complexité du code (beaucoup d'héritage de classe et de rxjs que je ne connais pas).

J'ai donc décidé de mettre une pause dans la lecture et d'essayer de repérer le problème tout en observant le code en action et en collectant des données de télémétrie.

Répliquer le problème de manière isolée

Comme aucune donnée de profilage n'était disponible (par exemple, profilage continu) qui pourrait m'aider à approfondir mes recherches, j'ai décidé de reproduire le problème localement et d'essayer de capturer des profils de mémoire.

J'ai trouvé plusieurs façons de capturer des profils de mémoire dans Node.js :

  • Utilisation de l'instantané du tas
  • Utilisation du Heap Profiler
  • Profilage des performances JavaScript
  • Clinique.js

N'ayant aucune idée où chercher, j'ai décidé d'exécuter ce que je pensais être la partie la plus « gourmande en données » de la bibliothèque, le producteur et le consommateur de flux Redis. J'ai construit deux services simples qui produiraient et consommeraient des données à partir d'un flux Redis et j'ai procédé à la capture des profils de mémoire et à la comparaison des résultats au fil du temps. Malheureusement, après quelques heures passées à produire de la charge sur les services et à comparer les profils, je n'ai pu constater aucune différence dans la consommation de mémoire dans aucun des deux services, tout semblait normal. La bibliothèque exposait un tas d'interfaces différentes et de manières d'interagir avec les flux Redis. Il est devenu clair pour moi qu'il serait plus compliqué que ce à quoi je m'attendais de reproduire le problème, en particulier avec ma connaissance limitée des services réels dans un domaine spécifique.

La question était donc : comment puis-je trouver le bon moment et les bonnes conditions pour capturer la fuite de mémoire ?

Capturer des profils à partir de services de transfert

Comme mentionné précédemment, le moyen le plus simple et le plus pratique de capturer des profils de mémoire serait d'avoir un profilage continu sur les services réels affectés, une option que je n'avais pas. J'ai commencé à étudier comment exploiter au moins nos services de transfert (ils étaient confrontés à la même consommation élevée de mémoire) qui me permettraient de capturer les données dont j'avais besoin sans effort supplémentaire.

J'ai commencé à chercher un moyen de connecter Chrome DevTools à l'un des pods en cours d'exécution et de capturer des instantanés de tas au fil du temps. Je savais que la fuite de mémoire se produisait lors de la préparation, donc si je pouvais capturer ces données, j'espérais pouvoir repérer au moins certains des points chauds. À ma grande surprise, il existe un moyen de faire exactement cela.

Le processus pour faire cela

  • Activez le débogueur Node.js sur le pod en envoyant un signal SIGUSR1 au processus de nœud sur votre pod.
kubectl exec -it <nodejs-pod-name> -- kill -SIGUSR1 <node-process-id>




</p>
<p><em>En savoir plus sur les signaux Node.js lors des événements Signal</em></p>

<p>En cas de succès, vous devriez voir un journal de votre service :<br>
</p>

<pre class="brush:php;toolbar:false">Debugger listening on ws://127.0.0.1:9229/....
For help, see: https://nodejs.org/en/docs/inspector
  • Exposez le port sur lequel le débogueur écoute, localement en exécutant
kubectl port-forward <nodejs-pod-name> 9229
  • Connectez Chrome Devtools au débogueur que vous avez activé lors des étapes précédentes. Visitez chrome://inspect/ et là vous devriez voir dans la liste des cibles votre processus Node.js :

Tracking down high memory usage in Node.js

sinon, assurez-vous que vos paramètres de découverte de cible sont correctement configurés

Tracking down high memory usage in Node.js

Vous pouvez maintenant commencer à capturer des instantanés au fil du temps (la période dépend du temps requis pour que la fuite de mémoire se produise) et les comparer. Chrome DevTools offre un moyen très pratique de le faire.

Vous pouvez trouver plus d'informations sur les instantanés de mémoire et les outils de développement Chrome sur Enregistrer un instantané du tas

Lors de la création d'un instantané, tous les autres travaux dans votre fil de discussion principal sont arrêtés. Selon le contenu du tas, cela peut même prendre plus d'une minute. L'instantané est intégré à la mémoire, il peut donc doubler la taille du tas, ce qui entraîne le remplissage de toute la mémoire, puis le blocage de l'application.

Si vous envisagez de prendre un instantané de tas en production, assurez-vous que le processus à partir duquel vous le récupérez peut planter sans affecter la disponibilité de votre application.

À partir de la documentation Node.js

Revenons donc à mon cas, en sélectionnant deux instantanés à comparer et à trier par delta, j'ai obtenu ce que vous pouvez voir ci-dessous.

Tracking down high memory usage in Node.js

Nous pouvons voir que le plus grand delta positif se produisait sur le constructeur de chaînes, ce qui signifiait que le service avait créé beaucoup de chaînes entre les deux instantanés mais qu'elles étaient toujours utilisées. La question était maintenant de savoir où ces éléments avaient été créés et qui y faisait référence. Heureusement que les instantanés capturés contenaient également ces informations appelées Retainers.

Tracking down high memory usage in Node.js

En fouillant dans les instantanés et dans la liste sans cesse réduite de chaînes, j'ai remarqué un motif de chaînes qui ressemblait à un identifiant. En cliquant dessus, je pouvais voir les objets de chaîne qui les faisaient référence – alias Retainers. C'était un tableau appelé sentEvents à partir d'un nom de classe que je pouvais reconnaître grâce au code de la bibliothèque. Tadaaa, nous avons notre coupable, une liste toujours croissante d'identifiants qui, à ce stade, je supposais qu'ils n'étaient jamais divulgués. J'ai pris un tas de clichés au fil du temps et c'était le seul endroit qui réapparaissait sans cesse comme un point chaud avec un gros delta positif.

Vérifiez le correctif

Avec ces informations, au lieu d'essayer de comprendre le code dans son intégralité, je devais me concentrer sur le but du tableau, quand il était rempli et quand il était effacé. Il y avait un seul endroit où le code poussait les éléments vers le tableau et un autre où le code les faisait ressortir, ce qui réduisait la portée du correctif.

On peut supposer que le tableau n'a pas été vidé quand il le devrait. En ignorant les détails du code, voici ce qui se passait :

  • La bibliothèque exposait des interfaces soit pour consommer, produire des événements, soit pour produire et consommer des événements.
  • Lorsqu'il consommait et produisait à la fois des événements, il devait suivre les événements produits par le processus lui-même afin de les ignorer et de ne pas les consommer à nouveau. Le sentEvents était renseigné lors de la production et effacé lorsque, en essayant de consommer, il ignorait les messages.

Pouvez-vous voir où cela va ? ? Lorsque les services utilisaient la bibliothèque uniquement pour produire des événements, les sentEvents étaient toujours remplis avec tous les événements, mais il n'y avait pas de chemin de code (consommateur) pour l'effacer.

J'ai corrigé le code pour suivre uniquement les événements en mode producteur, consommateur et déployés en staging. Même avec la charge de préparation, il était clair que le correctif aidait à réduire l'utilisation élevée de la mémoire et n'introduisait aucune régression.

Résultats

Lorsque le patch a été déployé en production, l'utilisation de la mémoire a été considérablement réduite et la fiabilité du service a été améliorée (plus de MOO).

Tracking down high memory usage in Node.js

Un effet secondaire intéressant a été la réduction de 50 % du nombre de pods nécessaires pour gérer le même trafic.

Tracking down high memory usage in Node.js

Conclusion

Cela a été une excellente opportunité d'apprentissage pour moi concernant le suivi des problèmes de mémoire dans Node.js et me familiariser davantage avec les outils disponibles.

J'ai pensé qu'il valait mieux ne pas m'attarder sur les détails de chaque outil car cela mériterait un article séparé, mais j'espère que c'est un bon point de départ pour toute personne souhaitant en savoir plus sur ce sujet ou confrontée à des problèmes similaires.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn