Verbraucht das Gateway-Backup zu viel Speicher?

Hallo,

Ich weiß nicht, ob ich der Einzige bin, aber ich habe regelmäßig OOM-Fehler, wenn ich Gladys 8 GB zuweise. Dadurch startet Gladys jede Nacht neu.

Nach Tagen des Debuggens, ohne die Ursache zu verstehen, habe ich Claude verwendet, um voranzukommen und eine bessere Hypothese zu identifizieren. Und in den folgenden Tagen wurde das Problem identifiziert: die Art und Weise, wie die Datenbank gesichert wird.

Kontext: Ich speichere alle Zustände unendlich lange, solange ich Festplattenspeicher habe.

Bin ich der Einzige, dem das passiert?


Fehler: Der Backup-Gateway kann einen OOM-Kill des Node-Prozesses verursachen

Betroffene Komponenten:
server/lib/gateway/gateway.backup.js
server/models/index.js (Funktion duckDbCreateBackupInstance)

Das Problem: duckDbCreateBackupInstance() öffnet eine separate DuckDB-Instanz auf derselben .duckdb-Datei wie die Hauptinstanz (bereits aktiv im Lese-/Schreibmodus):

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

Die offizielle Dokumentation des Node.js-Clients „Neo“ von DuckDB sagt jedoch ausdrücklich:

« Mehrere Instanzen im selben Prozess sollten nicht dieselbe Datenbank anhängen. »

Der vorgesehene Mechanismus, um dies zu vermeiden, ist DuckDBInstanceCache.getOrCreateInstance(path), der dieselbe Instanz (daher denselben Pufferpool / memory_limit) für dieselbe Datei wiederverwendet. Der aktuelle Code ruft DuckDBInstance.create() direkt auf, was zwei unabhängige Pufferpools erstellt, jeweils begrenzt auf denselben DUCKDB_MEMORY_LIMIT.

Ergebnis: Der schlechteste Speicherfall von DuckDB allein wird zu 2 × memory_limit statt 1 × memory_limit.

Verschlimmernd: Die DuckDB-Dokumentation weist auch darauf hin, dass memory_limit « nur für den Puffer-Manager gilt » — die Kompressionspuffer (COMPRESSION GZIP, das im EXPORT DATABASE verwendet wird) können zusätzlich zu dieser Grenze verbrauchen.

Beweis in der Produktion (2 identische Abstürze, Logs als Beweis):

  • RSS stabil ~2,5 GB vor dem Backup (davon ~1,2 GB BASE_TABLE DuckDB)
  • Übergang zu ~4,6 GB in weniger als einer Minute, direkt nach dem Log « Backing up DuckDB into a Parquet folder »
  • Prozess getötet (ExitCode=137) bevor der Log « Closing DuckDB backup instance to release memory » im finally erreicht wird — daher tritt der Absturz tatsächlich während des Aufrufs EXPORT DATABASE … FORMAT PARQUET COMPRESSION GZIP auf, nicht danach.
  • Reproduziert identisch zwei Tage hintereinander, immer zur Zeit des täglichen Gateway-Backups.

Was gefordert wird:

  1. Stellen Sie sicher, dass duckDbCreateBackupInstance() DuckDBInstanceCache.getOrCreateInstance() verwendet, anstatt DuckDBInstance.create(), damit das Backup den Pufferpool der Hauptinstanz teilt, anstatt einen zweiten zu öffnen.
  2. Überlegen Sie, COMPRESSION GZIP durch ZSTD oder SNAPPY im EXPORT DATABASE zu ersetzen (weniger Kompressionspuffer, schnellerer Export).
  3. Eventuell dokumentieren, dass DUCKDB_MEMORY_LIMIT so dimensioniert werden muss, dass zwei Instanzen während eines Backups koexistieren können (daher eine empfohlene Größe ≤ 25% des dem Container zugewiesenen RAMs, nicht 30-50%), solange Punkt 1 nicht behoben ist.

Kontext zur Einordnung der Auswirkungen: Installation mit ~8 GB, die dem Container zugewiesen sind, BASE_TABLE mit etwa 1,1-1,2 GB Sensorgeschichte — also kein extremer Fall, dieses Verhalten kann wahrscheinlich auch andere Installationen mit einer erheblichen Historie betreffen.

Und stell dir vor, ich habe seit 7 Tagen das gleiche Problem!
Allerdings muss ich jeden Morgen meinen LXC Docker mit Gladys neu starten, weil nichts mehr reagiert.

Ich bin mit Claude in Kontakt, der mir einen Watchdog und einen automatischen Neustart nach einem Absturz erstellen soll, mit einer Protokollwiederherstellung kurz vor dem OOM, denn danach habe ich keinen Zugriff mehr auf irgendetwas.

Ich halte dich auf dem Laufenden.

Danke für die Rückmeldung, ich werde mal schauen!

Zum ersten Mal heute Morgen ist es nicht abgestürzt, kein OOM:




Und ich habe das Skript noch nicht eingerichtet :frowning:

Seit ich meine Docker-Compose-Datei geändert habe, um DuckDB auf maximal 2 GB zu begrenzen statt 4 GB, gab es keine Abstürze (zumindest in dieser Nacht). Das bestätigt, dass es die richtige Richtung ist.

Meiner war bereits auf 2 GB begrenzt, seit die ersten OOM-Probleme auftraten.
Als es vor einer Woche wieder anfing, habe ich sogar auf 1,5 GB begrenzt, aber ohne Erfolg.

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 # Speicherbegrenzung für DuckDB

Normalerweise hat @pierre-gilles bereits eine automatische Begrenzung von xx% für DuckDB eingerichtet (ich erinnere mich nicht mehr an die genaue Zahl), was sehr gut funktioniert, außer bei Installationen mit Proxmox, wo der RAM-Wert des LXC nicht korrekt von Docker erkannt wird. In diesem Fall muss man tatsächlich begrenzen (es sei denn, RAM LXC = RAM Host).

Genau das ist es, LXC mit 8 GiB, Docker sieht die 64 GiB des Servers und OOM.

Das ist zwar kein „Mainstream“-Problem, aber es ist schon mehrmals passiert, wie ich sehe!

Hallo @lmilcent,

Tatsächlich gibt es hier drei verschiedene Phänomene :smiley:

1. Die separate Instanz für das Backup ist beabsichtigt. Wir öffnen tatsächlich eine vollständig separate DuckDB-Instanz, um das Backup zu erstellen, und ja, dadurch kann DuckDB während des Backups temporär das 2-fache des zulässigen Limits nutzen. Wir tun das, um das Backup von der Produktion zu isolieren und vor allem, um den RAM am Ende freigeben zu können: Das Schließen der Instanz gibt den gesamten Speicher an das Betriebssystem zurück. Wenn wir die Produktionsinstanz wiederverwenden würden, blieben wir zwar während des Backups unter dem 2-fachen, aber der Export würde den Produktionspufferpool bis zu 100 % des Limits füllen und würde ihn nie freigeben, für DuckDB ist es ein Cache, er behält ihn ein Leben lang. Es ist jedoch nicht ideal, die gesamte Datenbank für 10 Minuten pro Nacht im Cache zu behalten, nur wegen eines Backups ^^

2. In deinem Fall liegt das eigentliche Problem bei LXC. Das RAM-Limit des Containers ist von innen nicht sichtbar: Node berechnet die 30 % auf den 64 GB des Servers, also ~19 GB DuckDB-Limit… viel zu viel für einen 8-GB-Container und das 2-fache während des Backups. Bis dahin kannst du in der Konfiguration deines Containers eine angepasste Grenze erzwingen, zum Beispiel:

DUCKDB_MEMORY_LIMIT=2GB

(~25 % der 8 GB zugewiesen, das lässt Platz für den Backup-Peak.)

3. Warum erst jetzt? Du hattest das Problem in den vorherigen Monaten wahrscheinlich nicht bemerkt, weil deine Datenbank kleiner war. Mit der Ansammlung der Zustände steigt der Export immer höher im RAM an, bis der OOM ausgelöst wird.

Um die automatische Erkennung der Speichergrenze zu verbessern, habe ich einige Ansätze (Lesen des cgroup-Limits über process.constrainedMemory() von Node), aber soweit ich mich erinnere, hatten wir bei LXC einige wenig erfolgreiche Versuche gemacht…

Um erneut zu testen, könntest du diese drei Befehle ausführen und das Ergebnis posten?

Was Node vom Container Gladys aus sieht:

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

Die cgroup-Grenzen, die vom Container aus sichtbar sind:

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

Die Grenze, die Gladys tatsächlich angewendet hat:

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

Wenn process.constrainedMemory deine 8 GB zurückgibt (oder memory.max / cgroup v1 die echte Grenze anzeigt), können wir die automatische Berechnung für alle korrigieren. Wenn es 0 oder den RAM des Hosts zurückgibt, werden wir uns eher auf zusätzliche Dokumentation zu DUCKDB_MEMORY_LIMIT konzentrieren.

Hallo @pierre-gilles, ich erlaube mir, dir meine Testergebnisse mitzuteilen:

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

Natürlich ist das, was vom LXC aus gesehen wird, nicht das, was vom Docker von Gladys aus gesehen wird.
Mein LXC hat 6 GB RAM, mein Host hat 16 GB.

also habe ich nicht daran gedacht, als meine Probleme wieder aufgetaucht sind :roll_eyes:

Meine DuckDB-Datenbank ist fast 6 GB groß :flushed_face:
Und ist es normal, dass es (1) und (2) gibt? (Ich gebe zu, ich erinnere mich nicht, was ich am 22.06 gemacht habe …)

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

Ich habe einen Watchdog und Log-Capture von Claude eingerichtet und werde sehen, was in den nächsten Tagen passiert.
Falls nötig, werde ich den Speicherplatz von 1,5 GB wieder auf 2 GB erhöhen, wie es vor Ende der Woche war.

@lmilcent, wenn du die Skripte und die Konversation mit Claude brauchst, um sie auf deinem Proxmox einzurichten, kann ich sie dir gerne zur Verfügung stellen. Du musst dann nur noch die Pfade anpassen.

Hallo @mutmut,

Vielen Dank für dein Feedback, genau das haben wir gebraucht! Deine Ergebnisse bestätigen die Diagnose, und leider zeigen sie auch, dass wir die Grenze in deiner Konfiguration nicht automatisch erkennen können:

  • process.constrainedMemory = 17592186044416 : das sind genau 2⁴⁴ Bytes = 16 TB, der Wert « keine Grenze ». Konsistent mit deinem cgroup v2 auf max: Dein Docker-Container hat keine eigene Speichergrenze, und die 6 GB deines LXC sind in einem übergeordneten cgroup definiert, das vom Inneren des Containers aus nicht sichtbar ist.
  • MemTotal = 16 GB : Das von Gladys gesehene /proc/meminfo zeigt deinen Host, nicht dein LXC. Das ist die Falle von Docker-in-LXC: lxcfs virtualisiert zwar das /proc des LXC, aber Docker mountet sein eigenes procfs direkt vom Kernel aus, unter Umgehung von lxcfs.
  • process.availableMemory = 6574 MB : irreführend, es sieht aus wie deine 6 GB LXC, aber es ist tatsächlich der verfügbare Speicher deines Hosts zu diesem Zeitpunkt, der zufällig nahe an 6 GB liegt. Unbrauchbar für eine zuverlässige Berechnung.

Konkrekt hat Gladys also seine DuckDB-Grenze auf den 16 GB deines Hosts berechnet: 30 % ≈ 4,7 GB, und bis zu ×2 während des Backups ≈ 9,4 GB potenziell… in einem 6 GB-Container. Der OOM war garantiert, sobald deine Datenbank gewachsen ist.

Also: Behalte deinen DUCKDB_MEMORY_LIMIT explizit definiert, das ist die einzige zuverlässige Lösung in Docker-in-LXC, und wir werden das klar für Proxmox/LXC-Nutzer dokumentieren.

Zwei Punkte trotzdem:

  1. Du sagst, dass es immer noch mit einer Grenze von 1,5 GB abstürzt: Das ist interessant, denn es deutet darauf hin, dass ein Teil des Speichers des Exports der Grenze entgeht (der memory_limit von DuckDB steuert seinen Pufferpool, aber die Puffer des Parquet-Writers und der GZIP-Kompression sind teilweise außerhalb der Zählung). Die Logs deines Watchdogs kurz vor einem Absturz würden uns sehr helfen, das zu bestätigen, ich bin dabei!
  2. Dein leeres docker logs | grep memory_limit ist seltsam: Gladys loggt DuckDB initialized with memory_limit=... bei jedem Start. Deine Logs sind wahrscheinlich rotiert, kannst du das direkt nach einem Neustart des Containers überprüfen?

Auf der Seite der Korrektur haben wir Folgendes vorbereitet:

  • Den Speicher des Backups selbst stark begrenzen: Die temporäre DuckDB-Instanz des Backups wird ihre eigene Grenze haben, viel kleiner (~1 GB max statt der vollen Grenze). Der Export ist ein sequenzielles Scannen, er braucht keinen großen Cache. Der Spitzenwert wird von « 2× die Grenze » auf « Grenze + ~1 GB » sinken, und das schützt alle, auch wenn die Erkennung unmöglich ist.
  • Den Export von GZIP zu ZSTD wechseln: weniger Speicher, bessere Kompression.
  • Die Standardgrenze besser berechnen: Berücksichtigung der cgroup-Grenze, wenn sie sichtbar ist (Docker mit --memory), und absolute Obergrenze für große Hosts. Das kann deinen LXC-Fall nicht helfen, aber es wird das Problem für viele andere vermeiden.

Und für deine duplizierten Dateien (1) und (2) vom 22. Juni: Das sind wahrscheinlich Reste von Kopien, die du manuell erstellt hast? Überprüfe ihre Größe mit einem ls -lh im Ordner. Wenn es sich um datierte Kopien handelt, kannst du sie löschen, um Festplattenspeicher wiederzugewinnen (natürlich die Produktions-DBs und ihre .wal und -shm behalten).

Danke @pierre-gilles für all diese Infos!

Nach 2 Tagen ohne Absturz hatte ich heute Morgen ein Problem.

Allerdings haben die mit Claude erstellten Skripte das Leck erkannt, die Logs gespeichert (ich schicke dir das per PM) und den Gladys-Docker neu gestartet, also das funktioniert gut.

Claude hat mir einen PR-Vorschlag mit allen Rückmeldungen aus meiner vorherigen Nachricht gemacht :slight_smile:

Konkrete Änderungen:

  • Wechsel von GZIP zu ZSTD
  • Begrenzung des Backup-Speichers auf maximal 1 GB, mehr Cache ist nicht nötig
  • Bessere Berechnung des Standardlimits

Danke, dass du schneller geantwortet hast als ich, ich bin noch im Urlaub weit weg von zu Hause, ich hatte nicht viel Zeit :upside_down_face:

Danke für die Patches!

Ich bin natürlich für deine Tests unter LXC zu haben! @mutmut wir drücken die Daumen, dass das dein Problem löst!

Für das, was es wert ist, kein Absturz seitdem!

Von meiner Seite habe ich meinen LXC auf 13 GB erhöht, also keine Probleme mehr.
Allerdings stelle ich fest, dass es normalerweise gerade unter 5 GB RAM läuft und beim Sichern (zu überprüfen mit den genauen Zeiten, da meine Kurve auf 7 Tage ist) auf fast 8 GB RAM steigt.



image

Also, ich gehe wieder auf 10 GB RAM, um zu sehen :slight_smile:

Wow, bei mir ist es stabil bei 3 GB. Ich habe 4 GB für den LXC zugewiesen und DuckDB auf 2 GB begrenzt.

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

Nach meiner gladys-production.db wiegt fast 10 GB, ich denke, das ist ein schönes Baby.
Von meiner Seite habe ich auf 1 GB reduziert:

  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 # Speicherbegrenzung für 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

Ich sehe aber, dass du immer noch containrrr statt nickfedor verwendest. Hast du immer noch automatische Updates oder machst du das manuell?