¿Consumo excesivo de memoria por el backup del gateway?

Hola,

No sé si soy el único, pero tengo errores de OOM regularmente cuando asigno 8Gb a Gladys. Esto provoca un reinicio de Gladys todas las noches.

Después de días de depuración, sin entender la causa, utilicé Claude para avanzar e identificar una mejor hipótesis. Y los días siguientes se identificó el problema: la manera en que se guarda la base de datos.

Contexto: guardo todos los estados de forma infinita, siempre que tenga disco.

¿Soy el único al que le pasa?


Error: el backup Gateway puede provocar un OOM-kill del proceso Node

Componentes afectados:
server/lib/gateway/gateway.backup.js
server/models/index.js (función duckDbCreateBackupInstance)

El problema: duckDbCreateBackupInstance() abre una nueva instancia DuckDB separada en el mismo archivo .duckdb que la instancia principal (ya activa en lectura/escritura):

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

Sin embargo, la documentación oficial del cliente Node.js « Neo » de DuckDB dice explícitamente:

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

El mecanismo previsto para evitar esto es DuckDBInstanceCache.getOrCreateInstance(path), que reutiliza la misma instancia (por lo tanto, el mismo buffer pool / memory_limit) para un mismo archivo. El código actual llama a DuckDBInstance.create() directamente, lo que crea dos buffer pools independientes, cada uno limitado al mismo DUCKDB_MEMORY_LIMIT.

Resultado: el peor caso de memoria de DuckDB solo se convierte en 2 × memory_limit en lugar de 1 × memory_limit.

Agregando: la documentación de DuckDB también especifica que memory_limit « only applies to the buffer manager » — los buffers de compresión (COMPRESSION GZIP utilizado en el EXPORT DATABASE) pueden consumir además de este límite.

Prueba en producción (2 crashs idénticos, logs incluidos):

  • RSS estable ~2,5 Go antes del backup (incluyendo ~1,2 Go de BASE_TABLE DuckDB)
  • Paso a ~4,6 Go en menos de un minuto, justo después del log « Backing up DuckDB into a Parquet folder »
  • Proceso asesinado (ExitCode=137) antes de alcanzar el log « Closing DuckDB backup instance to release memory » en el finally — por lo tanto, el crash ocurre durante la llamada EXPORT DATABASE … FORMAT PARQUET COMPRESSION GZIP, no después.
  • Reproducido de manera idéntica 2 días seguidos, siempre en el momento del backup Gateway diario.

Lo que se solicita:

  1. Asegurarse de que duckDbCreateBackupInstance() utilice DuckDBInstanceCache.getOrCreateInstance() en lugar de DuckDBInstance.create(), para que el backup comparta el buffer pool de la instancia principal en lugar de abrir uno segundo.
  2. Considerar reemplazar COMPRESSION GZIP por ZSTD o SNAPPY en el EXPORT DATABASE (menos buffers de compresión, exportación más rápida).
  3. Eventualmente documentar que DUCKDB_MEMORY_LIMIT debe dimensionarse anticipando que dos instancias pueden coexistir durante un backup (por lo tanto, recomendar un valor ≤ 25% de la RAM asignada al contenedor, no 30-50%), siempre que el punto 1 no se corrija.

Contexto para situar el impacto: instalación con ~8 Go asignados al contenedor, BASE_TABLE de aproximadamente 1,1-1,2 Go de historial de sensores — por lo tanto, no un caso extremo, este comportamiento probablemente puede afectar a otras instalaciones con un historial considerable.

¡Pues resulta que yo tengo el mismo problema desde hace 7 días!
Sin embargo, debo reiniciar todos los días mi contenedor LXC de Docker con Gladys porque nada responde.

Estoy con Claude, que debe crearme un watchdog y un reinicio automático después de un fallo, con una recuperación de los registros justo antes del OOM, porque después de eso ya no tengo acceso a nada.

Te mantendré informado.

¡Gracias por la respuesta, lo voy a revisar!

Por primera vez esta mañana, no se ha bloqueado, no ha habido OOM:




Y aún no he puesto el script en marcha :frowning:

Desde que modifiqué mi docker compose para limitar DuckDB a 2Gb máximo en lugar de 4Go, no se ha producido ningún fallo (al menos esta noche). Esto confirma que es la dirección correcta.

El mío ya estaba limitado a 2 GB desde los primeros problemas de OOM.
Cuando volvió a ocurrir hace 1 semana, incluso lo limité a 1.5 GB, pero sin éxito.

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 # límite de uso de memoria de duckDB

Normalmente @pierre-gilles ya ha implementado una limitación de xx% para duckDB de forma automática (no me acuerdo del número exacto), lo cual funciona muy bien excepto en las instalaciones con proxmox donde el valor de la RAM del LXC no se recupera correctamente por docker, y en este caso hay que limitar (a menos que RAM LXC = RAM host).

¡Exactamente! LXC con 8 Gio, Docker que ve los 64 Gio del servidor y OOM.

No es un problema « mainstream » claro, pero ya ha ocurrido varias veces, según veo.

Hola @lmilcent,

Efectivamente, hay 3 fenómenos distintos aquí :smiley:

1. La instancia separada para el backup es intencional. Abrimos una instancia de DuckDB 100 % separada para generar el backup, y sí, esto permite que DuckDB utilice temporalmente 2× el límite autorizado durante el backup. Lo hacemos para aislar el backup de la producción y, sobre todo, para poder liberar la RAM al final: cerrar la instancia devuelve toda la memoria al SO. Si reutilizáramos la instancia de producción, estaríamos bajo los ×2 durante el backup, pero la exportación llenaría el buffer pool de producción hasta el 100 % del límite sin liberarlo nunca, para DuckDB es caché, la guarda de por vida. Guardar toda la base en caché debido a un backup de 10 minutos por noche no es ideal ^^

2. En tu caso, el verdadero problema proviene de LXC. El límite de RAM del contenedor no es visible desde el interior: Node calcula los 30 % sobre los 64 GB del servidor, es decir, ~19 GB de límite de DuckDB… demasiado para un contenedor de 8 GB, y ×2 durante el backup. Mientras tanto, puedes forzar un límite adecuado en la configuración de tu contenedor, por ejemplo:

DUCKDB_MEMORY_LIMIT=2GB

(~25 % de los 8 GB asignados, deja espacio para el pico del backup.)

3. ¿Por qué solo ahora? Probablemente no habías visto el problema los meses anteriores porque tu base era más pequeña. Con la acumulación de estados, la exportación consume cada vez más RAM, hasta desencadenar el OOM.

Para mejorar la detección automática del límite de memoria, tengo algunas ideas (leer el límite cgroup a través de process.constrainedMemory() de Node), pero de memoria hicimos muchos intentos infructuosos en LXC…

Para volver a probar, ¿podrías ejecutar estos 3 comandos y publicar el resultado?

Lo que Node ve desde el contenedor 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')"

Los límites cgroup vistos desde el contenedor:

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)"'

El límite que Gladys ha aplicado realmente:

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

Si process.constrainedMemory devuelve tus 8 GB (o si memory.max / cgroup v1 muestra el límite real), podremos corregir el cálculo automáticamente para todos. Si devuelve 0 o la RAM del host, nos centraremos más en la documentación sobre DUCKDB_MEMORY_LIMIT.

Hola @pierre-gilles, me permito compartirte mis resultados de pruebas:

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:~# 

Por supuesto, esto es lo que se ve desde el LXC, no desde el contenedor de Gladys.
Mi LXC tiene 6 GB de RAM, mi host tiene 16 GB.

pero no había pensado en eso para mis problemas que han reaparecido :roll_eyes:

Mi base de datos duckdb casi alcanza los 6Go :flushed_face:
¿Es normal lo de (1) y (2)? (Reconozco que no me acuerdo de lo que hice el 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)'

Bueno, he puesto en marcha un watchdog y captura de logs de Claude y voy a ver qué pasa en los próximos días.
A lo sumo, voy a volver de 1.5GB a 2GB como antes a finales de semana.

@lmilcent, si quieres los scripts y la conversación que Claude me ha proporcionado para ponerlo en marcha en tu proxmox, puedo proporcionártelos sin problemas, tendrás que ajustar las rutas.

Hola @mutmut,

Gracias por tus comentarios, ¡es exactamente lo que necesitábamos! Tus resultados confirman el diagnóstico, y desafortunadamente también muestran que no podremos detectar el límite automáticamente en tu configuración:

  • process.constrainedMemory = 17592186044416 : son exactamente 2⁴⁴ bytes = 16 TB, el valor « sin límite ». Coherente con tu cgroup v2 en max: tu contenedor Docker no tiene un límite de memoria propio, y los 6 GB de tu LXC están definidos en un cgroup padre, invisible desde dentro del contenedor.
  • MemTotal = 16 GB : el /proc/meminfo visto por Gladys muestra tu host, no tu LXC. Es la trampa del Docker-dentro-de-LXC: lxcfs virtualiza bien el /proc del LXC, pero Docker monta su propio procfs directamente desde el kernel, evitando lxcfs.
  • process.availableMemory = 6574 MB : engañoso, parece tus 6 GB de LXC, pero en realidad es la memoria disponible de tu host en ese momento, que por casualidad está cerca de 6 GB. Inutilizable para un cálculo fiable.

En concreto, en tu caso Gladys calculaba su límite DuckDB en los 16 GB del host: 30 % ≈ 4,7 GB, y hasta ×2 durante la copia de seguridad ≈ 9,4 GB potenciales… en un contenedor de 6 GB. El OOM estaba garantizado tan pronto como tu base de datos creció.

Por lo tanto: mantén tu DUCKDB_MEMORY_LIMIT definido explícitamente, es la única solución fiable en Docker-dentro-de-LXC, y lo documentaremos claramente para los usuarios de Proxmox/LXC.

Dos puntos de todos modos:

  1. Dices que aún se bloquea con un límite de 1,5 GB: eso es interesante, porque sugiere que parte de la memoria de la exportación escapa al límite (el memory_limit de DuckDB gobierna su buffer pool, pero los buffers del escritor Parquet y de la compresión GZIP están parcialmente fuera del conteo). Los registros de tu watchdog justo antes de un bloqueo nos ayudarían mucho a confirmar esto, ¡estoy interesado!
  2. Tu docker logs | grep memory_limit vacío es extraño: Gladys registra DuckDB initialized with memory_limit=... en cada inicio. Tus registros probablemente han girado, ¿puedes verificar justo después de un reinicio del contenedor?

En cuanto a la corrección, esto es lo que estamos preparando:

  • Limitar fuertemente la memoria de la copia de seguridad en sí: la instancia DuckDB temporal de la copia de seguridad tendrá su propio límite, mucho más pequeño (~1 GB máximo en lugar del límite completo). La exportación es un escaneo secuencial, no necesita un gran caché. El pico pasará de « 2× el límite » a « límite + ~1 GB », y eso protege a todos, incluso cuando la detección es imposible.
  • Cambiar la exportación de GZIP a ZSTD: menos memoria, mejor compresión.
  • Calcular mejor el límite por defecto: tener en cuenta el límite cgroup cuando es visible (Docker con --memory), y techo absoluto para los hosts grandes. No puede ayudar en tu caso LXC, pero evitará el problema a muchos otros.

Y para tus archivos duplicados (1) y (2) del 22 de junio: probablemente sean restos de copias que hayas hecho manualmente. Verifica su tamaño con un ls -lh en la carpeta. Si son copias con fecha, puedes eliminarlas para recuperar espacio en disco (manteniendo las bases de datos de producción, por supuesto, y su .wal, y su -shm).

¡Gracias @pierre-gilles por toda esta información!

Después de 2 días sin fallos, esta mañana he tenido un problema.

Por otro lado, los scripts implementados con Claude permitieron detectar la fuga, registrar los logs (te lo envío por MP) y reiniciar el docker Gladys, así que es en esta parte donde está el problema.

Claude me ha hecho una propuesta de PR con todos los comentarios listados en mi mensaje anterior :slight_smile:

Concretamente:

  • Cambio de GZIP a ZSTD
  • Limitar la memoria del backup a un máximo de 1 GB, no es necesario tener más caché
  • Mejor cálculo del límite por defecto

Gracias por responder más rápido que yo, como aún estoy de vacaciones lejos de casa, no tenía mucho tiempo :upside_down_face:

¡Gracias por los parches!

¡Por supuesto que acepto tus pruebas bajo LXC! @mutmut ¡cruzamos los dedos para que esto solucione tu problema!

Por lo que vale, ¡sin bloqueos desde entonces!

Por mi parte, he aumentado mi LXC a 13Go, así que ya no hay problemas.
Sin embargo, noto que en condiciones normales funciona justo por debajo de los 5Go de RAM, y cuando se guarda (hay que verificar con las horas exactas, ya que mi curva es de 7 días), sube a casi 8Go de RAM.



image

Vamos, vuelvo a 10Go de RAM para ver :slight_smile:

Vaya, en mi casa es estable a 3Gb. He puesto 4Gb en el LXC y limitado a 2Gb DuckDB.

docker-compose:

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: 80
      TZ: America/Toronto
      DUCKDB_MEMORY_LIMIT: 2GB
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/gladysassistant:/var/lib/gladysassistant
      - /dev:/dev
      - /run/udev:/run/udev:ro

  watchtower:
    image: containrrr/watchtower
    restart: always
    container_name: watchtower
    command: --cleanup --include-restarting
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Después de que mi gladys-production.db pesa casi 10Go, creo que es un gran bebé.
De mi lado he reducido a 1GB:

  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: 1000MB # límite de uso de memoria duckDB

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:rw
      - /var/lib/gladysassistant:/var/lib/gladysassistant:rw
      - /dev:/dev:rw
      - /run/udev:/run/udev:ro
      - /etc/localtime:/etc/locatime:ro
      
  watchtower:
    image: nickfedor/watchtower
    restart: always
    container_name: watchtower

Por otro lado, noto que sigues usando containrrr en lugar de nickfedor. ¿Todavía tienes las actualizaciones en automático o lo haces manualmente?