Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

Ich möchte meine erste erfolgreiche Erfahrung mit der Wiederherstellung der vollständigen Funktionsfähigkeit einer Postgres-Datenbank mit Ihnen teilen. Mit der Datenbank-Management-System (DBMS) Postgres habe ich vor einem halben Jahr Bekanntschaft geschlossen, vorher hatte ich keinerlei Erfahrungen in der Datenbankadministration.

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

Ich arbeite als halb DevOps-Ingenieur in einem großen IT-Unternehmen. Unser Unternehmen entwickelt Software für stark belastete Dienste, ich bin jedoch verantwortlich für die Funktionsfähigkeit, Wartung und Bereitstellung. Vor mir lag die Standardaufgabe, eine Anwendung auf einem Server zu aktualisieren. Die Anwendung ist in Django geschrieben, während des Updates werden Migrationen (Änderungen der Datenbankstruktur) ausgeführt, und vor diesem Prozess erstellen wir einen vollständigen Dump der Datenbank mit dem Standardprogramm pg_dump für alle Fälle.

Während der Erstellung des Dumps trat ein unvorhergesehenes Problem auf (Postgres-Version – 9.5):

pg_dump: Das Dumpen des Inhalts der Tabelle „ws_log_smevlog“ ist fehlgeschlagen: PQgetResult() ist fehlgeschlagen.
pg_dump: Fehlermeldung vom Server: FEHLER: ungültige Seite im Block 4123007 der Tabelle base/16490/21396989
pg_dump: Der Befehl war: COPY public.ws_log_smevlog [...]
pg_dump: [parallel archive] Ein Arbeitsprozess ist unerwartet beendet worden.

Fehler „ungültige Seite im Block“ weist auf Probleme auf der Dateisystemebene hin, was sehr schlecht ist. In verschiedenen Foren wurde vorgeschlagen, FULL VACUUM mit der Option zero_damaged_pages zur Lösung dieses Problems durchzuführen. Na gut, probieren wir es…

Vorbereitung zur Wiederherstellung

ACHTUNG! Stellen Sie sicher, dass Sie ein Backup von Postgres erstellen, bevor Sie irgendwelche Versuche zur Wiederherstellung der Datenbank unternehmen. Wenn Sie eine virtuelle Maschine haben, stoppen Sie die Datenbank und erstellen Sie einen Snapshot. Wenn es keine Möglichkeit gibt, einen Snapshot zu machen, stoppen Sie die Datenbank und kopieren Sie den Inhalt des Postgres-Verzeichnisses (einschließlich der WAL-Dateien) an einen sicheren Ort. Das Wichtigste in unserem Fall ist, nicht mehr Schaden anzurichten. Lesen Sie dieses.

Da die Datenbank insgesamt funktionierte, begnügte ich mich mit einem normalen Datenbank-Dump, aber ich schloss die Tabelle mit den beschädigten Daten aus (Option -T, —exclude-table=TABLE in pg_dump).

Der Server war physisch, einen Snapshot zu machen war unmöglich. Backup ist gemacht, weiter geht's.

Überprüfung des Dateisystems

Vor dem Versuch, die Datenbank wiederherzustellen, müssen wir sicherstellen, dass mit dem Dateisystem alles in Ordnung ist. Und im Falle von Fehlern müssen diese behoben werden, da man ansonsten nur noch mehr Schaden anrichten kann.

In meinem Fall war das Dateisystem mit der Datenbank in „/srv“ eingehängt und der Typ war ext4.

Stoppen Sie die Datenbank: systemctl stop postgresql@9.5-main.service und überprüfen, dass das Dateisystem von niemandem verwendet wird und es mit dem Befehl ausgehängt werden kann lsof:
lsof +D /srv

Ich musste auch die Redis-Datenbank stoppen, da sie ebenfalls verwendet wurde „/srv“. Danach habe ich es ausgehängt /srv (umount).

Die Überprüfung des Dateisystems wurde mit dem Tool e2fsck mit dem Schalter -f (Zwangsüberprüfung, selbst wenn das Dateisystem als sauber markiert ist):

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

Danach habe ich mit dem Tool dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep checked) kann man sicherstellen, dass die Überprüfung tatsächlich durchgeführt wurde:

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

e2fsck sagt, dass keine Probleme auf Ebene des ext4-Dateisystems gefunden wurden, was bedeutet, dass wir mit den Versuchen zur Wiederherstellung der Datenbank fortfahren können, genauer gesagt zurück zu vacuum full (natürlich muss das Dateisystem wieder eingehängt und die Datenbank neu gestartet werden).

Wenn Sie einen physischen Server haben, stellen Sie sicher, dass Sie den Zustand der Festplatten überprüfen (über smartctl -a /dev/XXX) oder den RAID-Controller, um sicherzustellen, dass das Problem nicht auf Hardwareebene liegt. In meinem Fall stellte sich der RAID als „Eisen“ heraus, deshalb bat ich den lokalen Administrator, den Zustand des RAID zu überprüfen (der Server war Hunderte von Kilometern entfernt). Er sagte, dass keine Fehler vorliegen, was bedeutet, dass wir mit der Wiederherstellung beginnen können.

Versuch 1: zero_damaged_pages

Wir verbinden uns über psql mit der Datenbank mit einem Benutzerkonto, das über Superuser-Rechte verfügt. Wir benötigen genau diesen Superuser, da die Option zero_damaged_pages nur von ihm geändert werden kann. In meinem Fall ist das postgres:

psql -h 127.0.0.1 -U postgres -s [database_name]

Option zero_damaged_pages ist notwendig, um Lesefehler zu ignorieren (von der Seite postgrespro):

Wenn ein beschädigter Seitenheader festgestellt wird, gibt Postgres Pro normalerweise einen Fehler aus und bricht die aktuelle Transaktion ab. Wenn die Option zero_damaged_pages aktiviert ist, gibt das System stattdessen eine Warnung aus, setzt die beschädigte Seite im Speicher auf Null und fährt mit der Verarbeitung fort. Dieses Verhalten zerstört Daten, insbesondere alle Zeilen in der beschädigten Seite.

Wir aktivieren die Option und versuchen ein vollständiges Vacuum der Tabelle durchzuführen:

VACUUM FULL VERBOSE

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.
Leider ein Misserfolg.

Wir sind auf einen ähnlichen Fehler gestoßen:

INFO: vacuuming "“public.ws_log_smevlog”\nWARNING: ungültige Seite im Block 4123007 der Relation base/16400/21396989; Seite wird auf Null gesetzt\nERROR: unerwartete Chunk Nummer 573 (erwartet 565) für Toast-Wert 21648541 in pg_toast_106070

pg_toast – das Speichermechanismus für "lange Daten" in Postgres, wenn sie nicht auf eine Seite passen (standardmäßig 8 KB).

Versuch 2: reindex

Der erste Ratschlag aus Google hat nicht geholfen. Nach ein paar Minuten Suche fand ich den zweiten Ratschlag – ein reindex beschädigter Tabelle. Dieser Tipp tauchte an vielen Orten auf, aber er war nicht vertrauenswürdig. Lass uns reindexieren:

reindexiere Tabelle ws_log_smevlog

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

reindex beendet ohne Probleme.

Das hat jedoch nicht geholfen, VACUUM FULL stürzte mit einem ähnlichen Fehler ab. Da ich an Misserfolge gewöhnt bin, begann ich weiter nach Ratschlägen im Internet zu suchen und stieß auf etwas Interessantes einen Artikel.

Versuch 3: SELECT, LIMIT, OFFSET

In dem oben genannten Artikel wurde vorgeschlagen, die Tabelle zeilenweise zu betrachten und problematische Daten zu löschen. Zunächst musste ich alle Zeilen durchsehen:

for ((i=0; i/dev/null || echo $i; done

In meinem Fall enthielt die Tabelle 1 628 991 Zeilen! Eigentlich wäre es besser gewesen, der Datenpartitionierung, aber das ist ein Thema für ein separates Gespräch. Es war Samstag, ich startete diesen Befehl in tmux und ging schlafen:

for ((i=0; i/dev/null || echo $i; done

Am Morgen wollte ich nachsehen, wie es lief. Zu meinem Erstaunen stellte ich fest, dass in 20 Stunden nur 2% der Daten gescannt wurden! 50 Tage wollte ich nicht warten. Ein weiterer totaler Fehlschlag.

Aber ich wollte nicht aufgeben. Es interessierte mich, warum das Scannen so lange dauerte. Aus der Dokumentation (wieder bei postgrespro) erfuhr ich:

OFFSET gibt an, wie viele Zeilen übersprungen werden sollen, bevor mit der Ausgabe von Zeilen begonnen wird.
Wenn sowohl OFFSET als auch LIMIT angegeben sind, überspringt das System zunächst die OFFSET-Zeilen und beginnt dann mit dem Zählen der Zeilen für das LIMIT.

Bei der Verwendung von LIMIT ist es wichtig, auch die ORDER BY-Anweisung zu verwenden, damit die Ergebnismengen in einer bestimmten Reihenfolge ausgegeben werden. Andernfalls werden unvorhersehbare Teilmengen von Zeilen zurückgegeben.

Offensichtlich war der obige Befehl fehlerhaft: Erstens fehlte ein order by, das Ergebnis könnte fehlerhaft sein. Zweitens musste Postgres zunächst die OFFSET-Zeilen scannen und überspringen, und mit steigendem OFFSET würde die Leistung weiter sinken.

Versuch 4: Einen Dump im Textformat erstellen

Dann kam mir eine scheinbar geniale Idee: Mach einen Dump im Textformat und analysiere die letzte aufgezeichnete Zeile.

Aber zuerst lassen Sie uns die Struktur der Tabelle ws_log_smevlog:

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

In unserem Fall haben wir eine Spalte "id", die die eindeutige Identifikationsnummer (Zähler) der Zeile enthielt. Der Plan war folgendermaßen:

  1. Beginnen wir mit der Erstellung eines Dumps im Textformat (als SQL-Befehle)
  2. Zu einem bestimmten Zeitpunkt wurde der Dump aufgrund eines Fehlers unterbrochen, aber die Textdatei wurde dennoch auf der Festplatte gespeichert.
  3. Wir schauen ans Ende der Textdatei, um die ID der letzten erfolgreich erstellten Zeile zu finden.

Ich begann mit dem Erstellen des Dumps im Textformat:

pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dump

Wie erwartet wurde das Dumpen aufgrund des gleichen Fehlers unterbrochen:

pg_dump: Fehlermeldung vom Server: FEHLER: ungültige Seite im Block 4123007 der Tabelle base/16490/21396989

Dann habe ich über tail tail -5 ./my_dump.dump) das Ende des Dumps überprüft und festgestellt, dass der Dump bei der Zeile mit der ID. „Das Problem liegt also bei der Zeile mit der ID 186 526, die ist beschädigt, die muss ich löschen!“, dachte ich. Aber nachdem ich eine Abfrage in der Datenbank gemacht hatte: 186 525select * from ws_log_smevlog where id=186529
«„Stellte sich heraus, dass mit dieser Zeile alles in Ordnung ist… Die Zeilen mit den IDs 186 530 – 186 540 funktionierten ebenfalls einwandfrei. Eine weitere „geniale Idee“ ist gescheitert. Später verstand ich, warum das so war: Wenn Daten aus der Tabelle gelöscht oder geändert werden, werden sie nicht physisch gelöscht, sondern als „tote Tupel“ markiert, dann kommt.und markiert diese Zeilen als gelöscht und erlaubt, diese Zeilen erneut zu verwenden. Zum Verständnis, wenn Daten in der Tabelle geändert werden und autovacuum aktiviert ist, werden sie nicht in aufeinanderfolgender Reihenfolge gespeichert. autovacuum Versuch 5: SELECT, FROM, WHERE id=

Misserfolge machen uns stärker. Man sollte niemals aufgeben, man muss bis zum Ende gehen und an sich und seine Möglichkeiten glauben. Deshalb beschloss ich, noch eine weitere Variante auszuprobieren: einfach alle Einträge in der Datenbank einzeln zu überprüfen. Da ich die Struktur meiner Tabelle kenne (siehe oben), haben wir ein Feld id, das einzigartig ist (Primärschlüssel). In der Tabelle sind 1 628 991 Zeilen und

gehen der Reihenfolge nach, was bedeutet, dass wir sie einfach einzeln durchgehen können: id for ((i=1; i/dev/null || echo $i; done

Falls jemand nicht versteht, funktioniert der Befehl folgendermaßen: Er durchläuft die Tabelle zeilenweise und sendet stdout in

, aber wenn der SELECT-Befehl fehlschlägt, wird der Fehlertext ausgegeben (stderr wird in die Konsole gesendet) und die Zeile, die den Fehler enthält, wird ausgegeben (dank der ||, die besagt, dass der SELECT probleme hatte (Rückgabecode des Befehls nicht 0)). /dev/nullIch hatte Glück, ich hatte Indizes für das Feld erstellt.

Das bedeutet, dass das Auffinden der Zeile mit der benötigten ID nicht lange dauern sollte. Theoretisch sollte es funktionieren. Also, starten wir den Befehl in id:

Mein erster Versuch, eine Postgres-Datenbank nach einem Ausfall (invalid page in block 4123007 von relatton base/16490) wiederherzustellen.

Das bedeutet, dass das Finden der Zeile mit der benötigten ID nicht viel Zeit in Anspruch nehmen sollte. Theoretisch sollte es funktionieren. Nun, wir führen den Befehl in tmux Und jetzt gehen wir schlafen.

Am Morgen stellte ich fest, dass etwa 90.000 Datensätze angesehen wurden, was etwas mehr als 5 % ausmacht. Ein hervorragendes Ergebnis im Vergleich zur vorherigen Methode (2 %)! Aber 20 Tage warten wollte ich nicht...

Versuch 6: SELECT, FROM, WHERE id >= und id <

Für die Datenbank wurde dem Kunden ein hervorragender Server zugewiesen: ein Dual-Prozessor Intel Xeon E5-2697 v2, in unserem Setup hatten wir ganze 48 Threads! Die Serverlast war durchschnittlich, wir konnten problemlos etwa 20 Threads abholen. Auch der Arbeitsspeicher war ausreichend: satte 384 Gigabyte!

Deshalb musste das Team parallelisiert werden:

Falls jemand nicht versteht, funktioniert der Befehl folgendermaßen: Er durchläuft die Tabelle zeilenweise und sendet stdout in

Hier hätte man ein schönes und elegantes Skript schreiben können, aber ich wählte die schnellste Methode der Parallelisierung: den Bereich 0-1628991 manuell in Intervalle zu je 100.000 Datensätze zu unterteilen und separat 16 Befehle des Typs zu starten:

for ((i=N; i/dev/null || echo $i; done

Aber das ist noch nicht alles. Eigentlich benötigt auch die Verbindung zur Datenbank eine gewisse Zeit und Ressourcen. Es wäre nicht sehr vernünftig, 1.628.991 Datensätze anzuschließen, einverstanden? Deshalb lasst uns bei einer Verbindung 1.000 Zeilen statt einer abfragen. Am Ende wurde der Befehl zu diesem:

for ((i=N; i=$i and id/dev/null || echo $i; done

Öffnen Sie 16 Fenster in der tmux-Sitzung und starten Sie die Befehle:

1) for ((i=0; i=$i and id/dev/null || echo $i; done
2) for ((i=100000; i=$i and id/dev/null || echo $i; done
…
15) for ((i=1400000; i=$i and id/dev/null || echo $i; done
16) for ((i=1500000; i=$i and id/dev/null || echo $i; done

Einen Tag später erhielt ich die ersten Ergebnisse! Und zwar (die Werte XXX und ZZZ sind nicht mehr erhalten geblieben):

ERROR: missing chunk number 0 for toast value 37837571 in pg_toast_106070
829000
ERROR: missing chunk number 0 for toast value XXX in pg_toast_106070
829000
ERROR: missing chunk number 0 for toast value ZZZ in pg_toast_106070
146000

Das bedeutet, dass bei uns drei Zeilen einen Fehler enthalten. Die ID des ersten und zweiten problematischen Datensatzes lag zwischen 829.000 und 830.000, die ID des dritten zwischen 146.000 und 147.000. Danach mussten wir nur noch die exakte ID der problematischen Datensätze finden. Dazu durchlaufen wir unseren Bereich mit problematischen Datensätzen mit Schrittweite 1 und identifizieren die IDs:

for ((i=829000; i/dev/null || echo $i; done
829417
ERROR: unerwartete Chunknummer 2 (erwartet 0) für Toast-Wert 37837843 in pg_toast_106070
829449
for ((i=146000; i/dev/null || echo $i; done
829417
ERROR: unerwartete Chunknummer ZZZ (erwartet 0) für Toast-Wert XXX in pg_toast_106070
146911

Ein glückliches Ende

Wir haben problematische Zeilen gefunden. Wir greifen über psql auf die Datenbank zu und versuchen, sie zu löschen:

my_database=# delete from ws_log_smevlog where id=829417;
DELETE 1
my_database=# delete from ws_log_smevlog where id=829449;
DELETE 1
my_database=# delete from ws_log_smevlog where id=146911;
DELETE 1

Zu meiner Überraschung wurden die Einträge ohne Probleme gelöscht, sogar ohne Option zero_damaged_pages.

Danach habe ich mich mit der Datenbank verbunden und VACUUM FULL (ich denke, es war nicht unbedingt nötig), und schließlich habe ich erfolgreich ein Backup mit pg_dump. Das Dump wurde ohne irgendwelche Fehler erstellt! Das Problem konnte auf diese einfachste Weise gelöst werden. Es gab grenzenlos Freude, nachdem ich nach so vielen Rückschlägen eine Lösung gefunden hatte!

Danksagungen und Abschluss

So war meine erste Erfahrung mit der Wiederherstellung einer realen Postgres-Datenbank. Diese Erfahrung werde ich lange im Gedächtnis behalten.

Und zum Schluss möchte ich PostgresPro für die übersetzte Dokumentation ins Russische und für völlig kostenlose Online-Kurse, die mir während der Analyse des Problems sehr geholfen haben, danken.

Quelle: habr.com

60GB SSD 8Gb DDR4