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.

Salut @pierre-gilles , je me permets de te donner mes résultats de tests :

root@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')"
os.totalmem               = 15748 MB
process.constrainedMemory = 17592186044416 MB
process.availableMemory   = 6574 MB
root@gladys:~# 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)"'
cgroup v2 : max
cgroup v1 : absent
meminfo   : MemTotal:       16125760 kB
root@gladys:~# docker logs gladys 2>&1 | grep -i "memory_limit"
root@gladys:~# 

Bien entendu, c’est ce qui est vu depuis le LXC, pas depuis le docker de Gladys.
Mon LXC a 6Go de RAM, mon host a 16Go.

alors je n’avais pas pensé à ça pour mes problèmes qui sont réapparus :roll_eyes:

Ma base duckdb frôle les 6Go :flushed_face:
Et c’est normal les (1) et (2) ? (J’avoue ne pas me rappeler ce que j’ai fait le 22/06 …)

root@gladys:/var/lib/gladysassistant# ls -al
total 6612704
drwxr-xr-x 15 root root       4096 Jul 29 16:53  .
drwxr-xr-x 22 root root       4096 Jul 14 23:57  ..
drwxr-xr-x  3 root root       4096 Jul 29 02:55  backups
-rw-r-----  1 root root   12009472 Jun 22 11:36 'gladys-production(1).db'
-rw-r-----  1 root root  706248704 Jun 22 11:36 'gladys-production(1).duckdb'
-rw-r-----  1 root root   12009472 Jun 22 15:25 'gladys-production(2).db'
-rw-r-----  1 root root   12009472 Jul 29 22:08  gladys-production.db
-rw-r-----  1 root root      32768 Jul 29 22:08  gladys-production.db-shm
-rw-r-----  1 root root    4120032 Jul 29 22:08  gladys-production.db-wal
-rw-r-----  1 root root 5994196992 Jul 29 16:53  gladys-production.duckdb
-rw-r--r--  1 root root    9957883 Jun 22 11:36 'gladys-production.duckdb(1).wal'
-rw-r--r--  1 root root    5194409 Jun 22 15:25 'gladys-production.duckdb(2).wal'
-rw-r--r--  1 root root   15536313 Jul 29 22:08  gladys-production.duckdb.wal
drwxr-xr-x  2 root root       4096 May 11  2025  homekit
drwxr-xr-x  2 root root       4096 Jun 22 11:36 'homekit(1)'
drwxr-xr-x  2 root root       4096 Jun 22 15:25 'homekit(2)'
drwxr-xr-x  3 root root       4096 May 11  2025  matter
drwxr-xr-x  3 root root       4096 Jun 22 11:35 'matter(1)'
drwxr-xr-x  3 root root       4096 Jun 22 15:25 'matter(2)'
drwxr-xr-x  2 root root       4096 May 11  2025  mosquitto
drwxr-xr-x  2 root root       4096 Jun 22 11:36 'mosquitto(1)'
drwxr-xr-x  2 root root       4096 Jun 22 15:25 'mosquitto(2)'
drwxr-xr-x  5 1000 1000       4096 Feb 21 20:28  node-red
drwxr-xr-x  5 root root       4096 Jun 22 11:37 'node-red(1)'
drwxr-xr-x  5 root root       4096 Jun 22 15:25 'node-red(2)'

Bon j’ai mis en place un watchdog et capture de log de Claude et je vais voir ce que ça donne sur les prochains jours.
A la limite, je vais repasser de 1.5GB à 2GB comme avant fin de semaine.

@lmilcent , si tu veux les scripts et la conversation que Claude m’a fait pour mettre en place sur ton proxmox, je peux te les fournir sans soucis, il faudra ajuster les chemins.

Salut @mutmut,

Merci pour ces retours, c’est exactement ce qu’il nous fallait ! Tes résultats confirment le diagnostic, et malheureusement ils montrent aussi qu’on ne pourra pas détecter la limite automatiquement dans ton setup :

  • process.constrainedMemory = 17592186044416 : c’est exactement 2⁴⁴ octets = 16 To, la valeur « pas de limite ». Cohérent avec ton cgroup v2 à max : ton conteneur Docker n’a pas de limite mémoire propre, et les 6 Go de ton LXC sont définis dans un cgroup parent, invisible depuis l’intérieur du conteneur.
  • MemTotal = 16 Go : le /proc/meminfo vu par Gladys montre ton hôte, pas ton LXC. C’est le piège du Docker-dans-LXC : lxcfs virtualise bien le /proc du LXC, mais Docker monte son propre procfs directement depuis le kernel, en contournant lxcfs.
  • process.availableMemory = 6574 MB : trompeur, ça ressemble à tes 6 Go de LXC, mais c’est en fait la mémoire disponible de ton hôte à cet instant, qui tombe par coïncidence près de 6 Go. Inutilisable pour un calcul fiable.

Concrètement, chez toi Gladys calculait donc sa limite DuckDB sur les 16 Go de l’hôte : 30 % ≈ 4,7 Go, et jusqu’à ×2 pendant le backup ≈ 9,4 Go potentiels… dans un conteneur de 6 Go. L’OOM était garanti dès que ta base a grossi.

Donc : garde bien ton DUCKDB_MEMORY_LIMIT défini explicitement, c’est la seule solution fiable en Docker-dans-LXC, et on va le documenter clairement pour les utilisateurs Proxmox/LXC.

Deux points quand même :

  1. Tu dis crasher encore avec une limite à 1,5 Go : ça, c’est intéressant, car ça suggère qu’une partie de la mémoire de l’export échappe à la limite (le memory_limit de DuckDB gouverne son buffer pool, mais les buffers du writer Parquet et de la compression GZIP sont partiellement hors comptage). Les logs de ton watchdog juste avant un crash nous aideraient beaucoup à confirmer ça, je suis preneur !
  2. Ton docker logs | grep memory_limit vide est bizarre : Gladys logge DuckDB initialized with memory_limit=... à chaque démarrage. Tes logs ont probablement tourné, tu peux revérifier juste après un restart du conteneur ?

Côté correctif, voilà ce qu’on prépare :

  • Borner fortement la mémoire du backup lui-même : l’instance DuckDB temporaire du backup aura sa propre limite, beaucoup plus petite (~1 Go max au lieu de la limite complète). L’export est un scan séquentiel, il n’a pas besoin d’un gros cache. Le pic passera de « 2× la limite » à « limite + ~1 Go », et ça protège tout le monde, même quand la détection est impossible.
  • Passer l’export de GZIP à ZSTD : moins de mémoire, meilleure compression.
  • Mieux calculer la limite par défaut : prise en compte de la limite cgroup quand elle est visible (Docker avec --memory), et plafond absolu pour les gros hôtes. Ça ne peut pas aider ton cas LXC, mais ça évitera le problème à beaucoup d’autres.

Et pour tes fichiers dupliqués (1) et (2) du 22 juin : ce sont probablement des restes de copies que tu aurais faites à la main ? Vérifie leur taille avec un ls -lh dans le dossier. Si ce sont bien des copies datées, tu peux les supprimer pour récupérer de l’espace disque (en gardant bien les DB de prod évidemment, et leur .wal, et leur -shm).

Merci @pierre-gilles pour toutes ces info !

Après 2 jours sans plantage, ce matin j’ai eu un soucis.

Par contre les scripts mis en place avec Claude ont permis de détecter la fuite, enregistrer les logs (je t’envoie ça en PM) et de redémarrer le docker Gladys donc c’est bien sur cette partie.

Claude m’a fait une proposition de PR avec tous les retours listé dans mon précédent message :slight_smile:

Concrètement:

  • Passage de GZIP à ZSTD
  • Borner la mémoire du backup à 1Go max, pas besoin d’avoir plus de cache
  • Mieux calculer la limite par défaut

Merci d’avoir répondu plus vite que moi, étant encore en congés loin de chez moi, j’avais pas trop temps :upside_down_face:

Merci pour les patchs !

Je suis preneur de vos tests sous LXC bien-sûr ! @mutmut on croise fort les doigts pour que ça corrige ton souci !