Warum die meisten pg_dump-Cron-Jobs versagen, wenn man sie wirklich braucht
Fünf Ausfallmuster von pg_dump-Cron-Jobs, sortiert danach, wie hart sie bei einer Wiederherstellung zuschlagen.
In kleinen selbst verwalteten PostgreSQL-Setups tauchen dieselben Backup-Fehler immer wieder auf: eine Cron-Zeile, die seit drei Jahren in /etc/cron.d/ liegt, tar.gz-Dateien, die sich auf einer Storage-Box stapeln, die niemand mehr eingehängt hat, und kein Restore-Versuch, seit der ursprüngliche Ingenieur das Unternehmen verlassen hat. Dann stirbt eines Morgens der Primary — und das Archiv entpackt irgendetwas, aber nicht die Datenbank, die die Anwendung erwartet.
Das sind fünf Ausfallmuster, die in Restore-Reviews immer wieder auftauchen, geordnet nach wahrscheinlichem Schadensausmaß am Restore-Tag.
1. Der Exit-Code lügt, weil Cron sich nicht darum kümmert
Der Klassiker:
0 2 * * * postgres pg_dump mydb | gzip > /backups/mydb-$(date +\%F).sql.gz
Was Cron meldet: Erfolg. Was tatsächlich passiert ist, als die Disk um 02:41 Uhr vollief: gzip hat mit einem Fehler-Exit beendet, der stdout von pg_dump wurde abgeschnitten, die resultierende .sql.gz enthält 90 % der Datenbank. Wegen der Pipe ist der Exit-Code der Cron-Zeile der von gzip, nicht der von pg_dump. Und Cron sendet nur bei einem Nicht-Null-Exit eine Benachrichtigung — was nie passiert, weil set -o pipefail in roher Cron-Syntax nicht verfügbar ist.
Ein typisches Ergebnis: Ein mehrgigabyte großer Dump stoppt mitten in einem
COPYeiner großen Tabelle, die Datei wiegt 80–90 % des Solls, und niemand bemerkt es monatelang — weil nichts fehler gibt, die Datei nur kleiner als erwartet aussieht.
Der Fix ist unspektakulär: in ein Shell-Skript einwickeln, set -euo pipefail setzen, laut scheitern. Oder die Pipe ganz weglassen mit pg_dump --compress=6 -Fc -f /backups/mydb.dump, was eine einzelne Datei produziert, deren Exit-Code der eigene Exit-Code des Dumps ist.
2. --format=plain, weil die Stack-Overflow-Antwort von 2014 das so sagte
Plain-SQL-Dumps wirken freundlich. Man kann sie im Texteditor öffnen, man kann grep verwenden. Sie sind aber auch:
- Bei Restore single-threaded — der 8-Kern-Restore-Host arbeitet mit 1/8 seiner Kapazität.
- Nicht in der Lage, eine Teilmenge von Objekten ohne manuelle Bearbeitung wiederherzustellen.
- Nicht prüfbar mit den üblichen Integritätswerkzeugen (
pg_restore -l, das TOC). - Typischerweise 3–4× größer als das Custom-Format bei gleichem
--compress=9.
Custom-Format (-Fc) ist die richtige Wahl: parallel wiederherstellbar, hat ein Inhaltsverzeichnis, und pg_restore -l zeigt, ob das Archiv überhaupt lesbar ist, bevor man einen Restore versucht. Für Datenbanken ab mehreren TB ist das Directory-Format (-Fd) mit --jobs=N den Lernaufwand wert.
3. statement_timeout greift am Server, nicht am Client
Produktionsdatenbanken haben routinemäßig statement_timeout = '5min' als sinnvollen Standard gesetzt, um laufende Queries zu beenden. pg_dump führt lange Queries aus — konkret COPY (SELECT * FROM huge_table) TO STDOUT, was bei einer 500-GB-Tabelle eine Stunde dauern kann. Wenn der Server-Timeout auslöst, bricht die Verbindung ab. pg_dump gibt einen Fehler aus, produziert je nach Version aber möglicherweise keine sichtbare Fehlermeldung.
Fix: statement_timeout = 0 in der Session-Konfiguration der Backup-Rolle setzen, nicht global.
ALTER ROLE backup_user SET statement_timeout = 0;
Dann prüfen, ob es tatsächlich greift:
psql -U backup_user -c "SHOW statement_timeout"
# 0
Auch idle_in_transaction_session_timeout prüfen — dieser beißt bei langsamen parallelen Workers.
4. Retention via find -mtime +30 -delete
Die Aufräum-Zeile ist fast immer auf dieselbe Weise falsch:
find /backups -name "mydb-*.sql.gz" -mtime +30 -delete
Was passiert, wenn die Disk voll läuft und der gestrige Dump nicht geschrieben wurde? Die neueste Datei in /backups ist jetzt 31 Tage alt. -mtime +30 löscht sie auch. Zwei Nächte später hat man null Backups und einen fragilen Primary.
Retention sollte die N neuesten behalten heißen, nicht älter als X löschen. Und es sollte sich komplett weigern zu laufen, wenn der heutige Dump nicht existiert. Jedes Backup-Tool, das seinen Job ernst nimmt, folgt dieser Regel.
5. Niemand hat jemals wiederhergestellt
Das ist das Problem, das man nicht mit besseren Flags lösen kann. Schemas ändern sich. Extensions werden hinzugefügt und entfernt. Rollen werden umbenannt. Ein Dump, der vor 18 Monaten sauber wiederhergestellt werden konnte, braucht heute vielleicht drei manuelle Patches — und niemand weiß das, bis man es versucht.
Der einzige zuverlässige Fix ist: wiederherstellen. Nicht simulieren. Nicht pg_restore -l. Wirklich wiederherstellen, in eine Wegwerf-Zielumgebung, regelmäßig, automatisch. Wenn man keine Wegwerf-Zielumgebung aufsetzen kann, ist das bereits der Befund des Audits.
Günstigste Version: Wöchentlicher Cron,
docker run --rm postgres:16,pg_restore,SELECT count(*) FROM users, Zahl loggen. Wenn das Skript einen Fehler gibt oder die Zahl null ist, jemanden alarmieren. 30 Zeilen Bash.
Warum Resistro existiert
Diese Liste erklärt auch mehrere Produktentscheidungen in Resistro: ein einziges Binary, ein Exit-Code dem man vertrauen kann, Custom-Format als Standard, Retention nach Anzahl mit einem heute-existiert-der-Dump-Guard, Prometheus-gescrapte Metriken, damit das Monitoring verpasste Läufe erkennt — bevor der Restore-Tag es tut.
Nichts davon ist neu. Es ist größtenteils die Produktisierung von Fehlermustern, die sich in kleinen selbst verwalteten Backup-Setups wiederholen.
Wenn Sie ein zweites Augenpaar für Ihr Backup-Setup möchten: ein 1-Tages-Backup-Audit analysiert jede Pipeline, die Sie heute betreiben, bewertet jede einzeln und hinterlässt Ihnen einen schriftlichen Bericht. Festpreis, kein Retainer. Wenn alles in Ordnung ist, werde ich das sagen — und wir sind in einem halben Tag fertig.
