Node.js: gestion de la mémoire disponible pour les applications s'exécutant dans des conteneurs

Lors de l'exécution d'applications Node.js dans des conteneurs Docker, les paramÚtres de mémoire traditionnels ne fonctionnent pas toujours comme prévu. Le matériel, dont nous publions la traduction aujourd'hui, est consacré à trouver la réponse à la question de savoir pourquoi. Il fournira également des recommandations pratiques pour gérer la mémoire disponible pour les applications Node.js s'exécutant dans des conteneurs.



Examen des recommandations


Supposons qu'une application Node.js s'exĂ©cute dans un conteneur avec une limite de mĂ©moire dĂ©finie. Si nous parlons de Docker, l'option --memory pourrait ĂȘtre utilisĂ©e pour dĂ©finir cette limite. Quelque chose de similaire est possible lorsque vous travaillez avec des systĂšmes d'orchestration de conteneurs. Dans ce cas, il est recommandĂ© d'utiliser l' --max-old-space-size lors du dĂ©marrage de l'application Node.js. Cela vous permet d'informer la plate-forme de la quantitĂ© de mĂ©moire dont elle dispose et de prendre Ă©galement en compte le fait que cette quantitĂ© doit ĂȘtre infĂ©rieure Ă  la limite dĂ©finie au niveau du conteneur.

Lorsque l'application Node.js s'exĂ©cute Ă  l'intĂ©rieur du conteneur, dĂ©finissez la capacitĂ© de la mĂ©moire disponible en fonction de la valeur maximale d'utilisation de la mĂ©moire active par l'application. Cela se fait si les limites de mĂ©moire du conteneur peuvent ĂȘtre configurĂ©es.

Parlons maintenant plus en détail du problÚme de l'utilisation de la mémoire dans les conteneurs.

Docker Memory Limit


Par défaut, les conteneurs n'ont pas de limites de ressources et peuvent utiliser autant de mémoire que le permet le systÚme d'exploitation. La commande docker run dispose d'options de ligne de commande qui vous permettent de définir des limites concernant l'utilisation de la mémoire ou les ressources du processeur.

La commande de lancement de conteneur peut ressembler Ă  ceci:

 docker run --memory <x><y> --interactive --tty <imagename> bash 

Veuillez noter ce qui suit:

  • x est la limite de la quantitĂ© de mĂ©moire disponible pour le conteneur, exprimĂ©e en unitĂ©s de y .
  • y peut prendre la valeur b (octets), k (kilo-octets), m (mĂ©gaoctets), g (gigaoctets).

Voici un exemple de commande de lancement de conteneur:

 docker run --memory 1000000b --interactive --tty <imagename> bash 

Ici, la limite de mémoire est fixée à 1000000 octets.

Pour vérifier la limite de mémoire définie au niveau du conteneur, vous pouvez, dans le conteneur, exécuter la commande suivante:

 cat /sys/fs/cgroup/memory/memory.limit_in_bytes 

Parlons du comportement du systÚme lors de la spécification de la limite de mémoire de l'application Node.js à l'aide de la --max-old-space-size . Dans ce cas, cette limite de mémoire correspondra à la limite définie au niveau du conteneur.

Ce qui est appelĂ© "ancien espace" dans le nom de la clĂ© est l'un des fragments du tas contrĂŽlĂ© par V8 (l'endroit oĂč les "anciens" objets JavaScript sont placĂ©s). Cette clĂ©, si vous n'entrez pas dans les dĂ©tails que nous touchons ci-dessous, contrĂŽle la taille maximale du tas. Les dĂ©tails sur les commutateurs de ligne de commande Node.js peuvent ĂȘtre trouvĂ©s ici .

En général, lorsqu'une application essaie d'utiliser plus de mémoire que celle disponible dans le conteneur, son opération se termine.

Dans l'exemple suivant (le fichier d'application est appelé test-fatal-error.js ), les objets MyRecord sont placés dans le tableau de list , avec un intervalle de 10 millisecondes. Cela conduit à une croissance incontrÎlée du tas, simulant une fuite de mémoire.

 'use strict'; const list = []; setInterval(()=> { const record = new MyRecord(); list.push(record); },10); function MyRecord() { var x='hii'; this.name = x.repeat(10000000); this.id = x.repeat(10000000); this.account = x.repeat(10000000); } setInterval(()=> { console.log(process.memoryUsage()) },100); 

Veuillez noter que tous les exemples de programmes dont nous allons discuter ici sont placĂ©s dans l'image Docker, qui peut ĂȘtre tĂ©lĂ©chargĂ©e Ă  partir du Docker Hub:

 docker pull ravali1906/dockermemory 

Vous pouvez utiliser cette image pour des expériences indépendantes.

De plus, vous pouvez emballer l'application dans un conteneur Docker, collecter l'image et l'exécuter avec la limite de mémoire:

 docker run --memory 512m --interactive --tty ravali1906/dockermemory bash 

Ici ravali1906/dockermemory est le nom de l'image.

Vous pouvez maintenant démarrer l'application en spécifiant une limite de mémoire qui dépasse la limite de conteneur:

 $ node --max_old_space_size=1024 test-fatal-error.js { rss: 550498304, heapTotal: 1090719744, heapUsed: 1030627104, external: 8272 } Killed 

Ici, le --max_old_space_size représente la limite de mémoire indiquée en mégaoctets. La méthode process.memoryUsage() donne des informations sur l'utilisation de la mémoire. Les valeurs sont exprimées en octets.

À un certain moment, l'application est abandonnĂ©e de force. Cela se produit lorsque la quantitĂ© de mĂ©moire utilisĂ©e par lui franchit une certaine frontiĂšre. Quelle est cette frontiĂšre? De quelles limitations sur la quantitĂ© de mĂ©moire peut-on parler?

Le comportement attendu d'une application exécutée avec la clé est - max-old-space-size


Par défaut, la taille de segment de mémoire maximale dans Node.js (jusqu'à la version 11.x) est de 700 Mo sur les plates-formes 32 bits et de 1400 Mo sur les plates-formes 64 bits. Vous pouvez en savoir plus sur la définition de ces valeurs ici .

En thĂ©orie, si vous utilisez la --max-old-space-size pour --max-old-space-size limite de mĂ©moire qui dĂ©passe la limite de mĂ©moire du conteneur, vous pouvez vous attendre Ă  ce que l'application soit arrĂȘtĂ©e par le mĂ©canisme de sĂ©curitĂ© du noyau du noyau Linux OOM Killer.

En réalité, cela peut ne pas arriver.

Le comportement réel de l'application exécutée avec la clé est max-old-space-size


L'application, immédiatement aprÚs le lancement, n'alloue pas toute la mémoire dont la limite est spécifiée à l'aide de --max-old-space-size . La taille du tas JavaScript dépend des besoins de l'application. Vous pouvez déterminer la quantité de mémoire utilisée par l'application en fonction de la valeur du champ heapUsed de l'objet renvoyé par la méthode process.memoryUsage() . En fait, nous parlons de la mémoire allouée dans le tas pour les objets.

Par conséquent, nous concluons que l'application sera --memory force si la taille du --memory est supérieure à la limite définie par la clé --memory démarrage du conteneur.

Mais en réalité, cela peut ne pas se produire non plus.

Lors du profilage des applications Node.js gourmandes en ressources qui s'exĂ©cutent dans des conteneurs avec une limite de mĂ©moire donnĂ©e, les modĂšles suivants peuvent ĂȘtre observĂ©s:

  1. OOM Killer est dĂ©clenchĂ© bien plus tard que le moment oĂč les heapUsed heapTotal et heapUsed sont nettement supĂ©rieures aux limites de la mĂ©moire.
  2. OOM Killer ne répond pas au dépassement des limites.

Une explication du comportement des applications Node.js dans les conteneurs


Un conteneur supervise un indicateur important des applications qui s'exécutent dessus. Il s'agit de RSS (taille de l'ensemble résident). Cet indicateur représente une certaine partie de la mémoire virtuelle de l'application.

De plus, c'est un morceau de mémoire qui est alloué à l'application.

Mais ce n'est pas tout. RSS fait partie de la mémoire active allouée à l'application.

Toute la mĂ©moire allouĂ©e Ă  une application peut ne pas ĂȘtre active. Le fait est que la «mĂ©moire allouĂ©e» n'est pas nĂ©cessairement allouĂ©e physiquement jusqu'Ă  ce que le processus commence Ă  vraiment l'utiliser. De plus, en rĂ©ponse aux demandes d'allocation de mĂ©moire provenant d'autres processus, le systĂšme d'exploitation peut vider des parties inactives de la mĂ©moire d'application dans le fichier d'Ă©change et transfĂ©rer l'espace libĂ©rĂ© vers d'autres processus. Et lorsque l'application aura Ă  nouveau besoin de ces morceaux de mĂ©moire, ils seront extraits du fichier d'Ă©change et retournĂ©s Ă  la mĂ©moire physique.

La mĂ©trique RSS indique la quantitĂ© de mĂ©moire active et disponible pour l'application dans son espace d'adressage. C'est lui qui influence la dĂ©cision sur l'arrĂȘt forcĂ© de l'application.

Preuve


▍ Exemple n ° 1. Une application qui alloue de la mĂ©moire pour un tampon


L'exemple suivant, buffer_example.js , montre un programme qui alloue de la mémoire pour un tampon:

 const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024) console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024))) 

Pour que la quantité de mémoire allouée par le programme dépasse la limite définie lors du lancement du conteneur, exécutez d'abord le conteneur avec la commande suivante:

 docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash 

AprÚs cela, exécutez le programme:

 $ node buffer_example 2000 2000 16 

Comme vous pouvez le voir, le systÚme n'a pas terminé le programme, bien que la mémoire allouée par le programme dépasse la limite de conteneur. Cela est dû au fait que le programme ne fonctionne pas avec toute la mémoire allouée. RSS est trÚs petit, il ne dépasse pas la limite de mémoire du conteneur.

▍ Exemple n ° 2. Application remplissant le tampon avec des donnĂ©es


Dans l'exemple suivant, buffer_example_fill.js , la mémoire n'est pas seulement allouée, mais également remplie de données:

 const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x') console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024))) 

Exécutez le conteneur:

 docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash 

AprÚs cela, exécutez l'application:

 $ node buffer_example_fill.js 2000 2000 984 

Apparemment, mĂȘme maintenant, l'application ne se termine pas! Pourquoi? Le fait est que lorsque la quantitĂ© de mĂ©moire active atteint la limite dĂ©finie au dĂ©marrage du conteneur et qu'il y a de la place dans le fichier d'Ă©change, certaines des anciennes pages de la mĂ©moire de processus sont dĂ©placĂ©es vers le fichier d'Ă©change. La mĂ©moire libĂ©rĂ©e est mise Ă  la disposition du mĂȘme processus. Par dĂ©faut, Docker alloue un espace pour le fichier d'Ă©change Ă©gal Ă  la limite de mĂ©moire dĂ©finie Ă  l'aide de l'indicateur --memory . Compte tenu de cela, nous pouvons dire que le processus a 2 Go de mĂ©moire - 1 Go dans la mĂ©moire active et 1 Go dans le fichier d'Ă©change. En effet, du fait que l'application peut utiliser sa propre mĂ©moire, dont le contenu est temporairement dĂ©placĂ© vers le fichier d'Ă©change, la taille de l'index RSS est dans la limite du conteneur. Par consĂ©quent, l'application continue de fonctionner.

▍ Exemple n ° 3. Une application qui remplit un tampon avec des donnĂ©es s'exĂ©cutant dans un conteneur qui n'utilise pas de fichier d'Ă©change


Voici le code que nous allons expĂ©rimenter ici (c'est le mĂȘme fichier buffer_example_fill.js ):

 const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x') console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024))) 

Cette fois, exécutez le conteneur, en configurant explicitement les fonctionnalités de travail avec le fichier d'échange:

 docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash 

Lancez l'application:

 $ node buffer_example_fill.js 2000 Killed 

Voir le message Killed ? Lorsque la valeur de la clĂ© --memory-swap est Ă©gale Ă  la --memory clĂ© --memory , cela indique au conteneur qu'il ne doit pas utiliser le fichier d' --memory-swap . De plus, par dĂ©faut, le noyau du systĂšme d'exploitation dans lequel s'exĂ©cute le conteneur lui-mĂȘme peut vider une certaine quantitĂ© de pages mĂ©moire anonymes utilisĂ©es par le conteneur dans le fichier d'Ă©change. --memory-swappiness sur 0 , nous dĂ©sactivons cette fonctionnalitĂ©. Par consĂ©quent, il s'avĂšre que le fichier d'Ă©change n'est pas utilisĂ© Ă  l'intĂ©rieur du conteneur. Le processus se termine lorsque la mĂ©trique RSS dĂ©passe la limite de mĂ©moire du conteneur.

Recommandations générales


Lorsque les applications Node.js sont lancées avec la --max-old-space-size , dont la valeur dépasse la limite de mémoire définie au démarrage du conteneur, il peut sembler que Node.js «ne fait pas attention» à la limite du conteneur. Mais, comme le montrent les exemples précédents, la raison évidente de ce comportement est le fait que l'application n'utilise tout simplement pas le volume de --max-old-space-size entier spécifié avec l' --max-old-space-size .

N'oubliez pas que l'application ne se comportera pas toujours de la mĂȘme façon si elle utilise plus de mĂ©moire que celle disponible dans le conteneur. Pourquoi? Le fait est que la mĂ©moire active (RSS) du processus est influencĂ©e par de nombreux facteurs externes que l’application elle-mĂȘme ne peut pas influencer. Ils dĂ©pendent de la charge du systĂšme et des caractĂ©ristiques de l'environnement. Par exemple, il s'agit des fonctionnalitĂ©s de l'application elle-mĂȘme, du niveau de parallĂ©lisme dans le systĂšme, des fonctionnalitĂ©s du planificateur du systĂšme d'exploitation, des fonctionnalitĂ©s du garbage collector, etc. De plus, ces facteurs, d'un lancement Ă  l'autre, peuvent changer.

Recommandations sur la dĂ©finition de la taille du segment Node.js pour les cas oĂč vous pouvez contrĂŽler cette option, mais pas avec des restrictions de mĂ©moire au niveau du conteneur


  • ExĂ©cutez l'application Node.js minimale dans le conteneur et mesurez la taille RSS statique (dans mon cas, pour Node.js 10.x, cela reprĂ©sente environ 20 Mo).
  • Le tas Node.js contient non seulement l'ancien_espace, mais Ă©galement d'autres (tels que nouvel_espace, espace_code, etc.). Par consĂ©quent, si vous prenez en compte la configuration standard de la plate-forme, vous devez vous fier au fait que le programme aura besoin d'environ 20 Mo de mĂ©moire supplĂ©mentaire. Si les paramĂštres standard ont changĂ©, ces changements doivent Ă©galement ĂȘtre pris en compte.
  • Maintenant, nous devons soustraire la valeur obtenue (supposons qu'elle sera de 40 Mo) de la quantitĂ© de mĂ©moire disponible dans le conteneur. Ce qui reste est une valeur qui, sans crainte que l' --max-old-space-size du programme ne manque de mĂ©moire, peut ĂȘtre spĂ©cifiĂ©e comme valeur clĂ© - --max-old-space-size .

Recommandations pour la dĂ©finition de limites de mĂ©moire de conteneur pour les cas oĂč ce paramĂštre peut ĂȘtre contrĂŽlĂ©, mais les paramĂštres d'application Node.js ne sont pas


  • ExĂ©cutez l'application dans des modes qui vous permettent de connaĂźtre les valeurs maximales de la mĂ©moire consommĂ©e par celle-ci.
  • Analysez le score RSS. En particulier, ici, avec la mĂ©thode process.memoryUsage() , la commande Linux top peut ĂȘtre utile.
  • Pourvu que dans le conteneur dans lequel il est prĂ©vu d'exĂ©cuter l'application, rien ne soit exĂ©cutĂ©, la valeur obtenue peut ĂȘtre utilisĂ©e comme limite de mĂ©moire du conteneur. Pour ĂȘtre sĂ»r, il est recommandĂ© de l'augmenter d'au moins 10%.

Résumé


Dans Node.js 12.x, certains des problĂšmes abordĂ©s ici sont rĂ©solus en ajustant de maniĂšre adaptative la taille du tas, qui est effectuĂ©e en fonction de la quantitĂ© de RAM disponible. Ce mĂ©canisme fonctionne Ă©galement lors de l'exĂ©cution d'applications Node.js dans des conteneurs. Mais les paramĂštres peuvent diffĂ©rer des paramĂštres par dĂ©faut. Cela se produit, par exemple, lorsque la clĂ© --max_old_space_size Ă©tĂ© utilisĂ©e lors du dĂ©marrage de l'application. Pour de tels cas, tout ce qui prĂ©cĂšde reste pertinent. Cela suggĂšre que toute personne qui exĂ©cute des applications Node.js dans des conteneurs doit ĂȘtre prudente et responsable concernant les paramĂštres de mĂ©moire. De plus, la connaissance des limites standard d'utilisation de la mĂ©moire, qui est plutĂŽt conservatrice, peut amĂ©liorer les performances des applications en modifiant dĂ©libĂ©rĂ©ment ces limites.

Chers lecteurs! Avez-vous manqué de problÚmes de mémoire lors de l'exécution des applications Node.js dans des conteneurs Docker?



Source: https://habr.com/ru/post/fr454522/


All Articles