Mitten in der Arbeit an den Backups fiel auf, dass der Dienst, der alle Backups überwacht, seit anderthalb Tagen kaputt war — ohne dass es jemand bemerkt hatte, denn der Melder war der Kranke. Die Ursache war eine Katalog-Datenbank, die auf 18 Gigabyte angewachsen war, obwohl sie nur 123 Einträge enthielt. Es gab eine Löschregel, aber ohne anschließendes Aufräumen gibt SQLite den Platz nie zurück — und für das Aufräumen fehlte der Platz. Dieser Artikel erzählt beide Stränge des Nachmittags, samt eines Beweises, den ich zunächst führte, obwohl er keiner war.
Es war eine Nebenfrage. Nach dem Klären der Include-Listen aus Teil 1 und den Messungen aus Teil 2 wollte ich nur schnell nachsehen, ob eine bestimmte Datei im letzten Snapshot liegt. Dafür gab es einen komfortablen Weg: einen selbstgebauten Katalogdienst, der die Dateilisten aller Backups indexiert und durchsuchbar macht.
Der Dienst antwortete nicht.
Ein Container, der seit 32 Stunden krank war
Die statischen Seiten des Dienstes luden in einer Zehntelsekunde. Jede Abfrage, die die Datenbank berührte, hing unbegrenzt. Der Zustandsbericht des Containers war eindeutig und ernüchternd:
| Containerstatus | Up 32 hours (unhealthy) |
| Fehlgeschlagene Prüfungen in Folge | 11 |
| Jede Prüfung | Zeitüberschreitung nach 5 Sekunden |
Zweiunddreißig Stunden. Die eingebaute Zustandsprüfung hatte elfmal hintereinander angeschlagen, der Container trug den Status sichtbar im Namen — und niemand hatte es erfahren. Das ist die unangenehme Eigenschaft eines Überwachungssystems, das selbst ausfällt: Es meldet nicht „ich bin defekt". Es meldet gar nichts. Und Stille ist von „alles in Ordnung" nicht zu unterscheiden.
Der Befund wurde schlimmer, je genauer ich hinsah. Dieser Katalog trug nämlich nicht nur die Suchfunktion, sondern auch die Alarmierung für fehlgeschlagene Server-Backups. Drei Stunden lang — die Zeit, die zwischen dem Beginn der Störung und ihrer Entdeckung lag — hätte kein einziges fehlgeschlagenes Backup irgendeine Meldung ausgelöst. Nicht weil die Alarmierung falsch konfiguriert war, sondern weil der Alarmgeber selbst am Boden lag.
Achtzehn Gigabyte für 123 Einträge
Die Ursache lag offen, sobald ich auf das Dateisystem sah:
katalog.db 18G
katalog.db-wal 15M Eine SQLite-Datei von 18 Gigabyte erklärt alles Weitere. Die Zustandsprüfung brach nach fünf Sekunden ab, weil selbst einfachste Abfragen länger brauchten. Ein bloßes Zählen der Zeilen lief über zehn Minuten.
Meine erste Vermutung war naheliegend und falsch: Die Datenbank sei so groß, weil sie so viel enthalte. Es gab schließlich einen Verdächtigen — das Backup-Skript meldete nach jedem Lauf jede einzelne Datei an den Katalog, zuletzt über 570.000 Stück, und im Protokoll standen 376 solcher Läufe allein für einen Rechner.
Der Blick in die Tabellen ergab etwas anderes:
| Der kranke Dienst | Die Replik auf einem zweiten Rechner | |
|---|---|---|
| Snapshots | 123 | 798 |
| Ereignisse | 2.183 | 2.183 |
| Neuester Eintrag | identisch, vom selben Tag | |
Die 18-Gigabyte-Datei enthielt 123 Snapshots. Die Replik, die auf einer anderen Datenbank läuft, enthielt 798 — also deutlich mehr Historie — und antwortete in sechs Hundertstelsekunden.
Die Löschregel, die es gab
Damit war die wahre Ursache klar, und sie war eine andere als gedacht. Es gab eine Aufbewahrungsregel. Alte Einträge wurden gelöscht — deshalb ja nur 123 Snapshots. Was fehlte, war der zweite Schritt.
SQLite gibt den Platz gelöschter Daten nicht an das Dateisystem zurück. Die Seiten werden in eine interne Freiliste eingehängt und für spätere Einfügungen wiederverwendet. Der Speicherplatz ist also nicht verloren — aber die Datei behält ihren einmal erreichten Höchststand für immer. Die Projektdokumentation formuliert das unmissverständlich: Wird gelöscht, „bleibt die Datenbankdatei gleich groß", unbenutzte Seiten kommen auf eine Freiliste und werden wiederverwendet, „die Datenbankdatei schrumpft jedoch nicht".
Wie weit das getrieben war, zeigte ein Blick auf die Zählerstände: Die höchste je vergebene Kennung in der größten Tabelle stand bei 352 Millionen, während real nur ein Bruchteil davon existierte. Die Datei war nicht voll. Sie war aufgebläht.
Die Sackgasse — und der Ausweg, den wir nicht kannten
Der Befehl, der eine solche Datei wieder verkleinert, heißt VACUUM. Er arbeitet nicht an Ort und Stelle, sondern baut die Datenbank vollständig neu auf: erst in eine temporäre Kopie, dann zurück über das Original. Die Dokumentation nennt den Preis dafür klar — es wird „bis zur doppelten Größe der ursprünglichen Datenbankdatei an freiem Plattenplatz" gebraucht.
Auf dem betroffenen Server standen 3,7 Gigabyte frei, bei einer 48-Gigabyte-Platte zu 92 Prozent belegt. Achtzehn Gigabyte für das Aufräumen einer 18-Gigabyte-Datei waren dort nicht zu holen. Eine Sackgasse, die sich selbst zuzieht: Je länger die Datei wächst, desto unmöglicher wird das Aufräumen, das sie verkleinern würde. Und liefe sie weiter, hätte sie irgendwann die Systempartition gefüllt und die übrigen Dienste auf demselben Server mitgerissen.
Wir haben das damals anders gelöst — die wertvolle Substanz herausgezogen, gesichert und die Datenbank neu aufgebaut. Beim Nachrecherchieren für diesen Artikel stellte sich heraus, dass die Sackgasse gar keine war.
Es gibt eine Variante des Aufräumbefehls, die direkt in eine Zieldatei schreibt. Sie kopiert nicht zurück, sie legt kein Journal neben dem Original an, und sie behandelt die Quelle als reinen Lesevorgang. Zeigt man sie auf ein anderes Dateisystem — eine externe Platte, ein Netzlaufwerk —, lässt sich eine aufgeblähte Datenbank auch dann kompaktieren, wenn das ursprüngliche Volume randvoll ist. Der Ausweg existierte. Wir kannten ihn nur nicht.
Eine Feinheit dazu, die man leicht übersieht: Der zusätzliche Platzbedarf verteilt sich beim klassischen Aufräumen auf zwei Orte. Die temporäre Kopie folgt der Einstellung für Temporärdateien und lässt sich umlenken. Das Journal für das Zurückschreiben liegt dagegen immer neben der Originaldatei und ist nicht verschiebbar. Wer nur das Temp-Verzeichnis umbiegt, löst die halbe Rechnung.
Und noch eine, die für Diagnosen zählt: Die temporäre Datenbank wird direkt nach dem Öffnen aus dem Verzeichnis entfernt und ist in keiner Dateiliste sichtbar. Sie belegt trotzdem die vollen Blöcke. Wer den Platzbedarf mit einem Verzeichnislisting prüft, unterschätzt ihn — dieselbe Falle wie die Platzhalter-Messung aus Teil 2.
Ein Beweis, der keiner war
Bevor die alte Datenbank verschwinden durfte, musste feststehen, dass ihr Inhalt anderswo vollständig vorliegt. Ich schrieb also eine Abfrage, die die Snapshot-Kennungen beider Seiten zog und verglich. Ergebnis: keine Kennung fehlte. Alles vorhanden.
Das Ergebnis war wertlos, und zum Glück fiel mir das auf, bevor ich es als Beweis verkaufte. Beide Abfragen hatten null Kennungen geliefert — die Spalte hieß in Wahrheit anders, als ich angenommen hatte. Der Vergleich zweier leerer Listen ergibt zwangsläufig „vollständige Übereinstimmung". Es sah aus wie eine bestandene Prüfung und war das genaue Gegenteil: eine Prüfung, die gar nicht stattgefunden hatte.
Das ist die gefährlichste Sorte Fehler beim Backup. Ein fehlgeschlagener Test ist ein Geschenk — er zeigt, wo man nachsehen muss. Ein Test, der aus dem falschen Grund besteht, erzeugt Sicherheit, wo keine ist. Und er hätte hier eine Löschentscheidung über 18 Gigabyte getragen.
Mit der richtigen Spalte sah der Vergleich so aus:
Der kranke Dienst: 123 Snapshots
Die Replik: 747 Snapshots
Nur beim kranken Dienst: 1 Eintrag — ohne Snapshot-Kennung
Nur bei der Replik: 629 zusätzliche Der eine Eintrag ohne Entsprechung trug gar keine Kennung — es war das Fehlerprotokoll eines gescheiterten Laufs, kein Backup. Jetzt war es ein Beweis.
Ein Nachspiel hatte auch dieser Vergleich noch: Bei einem der Snapshots wichen die Dateizahlen um knapp 8.000 ab. Auch das war kein echter Befund, sondern wieder ein Prüfwerkzeug-Problem. Ich hatte die Datenbank in einem Modus gelesen, der ausschließlich die Hauptdatei berücksichtigt und das Write-Ahead-Log ignoriert — und genau dort lagen die fehlenden Zeilen, weil der Übertrag in die Hauptdatei nie durchgelaufen war. Eine SQLite-Datei ohne ihr Write-Ahead-Log ist ein unvollständiger Stand, und der schnelle Lesemodus verschweigt das lautlos.
Der zweite Strang: ein Rechner, der sich für zwei hielt
Am selben Tag lief eine zweite Geschichte, die zunächst nichts damit zu tun zu haben schien — und deren Ursache in Teil 2 dieser Serie ausführlich steht: Ein Dienst identifizierte Knoten über den Rechnernamen, und dieser Name schwankte. Der einzige laufende Knoten hielt sich dadurch für einen zweiten, entzog sich selbst den Anspruch auf seine Sitzung und zerstörte die eigene Verbindung.
Was dort als Kuriosität endet, hat hier eine unangenehmere Fortsetzung. Derselbe schwankende Name tauchte an diesem Tag noch zweimal auf: einmal beim Aufbau des Schlüssel-Inventars, wo er beinahe zwei Gruppen für ein und denselben Rechner erzeugt hätte. Und einmal an der Stelle, die für Backups die unangenehmste ist.
Wie derselbe Fehler die Snapshot-Zuordnung bricht
Restic speichert in jedem Snapshot den Rechnernamen. Ohne ausdrückliche Angabe übernimmt es dafür ungefiltert das, was das Betriebssystem gerade meldet. Dieser Name ist kein Etikett, sondern ein Ordnungsmerkmal, und er wirkt an zwei Stellen:
Erstens beim Vorgänger-Snapshot. Damit ein Lauf nicht jedes Mal alles neu einlesen muss, sucht das Werkzeug den letzten passenden Snapshot und vergleicht dagegen die Metadaten jeder Datei. Gesucht wird standardmäßig innerhalb derselben Gruppe aus Rechnername und Pfaden. Schwankt der Name, findet der Lauf keinen Vorgänger — und liest den kompletten Quellbestand neu ein. Die Handbuchseite benennt das ausdrücklich als Grund, den Namen von Hand zu setzen: „um ein teures erneutes Einlesen zu verhindern".
Hier ist eine Präzisierung wichtig, damit man den Effekt nicht überzeichnet: Betroffen ist das Lesen, nicht das Speichern. Die Deduplizierung arbeitet über den gesamten Speicher hinweg und erkennt die Inhalte trotzdem wieder — es wird also kaum etwas übertragen und praktisch nichts zusätzlich abgelegt. Der Preis ist Zeit und Rechenlast, nicht Speicherplatz.
Zweitens bei der Aufbewahrung. Der Befehl zum Ausdünnen alter Snapshots gruppiert vor dem Anwenden der Regeln — standardmäßig ebenfalls nach Rechnername und Pfaden — und wendet die Regel dann auf jede Gruppe einzeln an. Bei einem schwankenden Namen entstehen zwei Gruppen, und eine Regel wie „behalte die letzten sieben Tage" behält plötzlich zwei getrennte Reihen.
Und jetzt kommt die Stelle, die diesen Fehler so gut versteckt: Der Befehl, der Snapshots auflistet, gruppiert standardmäßig gar nicht. Die Liste sieht beim Draufschauen völlig normal aus. Die Aufspaltung wirkt sich erst beim Ausdünnen aus — also genau dann, wenn gelöscht wird. Der Fehler verbirgt sich vor dem Befehl, mit dem man ihn suchen würde.
Die Konsequenz steht seither als feste Konvention in der Dokumentation der Sicherungsskripte: Der Snapshot-Name wird hart gesetzt und nicht vom Betriebssystem übernommen. Dieselben festen Namen gelten auch für alle anderen Inventare — damit ein Rechner überall gleich heißt.
Was bleibt
Beide Stränge dieses Nachmittags haben dieselbe Form. Ein System verlässt sich auf eine Angabe, die stabil aussieht, aber keine Zusage trägt — der Rechnername beim einen, die Rückmeldung des Wächters beim anderen. Und in beiden Fällen ist der Ausfall unsichtbar, weil sich nichts meldet.
Drei Dinge haben wir daraus mitgenommen:
Ein Überwachungssystem braucht einen zweiten, unabhängigen Kanal. Er muss nichts Kluges können. Er muss genau eine Frage beantworten: Hat sich der Wächter heute gemeldet? Diese Frage darf nicht dasselbe System beantworten, das überwacht wird.
Ein Katalog braucht dieselbe Aufbewahrungsregel wie das, was er katalogisiert — und die Regel ist erst vollständig, wenn nach dem Löschen auch aufgeräumt wird. Eine Löschregel ohne Kompaktierung erzeugt genau das Bild, das wir hier hatten: wenige Einträge, riesige Datei.
Alarmierung darf nicht ignorierbar werden. Ein verwandter Fall aus derselben Woche: Für die manuell angestoßenen Sicherungen wurde bewusst kein Zeitplan eingerichtet, weil das Ziel-Gerät nicht durchläuft. Hätte man sie in die reguläre Überfälligkeitsprüfung aufgenommen, leuchteten sie nach wenigen Tagen dauerhaft rot. Ein Überwachungssystem, das regelmäßig Fehlalarme erzeugt, wird irgendwann ignoriert — und dann übersieht man den echten.
Teil 4 handelt vom letzten offenen Punkt: Was nützt ein vollständiges, überwachtes Backup, wenn der Schlüssel dazu im Backup liegt?
Quellenverzeichnis
Alle Quellen abgerufen am 20.08.2026.
- SQLite, „VACUUM" — Arbeitsweise über temporäre Kopie und Rückschreiben; Angabe zum Platzbedarf („as much as twice the size of the original database file is required in free disk space"); Variante mit direktem Schreiben in eine Zieldatei.
- SQLite, „Pragma statements" — Verhalten nach dem Löschen (Angabe: „the database file remains the same size… the database file does not shrink"); Abfrage der Freiliste; Einstellungen für Temporärdateien.
- SQLite, „Temporary Files Used By SQLite" — Ablageort der temporären Datenbank und Suchreihenfolge; Angabe, dass Journal und Write-Ahead-Log stets im Verzeichnis der Datenbank liegen.
- SQLite, „Write-Ahead Logging" — Übertrag in die Hauptdatei, automatischer Auslöser und Bedingungen, unter denen er nicht durchläuft.
- SQLite, „Database File Format" — Aufbau der Freiliste; Überlaufseiten bei großen Werten.
- Restic-Dokumentation, „Backing up" — Ermittlung des Vorgänger-Snapshots (Angabe: Gruppierung nach Rechnername und Pfaden als Voreinstellung); Erkennung unveränderter Dateien über Metadaten.
- Restic-Dokumentation, „Removing backup snapshots" — Gruppierung vor Anwendung der Aufbewahrungsregeln (Angabe: „The policy is then applied to each group of snapshots individually").
- Restic, Entwurfsdokument — inhaltsbasierte Zerlegung und repository-weite Deduplizierung über Prüfsummen.
Transparenz: Der geschilderte Vorfall stammt aus dem Betrieb der eigenen Infrastruktur; alle Zahlen sind gemessen. Server-, Datei- und Verzeichnisnamen sowie Adressen sind durch Gattungsbegriffe ersetzt. Die Aussage zum Aufräumen einer Datenbank auf ein anderes Dateisystem stammt aus der Dokumentation und wurde nach dem Vorfall recherchiert — sie war zum Zeitpunkt der Störung nicht bekannt und kam dort nicht zum Einsatz. Es besteht keine Geschäftsbeziehung zu den genannten Softwareprojekten.
Der Artikel wurde mit KI-Unterstützung (Claude) recherchiert und geschrieben; die technischen Aussagen wurden gegen Primärquellen geprüft, die redaktionelle Verantwortung liegt bei Marco Fuhrmann.