Am 27. November habe ich in einer Testdatenbank ein DELETE FROM bestellungen ohne WHERE ausgeführt. Es war 11:22 Uhr, die Datenbank war eine Kopie, und es war Absicht. Ich wollte wissen, was passiert, wenn es keine Absicht gewesen wäre – und ob ich den Zustand von vor fünfzehn Minuten wiederherstellen kann, ohne den ganzen Tag zu verlieren.

Die Antwort war: ja, aber nicht in fünfzehn Minuten. Der Restore hat 22 Minuten gedauert, und das ist die gute Nachricht. Die schlechte Nachricht ist der sechste Versuch, bei dem ich eine Stunde gebraucht habe, weil ein WAL-Segment fehlte.

Was WAL-G macht

Zwei Dinge, die man auseinanderhalten muss. Erstens das Basisbackup: ein konsistenter Schnappschuss der Datenfiles, in meinem Fall einmal pro Tag um 02:00 Uhr, komprimiert in einen S3-kompatiblen Speicher. Zweitens die WAL-Archivierung: Jede Änderung, die Postgres schreibt, wird als Segment hochgeladen, sobald es voll ist oder spätestens nach 60 Sekunden.

Point-in-Time-Recovery setzt beides zusammen: Du spielst das Basisbackup ein und lässt Postgres die WAL-Segmente bis zu einem Zeitpunkt abspielen. Der Zeitpunkt ist ein Zeitstempel oder eine LSN, und genau dort liegt die erste Falle. „15 Minuten zurück“ ist keine Angabe, mit der ein Werkzeug arbeitet – es braucht ein recovery_target_time, und das muss in UTC sein, sonst landest du eine oder zwei Stunden daneben.

Sechs Restores, sechs Zeiten

Terminal mit Restore-Ausgabe und daneben eine handgeschriebene Tabelle mit Zeiten
Meine Messreihe vom Dezember, direkt nach dem sechsten Versuch notiert. Der Ausreißer nach oben war kein langsamer Speicher, sondern ein fehlendes Segment.

Ich habe sechs Ziele gewählt: 15 Minuten, eine Stunde, sechs Stunden, ein Tag, sieben Tage zurück und einen Zeitpunkt kurz vor dem Löschen. Jeder Restore in eine frische Instanz, gleiche Hardware, gleiche Netzwerkstrecke. Gemessen habe ich von „Befehl abgeschickt“ bis „Datenbank nimmt Verbindungen an und die Zeilenzahl stimmt“.

VersuchZiel (zurück)DauerErgebnis
115 Minuten22 minok, 1.284 fehlende Zeilen erwartet
21 Stunde26 minok
36 Stunden31 minok
41 Tag38 minok
57 Tage47 minok, aber Basisbackup von vor 8 Tagen
6vor dem DELETE61 minerst nach manuellem Eingriff

Versuch 6 ist der lehrreiche. Postgres brach mit „could not find WAL segment“ ab, und zwar an einer Stelle, die im Log wie ein Datenproblem aussah, aber keines war: Beim Failover im Oktober hatte der neue Primary eine neue Timeline begonnen, und die WAL-Kette hatte dort eine Lücke. Die Archivierung lief weiter, aber ein Segment fehlte dauerhaft. Ohne diesen Test hätte ich das erst im Ernstfall gemerkt.

Wie ich die Zeiten gemessen habe

Ich habe jeden Restore mit Zeitstempeln in eine CSV geschrieben, weil ich die Zahlen nicht aus dem Kopf in einen Blogpost übertragen wollte. Der Unterschied zwischen „Restore fertig“ und „Datenbank nutzbar“ ist übrigens real: Nach dem letzten WAL-Segment braucht Postgres noch Zeit für den Consistency-Check, und in meinem Fall waren das vier bis neun Minuten.

import pandas as pd

runs = pd.read_csv("restores.csv")
# spalten: versuch, ziel_minuten, start_utc, pg_ready_utc, zeilen_ok

runs["start"] = pd.to_datetime(runs.start_utc, utc=True)
runs["ready"] = pd.to_datetime(runs.pg_ready_utc, utc=True)
runs["rto_min"] = (runs.ready - runs.start).dt.total_seconds() / 60

print(runs[["versuch", "ziel_minuten", "rto_min", "zeilen_ok"]])
print("Median RTO:", round(runs.rto_min.median(), 1), "min")
print("Maximum:  ", round(runs.rto_min.max(), 1), "min")

# Wie viel Zeit kostet das Abspielen pro Stunde Rückwärtsgang?
runs["pro_stunde"] = runs.rto_min / (runs.ziel_minuten / 60)
print(runs[["ziel_minuten", "rto_min", "pro_stunde"]].round(2))

Die letzte Spalte war die interessanteste: Der Restore wird nicht linear teurer. Von 15 Minuten auf eine Stunde kostet vier Minuten mehr, von einem Tag auf sieben Tage neunzehn. Der Grund ist, dass das Basisbackup den größten Block ausmacht und die WAL-Segmente danach relativ günstig sind. Für die Planung heißt das: Die Zielzeit ist fast egal, die Backup-Größe nicht.

Was ich nicht getestet habe

Ich habe nicht getestet, was passiert, wenn der Speicher langsam ist. Meine Messung lief gegen ein lokales MinIO im selben Netz, und in der Produktion liegt der Bucket woanders. Ich habe auch keine parallelen Restores getestet, und ich habe die Wiederherstellung in die bestehende Instanz nicht angefasst – nur in frische Instanzen. Das sind drei Lücken, die ich kenne und die in einem echten Notfall relevant werden.

Was ich getestet habe, ist die Frage, die zählt: Wie lange dauert es, bis jemand wieder arbeiten kann. Die Antwort ist eine Zahl mit einer Verteilung, nicht ein Gefühl von „das läuft schon“. Und sie hängt an einer Kleinigkeit, die niemand aufschreibt: ob die WAL-Kette lückenlos ist.

Fazit

Ein Backup ohne Restore-Test ist eine Datei mit Hoffnung. Bei mir hat der Test einen Fehler gefunden, der seit zwei Monaten im Archiv lag, und er hat eine Zahl geliefert, mit der ich argumentieren kann: 22 Minuten für 15 Minuten zurück, 47 Minuten für sieben Tage. Diese Zahlen gehören in den Betriebshandbuch, nicht in ein Wiki, das niemand liest.

Meine Frage an dich: Wann hast du zuletzt einen Restore gefahren, ohne dass es ein Notfall war – und weißt du, ob deine WAL-Kette an der Stelle des letzten Failovers noch vollständig ist?