Mémoire et performance JS/TS - 17 - Mémoire et Docker

Limites mémoire des conteneurs, OOM killer, et comment faire cohabiter Node.js avec les contraintes Docker et Kubernetes.

  1. 01 Mémoire et performance JS/TS - 00 - Pourquoi la mémoire compte meme avec un garbage collector
  2. 02 Mémoire et performance JS/TS - 01 - Stack vs Heap
  3. 03 Mémoire et performance JS/TS - 02 - Le cycle de vie de la mémoire
  4. 04 Mémoire et performance JS/TS - 03 - Le garbage collector
  5. 05 Mémoire et performance JS/TS - 04 - V8 en profondeur
  6. 06 Mémoire et performance JS/TS - 05 - Les 6 fuites mémoire classiques
  7. 07 Mémoire et performance JS/TS - 06 - Closures et mémoire
  8. 08 Mémoire et performance JS/TS - 07 - WeakRef, WeakMap et WeakSet
  9. 09 Mémoire et performance JS/TS - 08 - FinalizationRegistry : savoir quand le GC passe
  10. 10 Mémoire et performance JS/TS - 09 - DevTools Memory : investiguer dans Chrome
  11. 11 Mémoire et performance JS/TS - 10 - Profiling mémoire en Node.js
  12. 12 Mémoire et performance JS/TS - 11 - Détecter et corriger les fuites mémoire
  13. 13 Mémoire et performance JS/TS - 12 - ArrayBuffer et TypedArrays
  14. 14 Mémoire et performance JS/TS - 13 - Workers et mémoire partagee
  15. 15 Mémoire et performance JS/TS - 14 - Streams et backpressure
  16. 16 Mémoire et performance JS/TS - 15 - Fuites mémoire en React
  17. 17 Mémoire et performance JS/TS - 16 - Serveurs Node.js et mémoire
  18. 18 Mémoire et performance JS/TS - 17 - Mémoire et Docker
  19. 19 Mémoire et performance JS/TS - 18 - Optimisations mémoire
  20. 20 Mémoire et performance JS/TS - 19 - Comparaison avec d'autres langages
  21. 21 Mémoire et performance JS/TS - 20 - Tester la mémoire
  22. 22 Mémoire et performance JS/TS - 21 - Glossaire

17 - Mémoire et Docker

Ce que tu vas apprendre

  • Comment Docker limite la mémoire d'un conteneur et ce qui se passe quand tu depasses
  • Pourquoi --max-old-space-size et la limite Docker doivent etre coordonnees
  • Monitorer la mémoire des conteneurs en production
  • Les requests vs limits de Kubernetes
  • Debugger un Node.js OOM dans Docker pas a pas

Prerequisites


Exit code 137 : le tueur silencieux

Un lundi matin, je recois une alerte : le service de génération de PDF plante en boucle. Les logs montrent juste "Killed". Pas de stack trace, pas d'erreur JavaScript, pas de heap dump. Juste "Killed" et un redemarrage automatique.

Le code de sortie etait 137. En Unix, 137 = 128 + 9. Le signal 9, c'est SIGKILL. Et dans Docker, SIGKILL vient du OOM killer du kernel Linux. Le conteneur avait dépassé sa limite mémoire, et le kernel l'a abattu sans prevenir.

Les limites mémoire Docker

bash# Limite a 512 Mo
docker run --memory=512m node-app

# docker-compose
services:
  api:
    image: node-app
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

Quand un conteneur atteint sa limite mémoire, Docker (via cgroups) demande au kernel de tuer le process principal. Pas de signal catchable, pas de cleanup, pas de graceful shutdown. Le process disparaît.

C'est différent d'un crash JavaScript "heap out of memory". Le crash V8 te donne une erreur, un stack trace, et une chance de diagnostiquer. Le OOM kill te donne rien.

Le piège : Node.js ne connaît pas la limite Docker

Par défaut, V8 fixe sa limite de heap en fonction de la RAM totale de la machine. Si ta machine a 16 Go, V8 peut monter a 1.5 Go ou plus. Mais si ton conteneur est limite a 512 Mo, V8 ne le sait pas. Il va essayer d'allouer 1.5 Go dans un espace de 512 Mo.

dockerfile# Mauvais : V8 ignore la limite Docker
CMD ["node", "server.js"]

# Bon : on dit a V8 de rester sous la limite
CMD ["node", "--max-old-space-size=384", "server.js"]

La regle : --max-old-space-size doit etre inférieur a la limite Docker. Je mets généralement 75% de la limite conteneur. Pour un conteneur a 512 Mo, je mets 384 Mo. Les 128 Mo restants couvrent la stack, le code compile, les buffers natifs, et le overhead de Node.js lui-meme.

Depuis Node.js 19, le flag --max-semi-space-size est automatiquement ajuste en fonction de la mémoire disponible. Mais --max-old-space-size doit toujours etre défini manuellement dans un conteneur.

Monitoring : docker stats et au-delà

bash# Monitoring basique en temps reel
docker stats

# Sortie :
# CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %   NET I/O   BLOCK I/O
# api-1       2.3%    287MiB / 512MiB      56%     1.2kB     0B

# Memoire d'un conteneur specifique
docker stats api-1 --no-stream --format "{{.MemUsage}}"

docker stats montre la mémoire RSS du process principal. C'est la mémoire totale vue par l'OS, pas juste le heap V8. Ca inclut le code charge, les buffers natifs, la mémoire partagee.

Pour un monitoring plus serieux en production, cAdvisor (Container Advisor de Google) expose des metriques Prometheus par conteneur. Tu peux tracer la mémoire dans Grafana et poser des alertes avant d'atteindre la limite.

Kubernetes : requests vs limits

yamlapiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: api
          resources:
            requests:
              memory: "256Mi"
            limits:
              memory: "512Mi"

La différence est subtile mais critique :

  • requests : la mémoire garantie. Kubernetes utilise cette valeur pour placer le pod sur un noeud qui a assez de mémoire disponible.
  • limits : la mémoire maximale. Si le pod dépassé, le OOM killer intervient.

Si tu mets requests = limits, ton pod a une QoS "Guaranteed". Kubernetes ne le tuera qu'en dernier recours. Si requests < limits, c'est "Burstable" : le pod peut utiliser plus que sa reservation, mais il sera tue en premier si le noeud manque de mémoire.

Mon conseil : commence avec requests = limits pour les services critiques. Ca évité les surprises.

Dimensionner correctement

Comment je procede pour trouver la bonne taille :

  1. Je deploie avec une limite genereuse (1 Go)
  2. Je généré du trafic realiste pendant 24h
  3. Je regarde le pic de mémoire
  4. Je mets la limite a 1.5x le pic
  5. Je mets --max-old-space-size a 75% de cette limite
Pic observe : 300 Mo
Limite conteneur : 450 Mo (1.5x)
--max-old-space-size : 340 Mo (75% de 450)

Sur paltemps.fr, le service principal tourne avec 256 Mo de limite et 192 Mo pour V8. Ca suffit largement pour une API qui fait du CRUD et du cache LRU. Le pic réel est autour de 150 Mo.

Session de debug : Node.js OOM dans Docker

Scénario : ton conteneur redemarre avec exit code 137. Voici comment je debug ca.

bash# 1. Verifier que c'est bien un OOM
docker inspect api-1 --format="{{.State.OOMKilled}}"
# true

# 2. Voir la memoire au moment du kill
docker logs api-1 --tail 50
# Chercher les logs de process.memoryUsage() si tu les as

# 3. Lancer le conteneur avec plus de memoire et un heap dump
docker run --memory=1g \
  -e NODE_OPTIONS="--max-old-space-size=768 --heapsnapshot-signal=SIGUSR2" \
  node-app

# 4. Quand la memoire monte, envoyer SIGUSR2
docker exec api-1 kill -USR2 1
# Ca genere un fichier .heapsnapshot dans le conteneur

# 5. Copier le snapshot en local
docker cp api-1:/app/Heap.20260329.123456.7.0.1.heapsnapshot ./

# 6. Ouvrir dans Chrome DevTools > Memory > Load

Le heapsnapshot te montre exactement quels objets consomment la mémoire. Trie par "Retained Size" pour trouver les gros blocs. Cherche les objets qui ne devraient pas etre la en si grand nombre.

Multi-stage build et taille mémoire

Un détail souvent oublie : la taille de ton image Docker impacte la mémoire au démarrage. Une image node:20 fait 1 Go. Une image node:20-alpine fait 180 Mo. Moins de fichiers charges, moins de mémoire consommee.

dockerfileFROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "--max-old-space-size=384", "dist/server.js"]

Résumé

  • Le OOM killer Docker tue le process sans prevenir (exit code 137).
  • --max-old-space-size doit etre a 75% de la limite mémoire du conteneur.
  • docker stats montre la mémoire RSS, pas juste le heap V8.
  • En Kubernetes, requests = limits donne la QoS "Guaranteed".
  • Le flag --heapsnapshot-signal=SIGUSR2 permet de générer des heap dumps a la demande dans un conteneur.

Precedent : Serveurs Node.js et mémoire Suivant : Optimisations mémoire

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.