Fuite de mémoire avec un module GRPC pour Node-red (application Node.js) en Javascript

a marqué ce sujet comme résolu.

Bonjour, Je ne suis pas expert JavaScript, mais pour le boulot je dois m’y coller et j’ai besoin d’un petit coup de main pour comprendre comment m’en sortir.

Je travaille sur un appareil embarqué avec des ressources limitées (typiquement 512 Mio pour la RAM). Mon client souhaite utiliser Node-red qui utilise nodejs en arrière plan pour personnaliser certains composants.

Jusqu’ici tout va bien.

Sauf que dans mon contexte, je dois pouvoir dialoguer avec d’autres composants via le protocole GRPC grâce au module node-red-contrib-grpc. Cela fonctionne, je peux récupérer des données et les afficher sur une page web.

Mais au bout d’un certain temps, l’application Node-red crash, le noyau Linux l’a tué faute de mémoire suffisante. Cela se répète au bout de quelques heures environ. Je peux reproduire le setup via Docker sur ma machine et effectivement, même avec la dernière version de Node-red et nodejs, la consommation de RAM semble augmenter d’environ 80 Mio par heure d’utilisation sans rien faire.

Je supprime les appels GRPC de mon flux Node-red, en conservant tout le reste, et la surconsommation semble avoir disparu. La consommation mémoire est stable dans le temps ce qui pointe le problème.

J’ai regardé dans les dépôts Github de grpc-js et node-red-contrib-grpc et pas de fuites mémoires non corrigées rapportées. Donc je vais devoir analyser moi même.

En particulier ce fichier attire mon attention, car c’est la fonction que j’appelle pour faire mes requêtes. Je vois que quand un message est reçu (ce qui arrive chaque seconde), un nouveau client et un nouveau canal est crée mais ils ne sont jamais détruits, ni jamais clos. Ils ne le sont explicitement que quand le flux Node-red est déployé à nouveau (dans le sens, on le remplace par un nouveau flux) ce qui n’arrive jamais quand le produit est déployé.

Donc je me demande si la fuite ne viendrait pas de là mais d’après ces pages qui explique comment fonctionnent delete en Javascript et la gestion mémoire dans le langage, j’ai l’impression de faire finalement fausse route. Mais comme je ne suis aps à l’aise avec Javascript, je loupe peut être quelque chose.

Est-ce que je me trompe de cible ? Est-ce que ces bouts de code que je pointe sont finalement bien écrits ? Comment faire avec nodejs dans ce contexte pour analyser finement les ressources en mémoire ?

+0 -0

J’ai crée un correctif qui semble améliorer la situation :

--- a/grpc/grpc-call.js
+++ b/grpc/grpc-call.js
@@ -54,7 +54,12 @@ module.exports = function (RED) {
                                 }
                             }
                         }
-                        
+
+                        if (node.client) {
+                            grpc.closeClient(node.client)
+                            delete node.client;
+                        }
+
                         node.client = new proto[config.service](
                             REMOTE_SERVER,
                             credentials || grpc.credentials.createInsecure()
@@ -78,6 +83,10 @@ module.exports = function (RED) {
                         } else {
                             node.status({});
                             if (proto[config.service].service[config.method].responseStream) {
+                                if (node.channel) {
+                                    delete node.channel;
+                                }
+
                                 node.channel = node.client[config.method](msg.payload, metadata);
                                 node.channel.on("data", function (data) {
                                     msg.payload = data;

Mais n’étant pas développeur Javascript, est-ce que cela est propre et a un sens ? Peut être que les delete sont inutiles ? Avant de soumettre cela éventuellement au projet d’origine je souhaiterais comprendre ce que je fais :)

+0 -0

Hello,

De mémoire delete indique au garbage collector que la propriété n’est plus utilisée, donc qu’il peut supprimer sa valeur si aucune autre référence n’existe.

Ça me paraît donc une bonne logique si tu n’as effectivement plus besoin du client ou du channel.

Par contre j’aurais pensé intuitivement que le remplacement de valeur (lignes 14 et 25) aurait permis d’éliminer l’ancienne de la mémoire, effectivement… 🤔

Je soupçonne que le client non fermé explicitement a encore une référence interne quelque part donc ne peut pas être libéré, même s’il n’est plus référencé.

Je serais curieux de savoir si la fuite est présente avec seulement les modifications des lignes 9, 10 et 12. Et si oui, est-ce que la fuite est présente sans la modification de la ligne 10 (la fermeture).

PS : sans fermeture "explicite" (ou via try-with-resource) en Java, tu vas avoir surtout des problèmes de fuite de descripteurs et de verrous sur les ressources non fermées, avec des connexions tuées par la BDD ou coincées en attente… Je ne connais pas bien JS mais on peut très bien imaginer qu’à chaque seconde, le "un peu trop" de données non libérées à chaque connexion soient suffisantes pour provoquer une fuite mémoire visible.

Par contre j’aurais pensé intuitivement que le remplacement de valeur (lignes 14 et 25) aurait permis d’éliminer l’ancienne de la mémoire, effectivement… 🤔

À la lecture des documents de Mozilla, c’est ce que j’aurais pensé aussi.

Je serais curieux de savoir si la fuite est présente avec seulement les modifications des lignes 9, 10 et 12. Et si oui, est-ce que la fuite est présente sans la modification de la ligne 10 (la fermeture).

Je vais tester ce scénario aussi, j’ai pensé comme toi que la fermeture explicite est nécessaire pour libérer des ressources internes. En lisant rapidement le code de grpc-js je pensais trouver quelque chose en ce sens mais pour l’instant rien.

Merci pour les indications, je teste ça demain. :)

+0 -0

Après quelques heures le verdict semble clair, écart non mesurable entre code d’origine et code avec juste les instructions delete quand celui qui ferme la connexion seulement a une consommation mémoire stable.

+4 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte