Einfrieren während des Updates eines Z2M-Geräts

Hallo zusammen,

DAS IST EIN NOTRUF XD

Ich versuche, meine Geräte zu aktualisieren, aber bei einigen friert Gladys ein, weil sie, wie ich vermute, viele Zustände haben.
Ich muss mir zum Jahresende zwei NVMe-SSDs für meinen NAS kaufen, um meinen RAM zu verbessern (die Preise sind jetzt aber teuer :eyes:)

  • Was kann ich in der Zwischenzeit tun?
  • Wonach soll ich in den Logs suchen, um zu überprüfen, ob Gladys hängt oder ob sie arbeitet?

Vielen Dank im Voraus (in der Zwischenzeit ist meine Smart-Home-Anlage außer Gefecht :smiley:

Hast du einfachen Zugriff auf die Datenbank? Bist du bereit, alte Zustände für diese Geräte zu löschen?
Falls ja, können wir dich anleiten, manuell Abfragen zu starten.

Falls nicht, können wir das als Bug betrachten und die Migration verbessern, damit sie schrittweise oder im Hintergrund erfolgt.

Versuchst du, über die Gladys-Schnittstelle für z2m zu aktualisieren? Um eine Funktion hinzuzufügen?

Ja, das ist kein Problem :slight_smile:

Ich habe versucht, meinen Plug-upz zu aktualisieren, um die Energieverbrauchs-Features über die Gladys-Schnittstelle in Entdeckung und dann MAJ hinzuzufügen.

Am besten wäre es, nichts zu löschen :smiley:

Ich habe die Diagnose mit ChatGPT fortgesetzt, während Gladys noch im Zustand „freeze“ war.

Das Problem tritt bei der Aktualisierung/Synchronisation eines Zigbee2MQTT-Geräts über Gladys auf. In meinem Fall handelte es sich um eine Steckdose, die neue Funktionen im Zusammenhang mit Energie/Verbrauch erhalten sollte.

Mehrere Überprüfungen haben einige Spuren ausgeschlossen:

  • Der Gladys-Container ist nicht überlastet: etwa 3 % CPU und 2,4 GB RAM von 19 GB verfügbar während des Freezes.
  • Zigbee2MQTT scheint ebenfalls nicht überlastet zu sein.
  • Der Node-Prozess von Gladys bleibt am Leben.
  • GET / auf Gladys antwortet sofort mit HTTP 200, also kann Express weiterhin das statische Frontend bedienen.
  • Im Gegensatz dazu enden die Aufrufe an die dynamische API, z. B. die Abfrage der Geräte über meinen Proxy, in Timeouts.
  • SQLite bleibt während des Freezes von einem anderen Prozess aus zugänglich: einfache Abfragen antworten in etwa 0,5 Sekunden.
  • Die SQLite-WAL war etwa 1 GB groß, aber PRAGMA wal_checkpoint(PASSIVE) gibt 0|227|227 zurück, also kein Hinweis auf einen blockierten Checkpoint.
  • t_device_feature_state enthält 0 Zeilen (die Historie wurde zu DuckDB migriert) und t_device_feature nur 446 Zeilen.

Mit ChatGPT haben wir anschließend direkt den Code in meinem Gladys-Container untersucht.

Der Teil von Zigbee2MQTT getDiscoveredDevices.js enthält tatsächlich die aktuellen Korrekturen bezüglich der Energie-Features: Speicherung der Features consumption/cost, Merge mit dem vorhandenen Gerät und dann Aufruf von addEnergyFeatures().

In meinem device.create.js habe ich jedoch nach der Aktualisierung eines Features derzeit:

await deviceFeature.update(featureToUpdate, { transaction });

if (deviceFeature.keep_history === false) {
  deviceFeaturesIdsToPurge.push(deviceFeature.id);
}

ChatGPT hat dieses Verhalten mit dem aktuellen Code von Gladys verglichen und einen wichtigen Unterschied identifiziert. Der korrigierte Code speichert den alten Wert:

const keepHistoryBeforeUpdate = deviceFeature.keep_history;

await deviceFeature.update(featureToUpdate, { transaction });

if (
  keepHistoryBeforeUpdate !== false &&
  deviceFeature.keep_history === false
) {
  deviceFeaturesIdsToPurge.push(deviceFeature.id);
}

Der Unterschied besteht darin, dass in meiner Version jede Aktualisierung eines Geräts eine Löschung für alle Features anfordern kann, die bereits keep_history=false haben, selbst wenn sich dieser Wert nicht geändert hat.

Nach der Transaktion sendet Gladys dann:

this.eventManager.emit(
  EVENTS.DEVICE.PURGE_STATES_SINGLE_FEATURE,
  deviceFeatureIdToPurge
);

Die aktuelle Hypothese von ChatGPT ist, dass die Z2M-Aktualisierungen eine Ansammlung unnötiger Löschaufgaben verursachen. Diese Verarbeitung erfolgt insbesondere über DuckDB und könnte sich ansammeln, bis die dynamischen APIs von Gladys nicht mehr reagieren, während der Node-Prozess selbst weiterhin funktioniert.

Diese Hypothese passt besonders gut zum beobachteten Verhalten: normale CPU/RAM + statisches Frontend zugänglich + SQLite zugänglich + Gladys-API mit Timeout.

Das ist nicht die gleiche Integration, aber es sieht sehr merkwürdig ähnlich aus zu dem, was ich hatte und als Issue geöffnet habe: Overkiz integration: assigning a room to a device takes several minutes (freezes the interface) · Issue #2900 · GladysAssistant/Gladys · GitHub

@pierre-gilles @cicoub13 kleiner Fortschritt

Zusammenfassung von chatGPT:

Kompletter Freeze von Gladys beim Update eines Zigbee2MQTT-Geräts

Ich stoße auf ein reproduzierbares Problem mit Gladys, wenn ich eine Steckdose namens Station informatique über die Zigbee2MQTT-Schnittstelle aktualisiere.

Ursprünglich wollte ich dieses Gerät einfach aktualisieren, da einige Funktionen im Zusammenhang mit Energie/Verbrauch fehlten.

Symptome

Beim Aktualisieren des Geräts über Gladys:

  • die Anfrage endet mit einem Timeout;

  • die Schnittstelle wird teilweise oder vollständig unbrauchbar;

  • die SQLite-Schreibvorgänge beginnen dann zu scheitern mit:

    SequelizeTimeoutError
    SQLITE_BUSY: Datenbank ist gesperrt

Zum Beispiel:

Unable to reset failure count of integration ...
SQLITE_BUSY: Datenbank ist gesperrt

Der HTTP-Server selbst antwortet jedoch weiterhin auf Port 8455.

DB-Konfiguration

Die SQLite-Datenbank ist recht groß:

gladys-production.db        ~12 GB
gladys-production.db-wal   ~1 GB
gladys-production.duckdb   ~1,9 GB

SQLite funktioniert im WAL-Modus:

PRAGMA journal_mode;
wal

Ein passiver Checkpoint funktioniert:

PRAGMA wal_checkpoint(PASSIVE);
0|227|227

Einfache Abfragen auf SQLite bleiben schnell:

SELECT COUNT(*) FROM t_device_feature;
446

real 0m0.090s

Und:

SELECT COUNT(*) FROM t_device_feature_state;
0

Die Zustände scheinen also erfolgreich zu DuckDB migriert worden zu sein.

Betroffenes Gerät

Das Gerät ist:

id:
fd928ba6-ca0d-4a0d-8483-5b74cfddce8c

external_id:
zigbee2mqtt:Station informatique

Vor dem Update-Versuch waren seine registrierten Features unter anderem:

Schalter
Verbrauchte Leistung
Verbrauchte Stromstärke
Durchschnittsspannung
Verbrauchte Energie
Signalstärke
Zugriffskontrollmodus

Das Feature « Zugriffskontrollmodus » entspricht:

zigbee2mqtt:Station informatique:access-control:mode:child_lock

Untersuchung in device.create.js

Ich habe instrumentiert:

/src/server/lib/device/device.create.js

um genau zu bestimmen, wo die Transaktion blockiert wird.

Der Beginn des Updates funktioniert normal:

[DEBUG DEVICE CREATE] START zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE getDeviceInDb zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] AFTER getDeviceInDb zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE device update zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] AFTER device update zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE feature cleanup zigbee2mqtt:Station informatique

Die Blockade tritt also während dieses Teils von device.create.js auf:

await Promise.map(deviceInDb.features, async (existingFeature) => {
  if (!matchFeatureInList(existingFeature, features)) {
    await existingFeature.destroy({ transaction });
  }
});

Ich habe dann jedes Feature instrumentiert.

Ergebnis:

[DEBUG FEATURE CLEANUP] ...:switch:binary:state matched= true
[DEBUG FEATURE CLEANUP] ...:switch:power:power matched= true
[DEBUG FEATURE CLEANUP] ...:switch:current:current matched= true
[DEBUG FEATURE CLEANUP] ...:switch:voltage:voltage matched= true
[DEBUG FEATURE CLEANUP] ...:switch:energy:energy matched= true

Dann:

[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:access-control:mode:child_lock matched= false
[DEBUG FEATURE DESTROY] BEFORE zigbee2mqtt:Station informatique:access-control:mode:child_lock

Und kein DEBUG FEATURE DESTROY AFTER erscheint.

Das nächste Feature wird noch vom Promise.map inspiziert:

[DEBUG FEATURE CLEANUP] ...:signal:integer:linkquality matched= true

aber das Promise.map beendet sich nie, da das destroy() von child_lock blockiert bleibt.

Was zu passieren scheint

Die neue Definition, die von Zigbee2MQTT hochgeladen wird, enthält nicht mehr das Feature:

access-control:mode:child_lock

Gladys betrachtet daher logischerweise dieses alte Feature als gelöscht und führt aus:

await existingFeature.destroy({ transaction });

Genau dieser Aufruf kehrt nicht zurück.

Die Transaktion device.create() bleibt dann offen.

Kurz darauf versuchen andere Komponenten von Gladys, in SQLite zu schreiben, und beginnen, Folgendes zu produzieren:

SQLITE_BUSY: Datenbank ist gesperrt

Zum Beispiel die Aktualisierungen von t_service:

UPDATE `t_service`
SET `failure_count`=$1,`updated_at`=$2
WHERE `id` = $3

enden mit SequelizeTimeoutError.

Das ergibt also, in diesem Stadium, die folgende Sequenz:

Geräteupdate Z2M
    ↓
device.create()
    ↓
deviceInDb.update()                  OK
    ↓
Bereinigung der alten Features
    ↓
child_lock fehlt im neuen Gerät
    ↓
existingFeature.destroy({transaction})
    ↓
BLOCKIERUNG
    ↓
SQLite-Transaktion bleibt offen
    ↓
andere Schreibvorgänge
    ↓
SQLITE_BUSY / Datenbank ist gesperrt

Zur Energie

Das Problem trat auf, als ich versuchte, die Energieverbrauchs-Funktionen dieser Steckdose abzurufen, was mich zunächst auf den neuen Energieüberwachungscode lenkte.

Die Spuren zeigen jedoch jetzt, dass die Blockade vor der Erstellung/Aktualisierung der Energie-Features auftritt.

Das vorhandene Feature energy wird übrigens korrekt erkannt:

zigbee2mqtt:Station informatique:switch:energy:energy matched=true

Die Blockade wird durch den Versuch ausgelöst, das alte Feature child_lock zu löschen.

Weitere Beobachtung

Während des Freezes setzt der Energy Monitoring-Service seine Verarbeitung fort und zeigt insbesondere an:

Found 52 energy devices
Found 0 devices with both INDEX and thirty-minutes-consumption features

Dann erscheinen die SQLITE_BUSY in verschiedenen Teilen von Gladys.

Ich habe auch t_job überprüft: Die Jobs für die Migration SQLite → DuckDB und das Löschen von verwaisten DuckDB-Zuständen werden als success angezeigt.

Getesteter Patch ohne Erfolg

Ich hatte auch eine Änderung von device.create.js getestet, um die Bereinigung der Zustände nur auszulösen, wenn keep_history tatsächlich von true auf false wechselt:

const keepHistoryBeforeUpdate = deviceFeature.keep_history;

await deviceFeature.update(featureToUpdate, { transaction });

if (keepHistoryBeforeUpdate !== false && deviceFeature.keep_history === false) {
  deviceFeaturesIdsToPurge.push(deviceFeature.id);
}

Dies behebt das Problem nicht: Dank der zusätzlichen Protokolle weiß man jetzt, dass der Freeze früher auftritt, während des destroy() des veralteten Features.

Aktueller Diagnosezustand

Der reproduzierbare Blockadepunkt ist jetzt recht genau identifiziert:

existingFeature.destroy({ transaction })

für:

zigbee2mqtt:Station informatique:access-control:mode:child_lock

Man müsste eine asynchrone Aufgabe zum Löschen der Zustände starten, um die Arbeit fortsetzen zu können.

Update: Ursache des Freezes genauer identifiziert

Ich habe weiter an dem Freeze beim Update eines Zigbee2MQTT-Geräts gearbeitet.

Das betroffene Gerät ist:

  • Station informatique
  • externe ID: zigbee2mqtt:Station informatique

Ich habe temporär Debug-Logs um die Feature-Bereinigung in device.create.js hinzugefügt.

Beim Update kommt Gladys normalerweise bis zur Feature-Bereinigung:

[DEBUG DEVICE CREATE] START zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE getDeviceInDb zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] AFTER getDeviceInDb zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE device update zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] AFTER device update zigbee2mqtt:Station informatique
[DEBUG DEVICE CREATE] BEFORE feature cleanup zigbee2mqtt:Station informatique
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:switch:binary:state matched= true
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:switch:power:power matched= true
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:switch:current:current matched= true
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:switch:voltage:voltage matched= true
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:switch:energy:energy matched= true
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:access-control:mode:child_lock matched= false
[DEBUG FEATURE DESTROY] BEFORE zigbee2mqtt:Station informatique:access-control:mode:child_lock
[DEBUG FEATURE CLEANUP] zigbee2mqtt:Station informatique:signal:integer:linkquality matched= true

Es gibt nie ein Log AFTER nach dem Versuch, child_lock zu löschen.

Der Freeze tritt also hier in device.create.js auf:

await existingFeature.destroy({ transaction });

Warum diese Feature so lange zum Löschen braucht

Ich habe die Zeilen überprüft, die sich auf diese bestimmte Feature beziehen:

feature id:
557c3bba-bcc2-47e7-9f7f-f39614093509

energy_children    0
states             0
aggregates         509363
supported_options  0

Die Feature hat also keine klassischen Zustände mehr in SQLite, aber sie hat immer noch 509.363 Zeilen in:

t_device_feature_state_aggregate

Der Fremdschlüssel ist wie folgt konfiguriert:

t_device_feature_state_aggregate.device_feature_id
    -> t_device_feature.id
    ON DELETE CASCADE

Die Aggregat-Tabelle hat auch Indizes, darunter:

t_device_feature_state_aggregate_device_feature_id
t_device_feature_state_aggregate_device_feature_id_type_created_at

Das Löschen der veralteten Feature child_lock führt also zum kaskadierenden Löschen von mehr als 500.000 SQLite-Aggregaten, direkt in der Transaktion des Geräteupdates.

Während dieses Vorgangs beginnen andere SQLite-Schreibvorgänge zu scheitern mit:

SequelizeTimeoutError
SQLITE_BUSY: database is locked

Zum Beispiel scheitern die Updates von t_service.failure_count, die von externen Integrationen durchgeführt werden, schließlich.

Die HTTP-Anfrage von Gladys überschreitet auch ihr Timeout, was den Eindruck erweckt, dass die gesamte Oberfläche eingefroren ist.

Gladys behandelt dieses Problem bereits in einem anderen Code-Pfad

Es ist interessant zu sehen, dass device.destroy.js bereits einen expliziten Schutz gegen diese Art von Situation beim vollständigen Löschen eines Geräts enthält.

Der Code zählt die Zustände und Aggregationen, bevor das Gerät gelöscht wird. Wenn es zu viele gibt, vermeidet er das sofortige kaskadierende Löschen und löst stattdessen aus:

this.eventManager.emit(
  EVENTS.DEVICE.PURGE_STATES_SINGLE_FEATURE,
  deviceFeature.id
);

Das Löschen des Geräts wird dann unterbrochen, damit die Verzeichnisse zuvor bereinigt werden können.

device.purgeStatesByFeatureId.js löscht absichtlich die SQLite-Aggregationen in Batches:

DELETE FROM t_device_feature_state_aggregate WHERE id IN (
  SELECT id FROM t_device_feature_state_aggregate
  WHERE device_feature_id = :id
  LIMIT :limit
);

mit einer Verzögerung zwischen den Batches.

Die Kommentare im Code weisen explizit darauf hin, dass dieser Mechanismus dazu dient, die Datenbank nicht für eine lange Zeit zu blockieren.

Möglicher Unterschied zwischen den beiden Löschpfaden

Es scheint also einen Unterschied im Verhalten zwischen dem vollständigen Löschen eines Geräts und dem Löschen einer veralteten Feature während eines Updates zu geben.

Beim Löschen eines Geräts:

device.destroy()
    -> zählt die Zustände
    -> wenn es zu viele gibt:
       PURGE_STATES_SINGLE_FEATURE
       -> schrittweise Bereinigung

Während beim Update eines bestehenden Geräts:

device.create()
    -> erkennt eine Feature, die nicht mehr existiert
    -> existingFeature.destroy({ transaction })
    -> ON DELETE CASCADE SQLite
    -> synchrones Löschen von etwa 509k Aggregaten
    -> verlängertes SQLite-Schloss

In meinem Fall ist die veraltete Feature child_lock.

Warum das Problem zunächst mit Energie verbunden schien

Das Problem wurde entdeckt, als ich versuchte, meine Steckdose Station informatique zu aktualisieren.

Die Steckdose war bereits in Gladys vorhanden, aber ihr fehlte der Teil, der sich auf den Energieverbrauch bezog, den ich beim Update von Zigbee2MQTT abrufen wollte.

Anfangs schien der Freeze also mit der Hinzufügung der neuen Energie-Features verbunden zu sein.

Die Spuren zeigen schließlich, dass der Blockiervorgang früher stattfindet: während der Bereinigung der alten Features, wenn Gladys versucht, child_lock zu löschen.

Es ist also wahrscheinlich nicht direkt die Erstellung der neuen Energie-Features, die blockiert, sondern das Löschen einer alten Feature mit einer sehr großen Anzahl von historischen Aggregaten.

Korrekturvorschlag

Einfaches Ersetzen von:

await existingFeature.destroy({ transaction });

durch ein Ereignis PURGE_STATES_SINGLE_FEATURE scheint nicht ausreichend zu sein.

Dies würde es ermöglichen, die Verzeichnisse schrittweise zu bereinigen, aber die veraltete Feature selbst würde in der Datenbank bleiben.

Es wäre wahrscheinlich ein Mechanismus erforderlich, der:

  1. erkennt, dass eine veraltete Feature viel Verzeichnis hat;
  2. ihr DELETE CASCADE in der Update-Transaktion vermeidet;
  3. ihre Zustände und Aggregationen schrittweise im Hintergrund bereinigt;
  4. die veraltete Feature anschließend löscht;
  5. gegebenenfalls überprüft, ob sie zum Zeitpunkt des Löschens immer noch veraltet ist, falls die Integration sie zwischenzeitlich wieder exponiert hat.

Ich beende meine Untersuchungen hier. Ich hoffe, das hilft euch :slight_smile:

Perfekt. Machst du einen PR? Oder sollen wir das machen?

äh, ich würde es lieber von Ihnen machen lassen, bitte :slight_smile:

Ich finde, das ist ein bisschen zu technisch für mich, um da ran zu gehen :confused:

Bei der weiteren Analyse stelle ich fest, dass du viele aggregierte Zustände in t_device_feature_state_aggregate hast, die nicht mehr verwendet werden. Alles ist in DuckDB (Zustände und aggregierte Daten werden dynamisch berechnet).

Ich glaube, dass hier nur die Zustände gelöscht werden und nicht diese Tabelle:

Es ist besser, die Daten zu löschen und den Fix nicht auf eine Tabelle zu implementieren, die eigentlich leer sein sollte. Ich führe die Untersuchung weiter :detective:

Ich vermute, dass du noch nicht gelöschte aggregierte Zustände hast. Ich habe einen PR eröffnet, um sie hier anzuzeigen Show the remaining SQLite aggregates in the DuckDB migration card by cicoub13 · Pull Request #2952 · GladysAssistant/Gladys · GitHub

Aber wenn du nicht warten willst, denke ich, dass du auf den „Löschen“-Button klicken und dann die SQLite-Datenbank bereinigen kannst. Dein Zigbee-Aktualisierungsproblem sollte dann verschwinden.

ich verstehe nicht, ich habe das schon zur Zeit der Ankunft von duckdb gemacht

Ich habe gerade auf „Leeren“ geklickt und jetzt friert es wieder ein
Ich habe das Gefühl, dass ich feststecke..
Um mich zu entsperren, muss ich leeren, aber um zu leeren, friere ich ein ahah help :face_with_spiral_eyes:

Ich glaube, du bist nie bis zum Ende gegangen. Klicke auf „Leeren“, wenn du Gladys nicht brauchst, und lass es laufen. Auch wenn du keine visuelle Rückmeldung erhältst, wird geleert.

@cicoub13 brauchst du den Tisch nicht mehr?

Der Tisch wird nicht mehr benötigt (zumindest der Inhalt). Der Tisch sollte nicht gelöscht werden, da Migrationsverweise darauf bestehen und ich mir nicht sicher bin, wie sich das Verhalten verhält, wenn der Tisch vollständig verschwindet.

Wenn du dich damit wohlfühlst, kannst du

  • Gladys ordnungsgemäß herunterfahren
  • Die Datenbankdateien sichern
  • Die Tabelle leeren (TRUNCATE)
  • Gladys starten

Wenn du mehr Sicherheit bei der Sache haben möchtest, kannst du warten, bis @pierre-gilles seine Meinung dazu äußert.

Das war mein Plan.
Danke für dein Feedback, ich werde auf seine Meinung warten, vielleicht hat er eine bessere Idee :slight_smile:

du kannst damit schon loslegen, das reicht völlig für einen RW-Cache: https://www.leboncoin.fr/ad/accessoires_informatique/3247461279

Dann hatte ich auf meinem Synology die 2 SSDs als Speicher für die VMs eingerichtet (nicht direkt für Docker möglich), aber ohne Backup. Achtung, nicht offizielle Methode.

Rückkehr nach der Datenbankbereinigung

Ich habe das Problem schließlich behoben (ich war zu ungeduldig).

Meine SQLite-Datenbank gladys-production.db hatte etwa 12 GB erreicht.

1. Integritätsprüfung bei gestopptem Gladys

Ich begann mit einem Backup meiner Datenbank und führte dann PRAGMA quick_check; aus.

Auf meinem NAS dauerte der Vorgang extrem lange aufgrund der I/O-Leistung.

Der quick_check benötigte etwa 3 Stunden, kehrte aber schließlich ok zurück.

Die SQLite-Datenbank war also nicht beschädigt.

Während der Prüfung befand sich der Prozess sqlite3 regelmäßig im Zustand D (disk sleep) und die Zähler /proc/<pid>/io zeigten, dass die Lesevorgänge weiterhin fortschritten.

2. Identifizierung der problematischen Tabelle

Das Problem lag hauptsächlich in der Tabelle t_device_feature_state_aggregate.

Die Datenbank war etwa 12 GB groß, und diese Tabelle mit ihren Indizes schien den größten Teil des Speicherplatzes zu beanspruchen.

Ich stoppte Gladys, bevor ich die Datenbank bearbeitete.

3. Löschen und Neuerstellen der Tabelle

Statt einen großen DELETE FROM t_device_feature_state_aggregate; durchzuführen, entschied ich mich, die Tabelle vollständig zu löschen und dann mit ihrem ursprünglichen Schema und Indizes neu zu erstellen.

Auch dieser Vorgang dauerte sehr lange aufgrund der I/O des NAS.

Ein erster Versuch wurde nach etwa 1 Stunde 30 Minuten aufgrund einer SSH-Sitzungsunterbrechung abgebrochen.

Zu diesem Zeitpunkt hatte SQLite bereits etwa 4,2 GB gelesen (read_bytes: 4 197 961 728).

Da der COMMIT nicht stattgefunden hatte, hat SQLite die Transaktion korrekt rückgängig gemacht und die alte Tabelle war weiterhin vorhanden.

4. Zweiter Versuch mit nohup

Ich startete den Vorgang erneut mit nohup, damit er einer möglichen SSH-Trennung standhalten würde.

Diesmal wurde der Vorgang abgeschlossen.

Einige Messungen während des DROP TABLE:

Vergangene Zeit Gelesene Daten
~22 min 0,73 GB
~1 h 04 2,75 GB
~1 h 32 4,20 GB
~1 h 50 5,02 GB
~2 h 20 6,02 GB
~2 h 30 9,44 GB
~2 h 39 10,81 GB

Der gesamte Vorgang dauerte schließlich etwa 2 Stunden 45 Minuten.

Der Prozess war fast ständig im Zustand D (disk sleep), mit sehr wenig CPU-Nutzung. Der begrenzende Faktor waren also eindeutig die I/O des NAS.

5. Überprüfung nach dem Neuerstellen

Nach Abschluss des Vorgangs:

SELECT COUNT(*) FROM t_device_feature_state_aggregate;

gibt 0 zurück.

Die Tabelle wurde also erfolgreich leer neu erstellt.

Ich führte dann PRAGMA wal_checkpoint(TRUNCATE); aus.

Ergebnis: 0|0|0.

Der WAL wurde also korrekt geleert.

6. Wiederhergestellter Speicherplatz

Die SQLite-Datei ist physisch immer noch etwa 12 GB groß, was ohne VACUUM normal ist.

Allerdings:

  • page_count = 3 118 963
  • freelist_count = 3 098 841

Das bedeutet, dass etwa 99,35 % der Seiten der Datenbank jetzt frei sind.

Der Speicherplatz wurde also noch nicht dem Dateisystem zurückgegeben, aber SQLite kann ihn nun wiederverwenden.

Ich habe absichtlich noch kein VACUUM gestartet.

7. Ergebnis nach dem Neustart von Gladys

Ich startete Gladys neu.

Alles funktioniert normal.

Und vor allem habe ich den Test wiederholt, der Probleme verursachte: die Änderung eines Geräts, die zuvor extrem langsam/blockiert war.

Dieses Mal wurde die Geräteänderung in wenigen Sekunden durchgeführt.

Fazit

  • PRAGMA quick_check: OK, aber etwa 3 Stunden
  • SQLite-Datenbank: etwa 12 GB
  • t_device_feature_state_aggregate machte praktisch die gesamte Datenbank aus
  • erster Versuch zum Löschen nach ~1 Stunde 30 Minuten abgebrochen
  • zweiter vollständiger Versuch: ~2 Stunden 45 Minuten
  • freelist_count: 3 098 841 freie Seiten von 3 118 963, also ~99,35 %
  • nach dem Neustart funktioniert Gladys normal
  • die Änderung des problematischen Geräts erfolgt jetzt in wenigen Sekunden

Das Problem lag also tatsächlich an der Explosion von t_device_feature_state_aggregate und den I/O-Leistungsanforderungen, um mit dieser riesigen Tabelle zu arbeiten.

Noch zu tun

  • Mein Backup löschen
  • ein VACUUM durchführen
  • NVMe-SSDs einbauen :sweat_smile:

Danke @cicoub13 @mutmut