Surconsommation de mémoire par le backup de gateway?

Salut,

Je ne sais pas si je suis le seul, mais j’ai des erreurs de OOM régulièrement, lorsque j’attribue 8Gb a Gladys. Cela provoque un redémarrage de Gladys toute les nuits.

Après des jours de débug, sans comprendre la cause, j’ai utilisé Claude pour avancer et identifier une meilleure hypothèse. Et les jours suivants le problème est identifié : la manière dont la base de données est sauvegardée.

Contexte : je garde tous les états a l’infini, tant que j’ai du disque.

Est ce que je suis le seul a qui ça arrive ?


Bug : le backup Gateway peut provoquer un OOM-kill du process Node

Composants concernés :
server/lib/gateway/gateway.backup.js
server/models/index.js (fonction duckDbCreateBackupInstance)

Le problème : duckDbCreateBackupInstance() ouvre une nouvelle instance DuckDB séparée sur le même fichier .duckdb que l’instance principale (déjà active en lecture/écriture) :

const backupDatabase = await DuckDBInstance.create(duckDbFilePath, {
  memory_limit: duckDbMemoryLimit,
  access_mode: 'READ_ONLY',
});

Or la documentation officielle du client Node.js « Neo » de DuckDB dit explicitement :

« Multiple instances in the same process should not attach the same database. »

Le mécanisme prévu pour éviter ça est DuckDBInstanceCache.getOrCreateInstance(path), qui réutilise la même instance (donc le même buffer pool / memory_limit) pour un même fichier. Le code actuel appelle DuckDBInstance.create() directement, ce qui crée deux buffer pools indépendants, chacun plafonné au même DUCKDB_MEMORY_LIMIT.

Résultat : le pire cas mémoire de DuckDB seul devient 2 × memory_limit au lieu de 1 × memory_limit.

Aggravant : la doc DuckDB précise aussi que memory_limit « only applies to the buffer manager » — les buffers de compression (COMPRESSION GZIP utilisé dans l’EXPORT DATABASE) peuvent consommer en plus de cette limite.

Preuve en prod (2 crashs identiques, logs à l’appui) :

  • RSS stable ~2,5 Go avant backup (dont ~1,2 Go de BASE_TABLE DuckDB)
  • Passage à ~4,6 Go en moins d’une minute, juste après le log « Backing up DuckDB into a Parquet folder »
  • Process tué (ExitCode=137) avant d’atteindre le log « Closing DuckDB backup instance to release memory » dans le finally — donc le crash survient bien pendant l’appel EXPORT DATABASE … FORMAT PARQUET COMPRESSION GZIP lui-même, pas après.
  • Reproduit à l’identique 2 jours de suite, toujours au moment du backup Gateway quotidien.

Ce qui est demandé :

  1. Faire en sorte que duckDbCreateBackupInstance() utilise DuckDBInstanceCache.getOrCreateInstance() au lieu de DuckDBInstance.create(), pour que le backup partage le buffer pool de l’instance principale plutôt que d’en ouvrir un second.
  2. Envisager de remplacer COMPRESSION GZIP par ZSTD ou SNAPPY dans l’EXPORT DATABASE (moins de buffers de compression, export plus rapide).
  3. Éventuellement documenter que DUCKDB_MEMORY_LIMIT doit être dimensionné en anticipant que deux instances peuvent coexister pendant un backup (donc recommander une valeur ≤ 25% de la RAM allouée au conteneur, pas 30-50%), tant que le point 1 n’est pas corrigé.

Contexte pour situer l’impact : installation avec ~8 Go alloués au conteneur, BASE_TABLE d’environ 1,1-1,2 Go d’historique de capteurs — donc pas un cas extrême, ce comportement peut probablement toucher d’autres installations avec un historique conséquent.

Et bien figure toi que j’ai le même soucis depuis 7 jours !
Par contre je dois redémarrer tous les matins mon LXC docker avec Gladys car plus rien ne répond.

Je suis avec Claude qui doit me pondre un watchdog et un restart auto après plantage, avec une récup des logs juste avant l’OOM car après cela je n’ai plus accès à rien.

Je te tiens au courant.

Merci du retour, je vais regarder !

Pour la première fois ce matin, ça n’a pas planté, pas de OOM :




Et je n’ai pas encore mis le script en place :frowning:

Depuis que j’ai modifié mon docker compose pour limiter DuckDB a 2Gb max au lieu de 4Go, plus de plantage (cette nuit en tout cas). Ce qui confirme que c’est la bonne direction.

Le mien était déjà limité à 2Go depuis les premiers soucis d’OOM.
Quand ça a recommencé il y a 1 semaine, j’ai même limité à 1.5Go mais sans succès.

version: '3'

services:
  gladys:
    image: gladysassistant/gladys:v4
    container_name: gladys
    restart: always
    privileged: true
    network_mode: host
    cgroup: host
    logging:
      driver: "json-file"
      options:
        max-size: 10m
    environment:
      NODE_ENV: production
      SQLITE_FILE_PATH: /var/lib/gladysassistant/gladys-production.db
      SERVER_PORT: 8420
      TZ: Europe/Paris
      DUCKDB_MEMORY_LIMIT: 1500MB # limitation utilisation memoire duckDB

Normalement @pierre-gilles a déjà mis en place une limitation de xx% pour duckDB en automatique (je ne me rappelle plus le chiffre exact), ce qui fonctionne très bien sauf pour les installations avec proxmox où la valeur de la RAM du LXC n’est pas bien récupérée par docker, et dans ce cas il faut effectivement limiter (sauf si RAM LXC = RAM host).

C’est exactement ça oui, LXC avec 8Gio, docker qui voit les 64Gio du serveur, et OOM.

C’est clairement pas un problème « mainstream » mais c’est deja arrivé plusieurs fois a ce que je vois !

Salut @lmilcent,

Effectivement, il y a 3 phénomènes distincts ici :smiley:

1. L’instance séparée pour le backup, c’est voulu. On ouvre bien une instance DuckDB 100 % séparée pour générer le backup, et oui, du coup ça permet à DuckDB d’utiliser temporairement 2× la limite autorisée pendant le backup. On fait ça pour isoler le backup de la production et surtout pour pouvoir relâcher la RAM à la fin : fermer l’instance rend toute sa mémoire à l’OS. Si on réutilisait l’instance de production, on resterait certes sous les ×2 pendant le backup, mais l’export remplirait le buffer pool de prod jusqu’à 100 % de la limite sans jamais la relâcher, pour DuckDB c’est du cache, il le garde à vie. Or garder toute la base en cache à cause d’un backup de 10 minutes par nuit, c’est pas terrible ^^

2. Dans ton cas, le vrai problème vient de LXC. La limite RAM du container n’est pas visible depuis l’intérieur : Node calcule les 30 % sur les 64 Go du serveur, soit ~19 Go de limite DuckDB… beaucoup trop pour un container de 8 Go, et ×2 pendant le backup. En attendant, tu peux forcer une limite adaptée dans la config de ton container, par exemple :

DUCKDB_MEMORY_LIMIT=2GB

(~25 % des 8 Go alloués, ça laisse la place pour le pic du backup.)

3. Pourquoi seulement maintenant ? Tu n’avais probablement pas vu le problème les mois précédents car ta base était plus petite. Avec l’accumulation des états, l’export monte de plus en plus haut en RAM, jusqu’à déclencher l’OOM.

Pour améliorer la détection automatique de la limite mémoire, j’ai des pistes (lire la limite cgroup via process.constrainedMemory() de Node), mais de mémoire on avait fait pas mal d’essais peu fructueux sur LXC…

Pour re-tester à nouveau, est-ce que tu pourrais exécuter ces 3 commandes et poster le résultat ?

Ce que Node voit depuis le container Gladys :

docker exec gladys node -e "const os=require('os'); \
console.log('os.totalmem               =', Math.round(os.totalmem()/1048576), 'MB'); \
console.log('process.constrainedMemory =', Math.round((process.constrainedMemory()||0)/1048576), 'MB'); \
console.log('process.availableMemory   =', process.availableMemory ? Math.round(process.availableMemory()/1048576)+' MB' : 'n/a')"

Les limites cgroup vues du container :

docker exec gladys sh -c 'echo "cgroup v2 : $(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo absent)"; \
echo "cgroup v1 : $(cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || echo absent)"; \
echo "meminfo   : $(head -1 /proc/meminfo)"'

La limite que Gladys a réellement appliquée :

docker logs gladys 2>&1 | grep -i "memory_limit"

Si process.constrainedMemory renvoie bien tes 8 Go (ou que memory.max / cgroup v1 montre la vraie limite), on pourra corriger le calcul automatiquement pour tout le monde. S’il renvoie 0 ou la RAM de l’hôte, on partira plutôt sur de la doc en plus sur DUCKDB_MEMORY_LIMIT.