Eine Datenbank statt drei, und WAL als Standard
Nach dem Umzug in Container mussten drei Anwendungen wieder dieselbe Datei sehen. Was dabei zu beachten war, warum der Journalmodus wichtiger ist, als er klingt, und ein Prüfcode, der immer falsch war.
968 Wörter · 5 Min. Lesezeit
- datenbank
- betrieb
- sqlite
Beim Umzug in Container stand die Frage wieder an, die ich im Frühjahr schon einmal beantwortet hatte: Wie kommen mehrere Anwendungen an dieselbe Datenbankdatei?
Im alten Aufbau war die Antwort „sie liegen ohnehin im selben Verzeichnisbaum". Bei Containern ist jeder für sich, und die Antwort muss ausdrücklich sein.
Der Aufbau
Ein gemeinsames Verzeichnis auf dem Wirtssystem, das in alle drei Container an derselben Stelle eingehängt wird. Alle drei Anwendungen bekommen denselben absoluten Pfad.
Drei Dinge, die dabei zu beachten waren:
Die Benutzernummern müssen zusammenpassen. Der Prozess im Container schreibt unter einer bestimmten Nummer, und diese Nummer muss auf dem Wirtssystem an der Datei berechtigt sein. Bei unterschiedlichen Nummern in verschiedenen Abbildern wird das zum Puzzle.
Der Pfad ist absolut. Die Lehre aus dem Frühjahr, als drei relative Pfade zu drei Datenbanken geführt haben.
Es gibt eine Prüfung. Jede Anwendung meldet beim Start den Pfad und die Kennung aus der Datenbank. Drei verschiedene Kennungen im Protokoll heißen: falsch eingehängt.
Der Journalmodus, und ein Prüfcode, der immer falsch war
Beim Aufsetzen habe ich die Datenbankeinstellungen mitgenommen, die ich im Vorjahr erarbeitet hatte. Der wichtigste Punkt ist der Journalmodus: In der Voreinstellung sperrt jeder Schreibvorgang die gesamte Datei; mit Write-Ahead-Log blockieren Leser die Schreiber nicht mehr. Bei einem Webserver mit parallelen Anfragen macht das den Unterschied.
Der Code sah so aus:
const ergebnis = db.pragma("journal_mode = WAL", { simple: true });
if (ergebnis[0]?.journal_mode === "wal") {
// gesetzt
}Und funktionierte nie. Nicht laut, sondern still: Die Prüfung war immer negativ, also lief eine Schleife durch alle Ersatzmodi und landete am Ende beim letzten Rückfallwert. Die Datenbank lief also monatelang in einem anderen Modus als beabsichtigt.
Ursache: Mit der Option für einfache Rückgabewerte liefert die Bibliothek den Wert direkt als Zeichenkette zurück, nicht als Objekt in einem Feld. Der Zugriff auf ein Feld eines Zeichenkettenindex ergibt undefined, der Vergleich ist immer falsch — und das ganz ohne Fehlermeldung.
const wert = typeof ergebnis === "string" ? ergebnis : ergebnis?.journal_mode;
if (wert?.toLowerCase() === "wal") { /* gesetzt */ }Warnung
Das ist der Fehlertyp, den ich am meisten fürchte: eine Prüfung, deren negatives Ergebnis nicht auffällt. Wäre der Vergleich immer positiv gewesen, hätte ich es vielleicht bemerkt. Weil er immer negativ war, griff der eingebaute Rückfallweg — und der tat etwas Sinnvolles. Ein Sicherheitsnetz, das eine defekte Prüfung verdeckt, ist ein zweischneidiges Ding. Seitdem protokolliere ich in solchen Fällen den tatsächlich erreichten Zustand, nicht nur den Versuch.
Die weiteren Einstellungen
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 10000;Der mittlere Wert spart pro Schreibvorgang eine Synchronisation aufs Speichermedium. In Verbindung mit dem Write-Ahead-Log bleibt das absturzsicher — verloren gehen können höchstens die letzten Transaktionen bei einem Stromausfall, nicht die Datenbank selbst.
Der letzte Wert ist der, den man am ehesten vergisst und der im Betrieb am meisten Ruhe bringt: Statt bei einer belegten Sperre sofort mit einem Fehler abzubrechen, wird gewartet. Ohne diesen Wert erscheinen unter Last Fehlermeldungen, die nach einem ernsten Problem aussehen und keines sind.
Was beim Sichern anders ist
Mit Write-Ahead-Log gehören zur Datenbank zusätzliche Dateien. Wer nur die Hauptdatei kopiert, während Betrieb läuft, bekommt einen unvollständigen Stand.
Der richtige Weg ist ein Befehl, der eine konsistente Kopie erzeugt, ohne den Betrieb anzuhalten:
VACUUM INTO '/pfad/zur/sicherung.db';Das ergibt eine einzelne, in sich stimmige Datei — die man anschließend kopieren, packen und wegräumen kann wie jede andere. Für die Panel-Datenbank meines neuen Aufbaus gilt dasselbe, und dort habe ich es mir ausdrücklich notiert, weil die Versuchung groß ist, einfach zu kopieren.
Die Prüfung, die ich mir dafür gebaut habe
Nach diesem Fall gibt es ein kleines Skript, das ich nach jeder Änderung an der Umgebung laufen lasse. Es beantwortet drei Fragen für alle Prozesse gleichzeitig.
Welche Datei benutzt jeder Prozess? Über die offenen Dateihandles des laufenden Prozesses — nicht über die Konfiguration. Die Konfiguration sagt, was gemeint war; die Handles sagen, was tatsächlich passiert.
Ist es dieselbe Datei? Über Gerät und Inode, nicht über den Pfad. Zwei verschiedene Pfade können auf dieselbe Datei zeigen, und zwei gleiche Pfade in verschiedenen Containern auf verschiedene.
Ist der Journalmodus wirklich gesetzt? Abgefragt, nicht angenommen.
Die zweite Frage ist die, die mich der Fall gelehrt hat. Ich hatte ursprünglich Pfade verglichen — und in Containern sind Pfade nichts wert, weil jeder Container seine eigene Sicht hat.
Was ich über Selbstprüfungen gelernt habe
Der eigentliche Fehler war nicht der falsche Modus, sondern eine Prüfung, deren Scheitern nichts bewirkte.
Daraus ist eine Regel geworden, die ich für eine der nützlichsten halte, die ich habe: Jede Prüfung muss einen sichtbaren Ausgang haben.
Konkret heißt das: Wenn ich prüfe, ob ein Zustand erreicht wurde, dann protokolliere ich das Ergebnis — nicht nur im Fehlerfall, sondern immer. Eine Zeile beim Start, die sagt „Journalmodus: WAL", hätte diesen Fehler am ersten Tag sichtbar gemacht.
Der Gegentest ist einfach: Woran würde ich merken, dass diese Prüfung selbst kaputt ist? Wenn die Antwort „gar nicht" lautet, ist die Prüfung wertlos — und schlimmer als keine, weil sie Sicherheit vortäuscht.
Was ich beim Einhängen in Container gelernt habe
Ein gemeinsames Verzeichnis für mehrere Container klingt einfach und hat drei Fallstricke, die alle mit Rechten zu tun haben.
Die Benutzernummer im Container muss zur Datei passen. Nicht der Name — die Nummer. Zwei Abbilder können denselben Benutzernamen mit verschiedenen Nummern haben, und dann schreibt der eine Dateien, die der andere nicht ändern darf.
Neue Dateien erben nicht automatisch die richtigen Rechte. Eine Datenbank legt Hilfsdateien an, und die gehören dem Prozess, der sie erzeugt hat.
Ein Container darf das Verzeichnis nicht nur lesen dürfen. Bei einer Datei-Datenbank braucht auch ein reiner Leser Schreibrechte — für die Hilfsdateien.
Der letzte Punkt hat mich eine Stunde gekostet. Ein Dienst, der nur liest, scheiterte mit einer Meldung über eine schreibgeschützte Datenbank — obwohl er tatsächlich nur lesen wollte.
Was ich mitnehme
Beim Wechsel der Umgebung gelten alte Annahmen nicht mehr. Der gemeinsame Pfad war im alten Aufbau eine Selbstverständlichkeit und ist im neuen eine Konfiguration.
Selbstprüfungen brauchen ein sichtbares Ergebnis. Eine Anwendung, die beim Start meldet, welchen Zustand sie tatsächlich erreicht hat, hätte mir Monate Unklarheit erspart.