almessadi.
Retour à l'index

Node.js OOMKilled : Comment Trouver le Réel Problème de Mémoire_

Qu'est-ce qui cause effectivement les services Node.js à être OOMKilled, comment inspecter le tas, et quand augmenter la limite de mémoire aide plutôt qu'elle ne masque la fuite.

Publié12 mars 2024
Temps de lecture14 min read

Lorsqu'un service Node.js est OOMKilled dans Kubernetes, la cause principale n'est pas toujours "le pod a besoin de plus de mémoire" et ce n'est pas toujours "V8 a encouru une fuite".

Vous avez au moins trois éléments mobiles :

  • le tas V8
  • la mémoire en dehors du tas V8, comme les tampons natifs
  • la limite de mémoire du conteneur imposée par le runtime

Si vous ne séparez pas ces éléments, vous pouvez passer des jours à corriger le mauvais problème.

Commencez Avec le Bon Modèle Mental

process.memoryUsage() rapporte plusieurs catégories de mémoire :

const usage = process.memoryUsage();

console.log({
  rss: usage.rss,
  heapTotal: usage.heapTotal,
  heapUsed: usage.heapUsed,
  external: usage.external,
  arrayBuffers: usage.arrayBuffers,
});

La distinction importante est :

  • heapUsed correspond aux objets JavaScript gérés par V8
  • external et arrayBuffers représentent souvent de la mémoire qui compte toujours contre la limite du conteneur
  • rss est l'ensemble de résidents du processus et est généralement le chiffre que les équipes opérationnelles se préoccupent pendant les incidents

Vous pouvez avoir un tas qui semble sain et être tout de même tué parce que la mémoire native ou les tampons continuent de croître.

--max-old-space-size Est un Outil, Pas un Diagnostic

Cette option augmente la taille du tas de génération ancienne de V8 :

node --max-old-space-size=4096 server.js

C'est la bonne solution quand :

  • le service a légitimement besoin de plus de tas en direct
  • la collecte des ordures est saine
  • la limite du conteneur a suffisamment de marge

C'est la mauvaise solution quand :

  • la fuite de mémoire se trouve dans des objets de l'espace utilisateur qui devraient être collectables
  • la croissance de la mémoire est dans les tampons ou les addons natifs
  • le pod est déjà trop proche de la limite du conteneur

Augmenter la limite du tas sur un processus présentant des fuites ne fait que retarder le crash.

Causes Communes de Croissance de Mémoire

Dans les services Node en production, je vérifie généralement cela en premier :

  1. Cartes ou caches à long terme sans éviction
  2. Écouteurs d'événements attachés de manière répétée et jamais supprimés
  3. Files d'attente qui acceptent le travail plus rapidement que les travailleurs ne peuvent les épurer
  4. Charges utiles JSON volumineuses totalement tamponnées en mémoire
  5. Flux qui ont été remplacés par await response.json()
  6. Addons natifs ou bibliothèques de traitement d'images/vidéos retenant de la mémoire en dehors du tas

Aucun de ces problèmes n'est exotique. Ce sont des erreurs d'ingénierie normales sous charge.

Les Instantanés de Tas Valorisent le Frottement

Si le suspect est la croissance du tas, prenez un instantané et inspectez la taille conservée :

import { writeHeapSnapshot } from "node:v8";

const filename = writeHeapSnapshot();
console.log(`Instantané de tas écrit dans ${filename}`);

Ensuite, ouvrez l'instantané dans les outils de développement Chrome et recherchez :

  • de grands chemins de rétention
  • des tableaux ou cartes de taille inattendue
  • des objets dupliqués qui devraient avoir des durées de vie courtes
  • des closures retenant un état spécifique à la demande

La question n'est pas "quel objet est volumineux ?" mais "pourquoi cet objet est-il toujours accessible ?"

Les Fuites Se Cachent Souvent Dans le Code de Commodité

Ce modèle est plus dangereux qu'il n'y paraît :

const pending = new Map<string, RequestContext>();

export function trackRequest(id: string, ctx: RequestContext) {
  pending.set(id, ctx);
}

Sans un chemin de suppression clair, cette carte devient une base de données en mémoire accidentelle.

La solution n'est généralement pas astucieuse. C'est une discipline de cycle de vie :

  • supprimer les entrées lorsque le travail est terminé
  • limiter les caches
  • streamer de grandes charges utiles
  • préférer la pression arrière à la mise en mémoire tampon de tout

Les Conteneurs Changent le Mode de Défaillance

Dans Kubernetes, le processus rivalise avec la limite du conteneur, et non seulement avec les valeurs par défaut de V8.

Cela signifie :

  • surveillez rss, pas seulement le tas
  • laissez de la marge pour les allocations natives
  • évitez de définir --max-old-space-size proche de la limite du conteneur

Un processus avec un tas de 4 Go dans un conteneur de 4 Go n'est pas "efficace". Il est fragile.

Une Boucle d'Incident Pratique

Lorsque un service Node commence à être OOMKilled :

  1. graphique rss, heapUsed, et le volume de demandes ensemble
  2. vérifiez si la croissance se réinitialise après une baisse du trafic
  3. inspectez les chemins de code riches en tampons et le traitement des grandes charges utiles
  4. capturez un instantané de tas si la croissance du tas semble suspecte
  5. seulement alors, décidez si l'ajustement du tas est justifié

Cet ordre a son importance. Ajuster avant de comprendre crée généralement un incident plus lent, pas un meilleur système.

Lectures Complémentaires