Dans cet article, je partagerai mon approche pour détecter et corriger l'utilisation élevée de la mémoire dans Node.js.
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 :
Ci-dessous le tableau de bord qui était lié au ticket :
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.
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.
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.
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 :
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 ?
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
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
kubectl port-forward <nodejs-pod-name> 9229
sinon, assurez-vous que vos paramètres de découverte de cible sont correctement configurés
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.
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.
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.
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 :
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.
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).
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.
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!