Un guide pratique pour gérer les fuites de mémoire dans Node.js

Les fuites de mémoire sont similaires aux entités parasites sur une application. Ils pénÚtrent tranquillement dans le systÚme, au début sans causer de mal. Mais si la fuite s'avÚre suffisamment forte, elle peut entraßner la catastrophe de l'application. Par exemple - pour le ralentir fortement ou simplement pour le «tuer». L'auteur de l'article, dont nous publions la traduction aujourd'hui, suggÚre de parler des fuites de mémoire en JavaScript. En particulier, nous parlerons de la gestion de la mémoire en JavaScript, comment identifier les fuites de mémoire dans les applications réelles et comment traiter les fuites de mémoire.





Qu'est-ce qu'une fuite de mémoire?


Une fuite de mĂ©moire est, au sens large, un morceau de mĂ©moire allouĂ© Ă  une application dont cette application n'a plus besoin, mais qui ne peut pas ĂȘtre renvoyĂ©e au systĂšme d'exploitation pour une utilisation future. En d'autres termes, il s'agit d'un bloc de mĂ©moire qui est capturĂ© par l'application sans l'intention d'utiliser cette mĂ©moire Ă  l'avenir.

Gestion de la mémoire


La gestion de la mémoire est un mécanisme d'allocation de mémoire systÚme à une application qui en a besoin et un mécanisme de renvoi de mémoire inutile au systÚme d'exploitation. Il existe de nombreuses approches de la gestion de la mémoire. L'approche utilisée dépend du langage de programmation utilisé. Voici un aperçu de plusieurs approches courantes de la gestion de la mémoire:

  • . . . , . C C++. , , malloc free, .
  • . , , , . , , , . , , , , . . — JavaScript, , JVM (Java, Scala, Kotlin), Golang, Python, Ruby .
  • Application du concept de propriĂ©tĂ© de la mĂ©moire. Avec cette approche, chaque variable doit avoir son propre propriĂ©taire. DĂšs que le propriĂ©taire est hors de portĂ©e, la valeur de la variable est dĂ©truite, libĂ©rant de la mĂ©moire. Cette idĂ©e est utilisĂ©e dans Rust.

Il existe d'autres approches de la gestion de la mémoire utilisées dans différents langages de programmation. Par exemple, C ++ 11 utilise l'idiome RAII , tandis que Swift utilise le mécanisme ARC . Mais en parler dépasse le cadre de cet article. Afin de comparer les méthodes de gestion de la mémoire ci-dessus, pour comprendre leurs avantages et leurs inconvénients, nous avons besoin d'un article séparé.

JavaScript, un langage sans lequel les programmeurs Web ne peuvent pas imaginer leur travail, utilise l'idée de garbage collection. Par conséquent, nous parlerons davantage du fonctionnement de ce mécanisme.

Collecte de déchets JavaScript


Comme dĂ©jĂ  mentionnĂ©, JavaScript est un langage qui utilise le concept de garbage collection. Pendant le fonctionnement des programmes JS, un mĂ©canisme appelĂ© garbage collector est pĂ©riodiquement lancĂ©. Il dĂ©couvre quelles parties de la mĂ©moire allouĂ©e sont accessibles Ă  partir du code d'application. Autrement dit, quelles variables sont rĂ©fĂ©rencĂ©es. Si le garbage collector dĂ©couvre qu'un morceau de mĂ©moire n'est plus accessible Ă  partir du code d'application, il libĂšre cette mĂ©moire. L'approche ci-dessus peut ĂȘtre mise en Ɠuvre en utilisant deux algorithmes principaux. Le premier est ce que l'on appelle l'algorithme Mark and Sweep. Il est utilisĂ© en JavaScript. Le deuxiĂšme est le comptage des rĂ©fĂ©rences. Il est utilisĂ© en Python et PHP.


Phases Mark (marquage) et Sweep (nettoyage) de l'

algorithme Mark and Sweep Lors de la mise en Ɠuvre de l'algorithme de marquage, une liste de nƓuds racine reprĂ©sentĂ©e par des variables d'environnement globales (c'est un objet dans le navigateurwindow) est d'abord crĂ©Ă©e, puis l'arborescence rĂ©sultante est analysĂ©e des nƓuds racine aux feuilles marquĂ©s de tous rencontrĂ© sur le chemin des objets. La mĂ©moire du tas occupĂ©e par des objets sans Ă©tiquette est libĂ©rĂ©e.

Fuites de mémoire dans les applications Node.js


À ce jour, nous avons analysĂ© suffisamment de concepts thĂ©oriques liĂ©s aux fuites de mĂ©moire et Ă  la collecte des ordures. Donc, nous sommes prĂȘts Ă  voir Ă  quoi tout cela ressemble dans les applications rĂ©elles. Dans cette section, nous allons Ă©crire un serveur Node.js qui a une fuite de mĂ©moire. Nous essaierons d'identifier cette fuite Ă  l'aide de divers outils, puis nous l'Ă©liminerons.

▍ FamiliaritĂ© avec un code prĂ©sentant une fuite de mĂ©moire


À des fins de dĂ©monstration, j'ai Ă©crit un serveur Express qui a une route de fuite de mĂ©moire. Nous allons dĂ©boguer ce serveur.

const express = require('express')

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Il existe un tableau leaksqui sort du domaine d'application du code de traitement des demandes d'API. Par consĂ©quent, chaque fois que le code correspondant est exĂ©cutĂ©, de nouveaux Ă©lĂ©ments sont simplement ajoutĂ©s au tableau. Le tableau n'est jamais effacĂ©. Étant donnĂ© que le lien vers ce tableau ne disparaĂźt pas aprĂšs avoir quittĂ© le gestionnaire de demandes, le garbage collector ne libĂšre jamais la mĂ©moire qu'il utilise.

▍Appeler une fuite de mĂ©moire


Nous arrivons ici au plus intĂ©ressant. De nombreux articles ont Ă©tĂ© Ă©crits sur la façon node --inspectde dĂ©boguer les fuites de mĂ©moire du serveur, aprĂšs avoir rempli le serveur de requĂȘtes en utilisant quelque chose comme l' artillerie . Mais cette approche prĂ©sente un inconvĂ©nient important. Imaginez que vous ayez un serveur API qui a des milliers de points de terminaison. Chacun d'eux prend beaucoup de paramĂštres, dont le code spĂ©cifique sera appelĂ© dĂ©pend des caractĂ©ristiques de celui-ci. Par consĂ©quent, dans des conditions rĂ©elles, si le dĂ©veloppeur ne sait pas oĂč se trouve la fuite de mĂ©moire, il devra accĂ©der Ă  chaque API plusieurs fois en utilisant toutes les combinaisons possibles de paramĂštres pour remplir la mĂ©moire. Pour moi, ce n'est pas facile. Cependant, la solution Ă  ce problĂšme est facilitĂ©e en utilisant quelque chose commegoreplay - un systĂšme qui vous permet d'enregistrer et de "jouer" du trafic rĂ©el.

Afin de faire face à notre problÚme, nous allons faire le débogage en production. Autrement dit, nous autoriserons notre serveur à déborder de mémoire pendant son utilisation réelle (car il reçoit une variété de demandes d'API). Et aprÚs avoir constaté une augmentation suspecte de la quantité de mémoire qui lui est allouée, nous procéderons au débogage.

▍ Vidage de tas


Afin de comprendre ce qu'est un vidage de tas, nous devons d'abord dĂ©couvrir la signification du concept de tas. Si vous dĂ©crivez ce concept aussi simplement que possible, il s'avĂšre que le tas est l'endroit oĂč tout ce que la mĂ©moire est allouĂ©e tombe. Tout cela est sur le tas jusqu'Ă  ce que le ramasse-miettes en retire tout ce qui est jugĂ© inutile. Un vidage de segment de mĂ©moire est un instantanĂ© de l'Ă©tat actuel du segment de mĂ©moire. Le vidage contient toutes les variables internes et variables dĂ©clarĂ©es par le programmeur. Il reprĂ©sente toute la mĂ©moire allouĂ©e sur le tas au moment de la rĂ©ception du vidage.

Par conséquent, si nous pouvions d'une maniÚre ou d'une autre comparer le vidage de tas du serveur qui venait de commencer avec le vidage du tas de serveur, qui fonctionnait depuis longtemps et débordait de mémoire, nous pourrions identifier les objets suspects dont l'application n'a pas besoin, mais qui ne sont pas supprimés par le garbage collector.

Avant de poursuivre la conversation, parlons de la façon de créer des vidages de tas. Pour résoudre ce problÚme, nous utiliserons le paquetage npm heapdump , qui vous permet d'obtenir par programme un vidage du tas du serveur.

Installez le package:

npm i heapdump

Nous allons apporter quelques modifications au code du serveur qui nous permettront d'utiliser ce package:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
  heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a bloated server written to", filename);

    res.status(200).send({msg: "successfully took a heap dump"})
  });
});

app.listen(port, () => {
  heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a fresh server written to", filename);
  });
});

Ici, nous avons utilisĂ© ce package pour vider un serveur fraĂźchement lancĂ©. Nous avons Ă©galement crĂ©Ă© une API /heapdumpconçue pour crĂ©er un tas lors de l'accĂšs. Nous allons nous tourner vers cette API au moment oĂč nous nous rendons compte que le serveur a commencĂ© Ă  consommer trop de mĂ©moire.

Si votre serveur s'exĂ©cute dans un cluster Kubernetes, vous ne pourrez pas, sans effort supplĂ©mentaire, vous tourner vers ce mĂȘme pod dont le serveur s'exĂ©cute et qui consomme trop de mĂ©moire. Pour ce faire, vous pouvez utiliser la redirection de port . De plus, comme vous n'aurez pas accĂšs au systĂšme de fichiers dont vous avez besoin pour tĂ©lĂ©charger des fichiers de vidage, il serait prĂ©fĂ©rable de tĂ©lĂ©charger ces fichiers vers un stockage cloud externe (comme S3).

▍ DĂ©tection de fuite de mĂ©moire


Et maintenant, le serveur est dĂ©ployĂ©. Il travaille depuis plusieurs jours. Il reçoit beaucoup de requĂȘtes (dans notre cas, uniquement des requĂȘtes du mĂȘme type) et nous avons fait attention Ă  l'augmentation de la quantitĂ© de mĂ©moire consommĂ©e par le serveur. Une fuite de mĂ©moire peut ĂȘtre dĂ©tectĂ©e Ă  l'aide d'outils de surveillance comme Express Status Monitor , Clinic , Prometheus . AprĂšs cela, nous appelons l'API pour vider le tas. Ce vidage contiendra tous les objets que le garbage collector n'a pas pu supprimer.

Voici Ă  quoi ressemble la requĂȘte pour crĂ©er un vidage:

curl --location --request GET 'http://localhost:3000/heapdump'

Lorsqu'un vidage de tas est crĂ©Ă©, le garbage collector est obligĂ© de s'exĂ©cuter. Par consĂ©quent, nous n'avons pas Ă  nous soucier des objets qui pourraient ĂȘtre supprimĂ©s par le garbage collector Ă  l'avenir, mais qui sont toujours sur le tas. C'est-Ă -dire sur les objets lorsque vous travaillez avec lesquels des fuites de mĂ©moire ne se produisent pas.

Une fois que nous avons les deux vidages à notre disposition (un vidage d'un serveur fraßchement lancé et un vidage d'un serveur qui fonctionne depuis un certain temps), nous pouvons commencer à les comparer.

L'obtention d'un vidage de la mĂ©moire est une opĂ©ration de blocage qui nĂ©cessite beaucoup de mĂ©moire. Par consĂ©quent, il doit ĂȘtre effectuĂ© avec prudence. Vous pouvez en savoir plus sur les problĂšmes possibles rencontrĂ©s lors de cette opĂ©ration ici .

Lancez Chrome et appuyez sur la touche.F12. Cela conduira à la découverte d'outils de développement. Ici, vous devez accéder à l'onglet Memoryet charger les deux instantanés de mémoire.


Le téléchargement mémoire amoncelés sur l'onglet Mémoire des outils de développement Chrome

AprÚs avoir téléchargédeux instantanés, vous devez changerperspectivepourComparisonet cliquez sur l'instantané de la mémoire du serveur quitravaillé pendantcertain temps.


Commencer à comparer des instantanés

Ici, nous pouvons analyser la colonneConstructoret rechercher des objets que le garbage collector ne peut pas supprimer. La plupart de ces objets seront reprĂ©sentĂ©s par des liens internes que les nƓuds utilisent. Ici, il est utile d'utiliser une astuce, qui consiste Ă  trier la liste par champAlloc. Size. Cela trouvera rapidement les objets qui utilisent le plus de mĂ©moire. Si vous dĂ©veloppez le bloc(array), puis -(object elements), vous pouvez voir un tableauleakscontenant un grand nombre d'objets qui ne peuvent pas ĂȘtre supprimĂ©s Ă  l'aide du garbage collector.


Analyse d'une baie suspecte

Cette technique nous permettra d'accéder à la baieleakset de comprendre que c'est l'opération incorrecte avec elle qui provoque une fuite de mémoire.

LeakFix mémoire fuite


Maintenant que nous savons que le «coupable» est un tableau leaks, nous pouvons analyser le code et dĂ©couvrir que le problĂšme est que le tableau est dĂ©clarĂ© en dehors du gestionnaire de requĂȘtes. En consĂ©quence, il s'avĂšre que le lien vers celui-ci n'est jamais supprimĂ©. Pour rĂ©soudre ce problĂšme est assez simple - il suffit de transfĂ©rer la dĂ©claration du tableau au gestionnaire:

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  const leaks = [];

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

Afin de vérifier l'efficacité des mesures prises, il suffit de répéter les étapes ci-dessus et de comparer à nouveau les images de tas.

Sommaire


Les fuites de mémoire se produisent dans différentes langues. En particulier, dans - ceux qui utilisent des mécanismes de collecte des ordures. Par exemple, en JavaScript. Il n'est généralement pas difficile de réparer une fuite - les vraies difficultés ne surviennent que lorsque vous la recherchez.

Dans cet article, vous vous ĂȘtes familiarisĂ© avec les bases de la gestion de la mĂ©moire et la façon dont la gestion de la mĂ©moire est organisĂ©e dans diffĂ©rentes langues. Ici, nous avons reproduit un scĂ©nario rĂ©el de fuite de mĂ©moire et dĂ©crit une mĂ©thode de dĂ©pannage.

Chers lecteurs! Avez-vous rencontré des fuites de mémoire dans vos projets Web?


All Articles