Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

Ich möchte mit Ihnen meine erste erfolgreiche Erfahrung bei der Wiederherstellung der vollen Funktionalität einer Postgres-Datenbank teilen. Ich habe mich vor einem halben Jahr mit dem Postgres-DBMS vertraut gemacht, davor hatte ich überhaupt keine Erfahrung in der Datenbankverwaltung.

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

Ich arbeite als Semi-DevOps-Ingenieur in einem großen IT-Unternehmen. Unser Unternehmen entwickelt Software für Hochlastdienste und ich bin für Leistung, Wartung und Bereitstellung verantwortlich. Mir wurde eine Standardaufgabe gegeben: eine Anwendung auf einem Server zu aktualisieren. Die Anwendung ist in Django geschrieben, während der Aktualisierung werden Migrationen durchgeführt (Änderungen in der Datenbankstruktur), und vor diesem Vorgang erstellen wir für alle Fälle einen vollständigen Datenbank-Dump über das Standardprogramm pg_dump.

Beim Erstellen eines Dumps ist ein unerwarteter Fehler aufgetreten (Postgres-Version 9.5):

pg_dump: Oumping the contents of table “ws_log_smevlog” failed: PQgetResult() failed.
pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989
pg_dump: The command was: COPY public.ws_log_smevlog [...]
pg_dunp: [parallel archtver] a worker process dled unexpectedly

Fehler „ungültige Seite im Block“ spricht von Problemen auf Dateisystemebene, was sehr schlimm ist. In verschiedenen Foren wurde dies vorgeschlagen VOLLSTÄNDIGES VAKUUM mit Option null_beschädigte_seiten um dieses Problem zu lösen. Nun, lasst es uns versuchen...

Vorbereitung auf die Genesung

WARNUNG! Erstellen Sie unbedingt ein Postgres-Backup, bevor Sie versuchen, Ihre Datenbank wiederherzustellen. Wenn Sie über eine virtuelle Maschine verfügen, stoppen Sie die Datenbank und erstellen Sie einen Snapshot. Wenn es nicht möglich ist, einen Snapshot zu erstellen, stoppen Sie die Datenbank und kopieren Sie den Inhalt des Postgres-Verzeichnisses (einschließlich WAL-Dateien) an einen sicheren Ort. Das Wichtigste in unserem Geschäft ist, die Dinge nicht noch schlimmer zu machen. Lesen es.

Da die Datenbank bei mir im Allgemeinen funktionierte, beschränkte ich mich auf einen regulären Datenbank-Dump, schloss aber die Tabelle mit beschädigten Daten aus (Option -T, --exclude-table=TABELLE in pg_dump).

Der Server war physisch, es war unmöglich, einen Snapshot zu erstellen. Das Backup wurde entfernt, fahren wir fort.

Überprüfung des Dateisystems

Bevor wir versuchen, die Datenbank wiederherzustellen, müssen wir sicherstellen, dass mit dem Dateisystem selbst alles in Ordnung ist. Und wenn Fehler passieren, korrigieren Sie sie, denn sonst kann es nur noch schlimmer werden.

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

Stoppen der Datenbank: systemctl stoppen [E-Mail geschützt] und überprüfen Sie, ob das Dateisystem von niemandem verwendet wird und mit dem Befehl ausgehängt werden kann lsof:
lsof +D /srv

Ich musste auch die Redis-Datenbank stoppen, da sie ebenfalls verwendet wurde „/srv“. Als nächstes stieg ich aus / srv (umount).

Das Dateisystem wurde mit dem Dienstprogramm überprüft e2fsck mit dem Schalter -f (Überprüfung erzwingen, auch wenn das Dateisystem als sauber markiert ist):

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

Als nächstes verwenden Sie das Dienstprogramm dumpe2fs (sudo dumpe2fs /dev/mapper/gu2 – sys-srv | grep überprüft) können Sie überprüfen, ob die Prüfung tatsächlich durchgeführt wurde:

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

e2fsck sagt, dass auf der Ebene des ext4-Dateisystems keine Probleme gefunden wurden, was bedeutet, dass Sie weiterhin versuchen können, die Datenbank wiederherzustellen bzw. zu ihr zurückzukehren Vakuum voll (Natürlich müssen Sie das Dateisystem wieder mounten und die Datenbank starten).

Wenn Sie einen physischen Server haben, überprüfen Sie unbedingt den Status der Festplatten (via smartctl -a /dev/XXX) oder einen RAID-Controller, um sicherzustellen, dass das Problem nicht auf Hardwareebene liegt. In meinem Fall stellte sich heraus, dass es sich bei dem RAID um „Hardware“ handelte, also bat ich den lokalen Administrator, den Status des RAID zu überprüfen (der Server war mehrere hundert Kilometer von mir entfernt). Er sagte, dass es keine Fehler gab, was bedeutet, dass wir definitiv mit der Wiederherstellung beginnen können.

Versuch 1: null_beschädigte_Seiten

Wir verbinden uns über psql mit einem Konto, das über Superuser-Rechte verfügt, mit der Datenbank. Wir brauchen einen Superuser, weil... Möglichkeit null_beschädigte_seiten nur er kann sich ändern. In meinem Fall ist es Postgres:

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

Option null_beschädigte_seiten wird benötigt, um Lesefehler zu ignorieren (von der Postgrespro-Website):

Wenn PostgreSQL einen beschädigten Seitenkopf erkennt, meldet es normalerweise einen Fehler und bricht die aktuelle Transaktion ab. Wenn „zero_damaged_pages“ aktiviert ist, gibt das System stattdessen eine Warnung aus, setzt die beschädigte Seite im Speicher auf Null und setzt die Verarbeitung fort. Dieses Verhalten zerstört Daten, nämlich alle Zeilen auf der beschädigten Seite.

Wir aktivieren die Option und versuchen, die Tabellen vollständig zu saugen:

VACUUM FULL VERBOSE

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)
Leider Pech.

Wir haben einen ähnlichen Fehler festgestellt:

INFO: vacuuming "“public.ws_log_smevlog”
WARNING: invalid page in block 4123007 of relation base/16400/21396989; zeroing out page
ERROR: unexpected chunk number 573 (expected 565) for toast value 21648541 in pg_toast_106070

pg_toast – ein Mechanismus zum Speichern „langer Daten“ in Poetgres, wenn diese nicht auf eine Seite passen (standardmäßig 8 KB).

Versuch 2: Neuindizierung

Der erste Rat von Google hat nicht geholfen. Nach ein paar Minuten der Suche habe ich den zweiten Tipp gefunden – zum Selbermachen neu indizieren beschädigter Tisch. Ich habe diesen Rat an vielen Stellen gesehen, aber er hat kein Vertrauen geweckt. Lassen Sie uns neu indizieren:

reindex table ws_log_smevlog

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

neu indizieren ohne Probleme abgeschlossen.

Dies hat jedoch nicht geholfen, VAKUUM VOLL stürzte mit einem ähnlichen Fehler ab. Da ich Misserfolge gewohnt bin, begann ich weiter im Internet nach Ratschlägen zu suchen und stieß auf einen recht interessanten Rat Artikel.

Versuch 3: SELECT, LIMIT, OFFSET

Im obigen Artikel wurde vorgeschlagen, die Tabelle Zeile für Zeile zu betrachten und problematische Daten zu entfernen. Zuerst mussten wir uns alle Zeilen ansehen:

for ((i=0; i<"Number_of_rows_in_nodes"; i++ )); do psql -U "Username" "Database Name" -c "SELECT * FROM nodes LIMIT 1 offset $i" >/dev/null || echo $i; done

In meinem Fall enthielt die Tabelle 1 628 991 Linien! Man musste gut aufpassen Datenpartitionierung, aber dies ist ein Thema für eine separate Diskussion. Es war Samstag, ich habe diesen Befehl in tmux ausgeführt und bin zu Bett gegangen:

for ((i=0; i<1628991; i++ )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog LIMIT 1 offset $i" >/dev/null || echo $i; done

Am Morgen beschloss ich, nachzusehen, wie die Dinge liefen. Zu meiner Überraschung stellte ich fest, dass nach 20 Stunden nur 2 % der Daten gescannt worden waren! Ich wollte nicht 50 Tage warten. Ein weiterer kompletter Misserfolg.

Aber ich habe nicht aufgegeben. Ich fragte mich, warum das Scannen so lange dauerte. Aus der Dokumentation (wieder auf Postgrespro) habe ich herausgefunden:

OFFSET gibt an, die angegebene Anzahl von Zeilen zu überspringen, 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 der Zählung der Zeilen für die LIMIT-Einschränkung.

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

Offensichtlich war der obige Befehl falsch: Erstens gab es nein Sortieren nach, könnte das Ergebnis fehlerhaft sein. Zweitens musste Postgres zunächst OFFSET-Zeilen scannen und überspringen, und zwar mit zunehmender Anzahl OFFSET Die Produktivität würde noch weiter sinken.

Versuch 4: Machen Sie einen Dump in Textform

Dann kam mir eine scheinbar brillante Idee: Erstellen Sie einen Dump in Textform und analysieren Sie die zuletzt aufgezeichnete Zeile.

Doch werfen wir zunächst einen Blick auf den Aufbau der Tabelle. ws_log_smevlog:

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

In unserem Fall haben wir eine Spalte "Ich würde", die den eindeutigen Bezeichner (Zähler) der Zeile enthielt. Der Plan war so:

  1. Wir beginnen mit der Erstellung eines Dumps in Textform (in Form von SQL-Befehlen).
  2. Zu einem bestimmten Zeitpunkt würde der Dump aufgrund eines Fehlers unterbrochen werden, die Textdatei wäre aber weiterhin auf der Festplatte gespeichert
  3. Wir schauen uns das Ende der Textdatei an und finden dabei die Kennung (id) der letzten Zeile, die erfolgreich entfernt wurde

Ich habe angefangen, einen Dump in Textform zu erstellen:

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

Der Dump wurde erwartungsgemäß mit demselben Fehler unterbrochen:

pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989

Weiter durch Schwanz Ich habe mir das Ende der Müllkippe angeschaut (tail -5 ./my_dump.dump) entdeckte, dass der Dump auf der Leitung mit der ID unterbrochen wurde 186 525. „Das Problem liegt also in der Zeile mit der ID 186 526, sie ist defekt und muss gelöscht werden!“ - Ich dachte. Aber eine Abfrage an die Datenbank stellen:
«Wählen Sie * aus ws_log_smevlog aus, wobei die ID = 186529 ist„Es stellte sich heraus, dass mit dieser Zeile alles in Ordnung war... Zeilen mit den Indizes 186 – 530 funktionierten ebenfalls problemlos. Eine weitere „geniale Idee“ scheiterte. Später verstand ich, warum das passierte: Beim Löschen und Ändern von Daten aus einer Tabelle werden diese nicht physisch gelöscht, sondern als „tote Tupel“ markiert, dann kommt Autovakuum und markiert diese Zeilen als gelöscht und ermöglicht die Wiederverwendung dieser Zeilen. Zum Verständnis: Wenn sich die Daten in der Tabelle ändern und die automatische Vakuumierung aktiviert ist, werden sie nicht sequentiell gespeichert.

Versuch 5: SELECT, FROM, WHERE id=

Misserfolge machen uns stärker. Sie sollten niemals aufgeben, Sie müssen bis zum Ende gehen und an sich selbst und Ihre Fähigkeiten glauben. Deshalb habe ich beschlossen, eine andere Option auszuprobieren: Sehen Sie sich einfach alle Datensätze in der Datenbank einzeln an. Da wir die Struktur meiner Tabelle kennen (siehe oben), haben wir ein eindeutiges ID-Feld (Primärschlüssel). Wir haben 1 Zeilen in der Tabelle und id sind in der richtigen Reihenfolge, was bedeutet, dass wir sie einfach einzeln durchgehen können:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

Falls es jemand nicht versteht: Der Befehl funktioniert wie folgt: Er durchsucht die Tabelle Zeile für Zeile und sendet stdout an / dev / null, aber wenn der SELECT-Befehl fehlschlägt, wird der Fehlertext gedruckt (stderr wird an die Konsole gesendet) und eine Zeile mit dem Fehler wird gedruckt (dank ||, was bedeutet, dass die Auswahl Probleme hatte (der Rückkehrcode des Befehls). ist nicht 0)).

Ich hatte Glück, ich hatte Indizes vor Ort erstellt id:

Meine erste Erfahrung mit der Wiederherstellung einer Postgres-Datenbank nach einem Fehler (ungültige Seite in Block 4123007 von Relatton Base/16490)

Das bedeutet, dass das Finden einer Zeile mit der gewünschten ID nicht viel Zeit in Anspruch nehmen sollte. Theoretisch sollte es funktionieren. Nun, lassen Sie uns den Befehl ausführen tmux und lass uns ins Bett gehen.

Bis zum Morgen stellte ich fest, dass etwa 90 Einträge angesehen wurden, was etwas mehr als 000 % entspricht. Ein hervorragendes Ergebnis im Vergleich zur vorherigen Methode (5%)! Aber ich wollte nicht 2 Tage warten...

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

Der Kunde verfügte über einen hervorragenden Server für die Datenbank: einen Dual-Prozessor Intel Xeon E5-2697 v2, es gab bis zu 48 Threads an unserem Standort! Die Auslastung des Servers war durchschnittlich, wir konnten problemlos ca. 20 Threads herunterladen. Auch RAM war ausreichend vorhanden: ganze 384 Gigabyte!

Daher musste der Befehl parallelisiert werden:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

Hier war es möglich, ein schönes und elegantes Skript zu schreiben, aber ich habe die schnellste Parallelisierungsmethode gewählt: Teilen Sie den Bereich 0-1628991 manuell in Intervalle von 100 Datensätzen auf und führen Sie separat 000 Befehle der Form aus:

for ((i=N; i<M; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

Aber das ist nicht alles. Theoretisch nimmt auch die Verbindung zu einer Datenbank einige Zeit und Systemressourcen in Anspruch. Sie werden mir zustimmen, dass die Verbindung von 1 nicht besonders clever war. Rufen wir daher 628 Zeilen anstelle einer einzelnen Verbindung ab. Infolgedessen verwandelte sich das Team in Folgendes:

for ((i=N; i<M; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

Öffnen Sie 16 Fenster in einer tmux-Sitzung und führen Sie die Befehle aus:

1) for ((i=0; i<100000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
2) for ((i=100000; i<200000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
…
15) for ((i=1400000; i<1500000; i=$((i+1000)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
16) for ((i=1500000; i<1628991; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

Einen Tag später erhielt ich die ersten Ergebnisse! Nämlich (die Werte XXX und ZZZ bleiben nicht mehr erhalten):

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 drei Zeilen einen Fehler enthalten. Die IDs des ersten und zweiten Problemdatensatzes lagen zwischen 829 und 000, die IDs des dritten zwischen 830 und 000. Als nächstes mussten wir lediglich den genauen ID-Wert der Problemdatensätze ermitteln. Dazu durchsuchen wir unser Sortiment mit problematischen Datensätzen in Schritten von 146 und ermitteln die ID:

for ((i=829000; i<830000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number 2 (expected 0) for toast value 37837843 in pg_toast_106070
829449
for ((i=146000; i<147000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number ZZZ (expected 0) for toast value XXX in pg_toast_106070
146911

Happy End

Wir haben die problematischen Zeilen gefunden. Wir gehen per psql in die Datenbank 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 auch ohne Option problemlos gelöscht null_beschädigte_seiten.

Dann habe ich eine Verbindung zur Datenbank hergestellt VAKUUM VOLL (Ich glaube, das war nicht nötig) und schließlich habe ich das Backup erfolgreich mit entfernt pg_dump. Der Dump wurde fehlerfrei erstellt! Das Problem wurde so dumm gelöst. Die Freude kannte keine Grenzen, nach so vielen Fehlschlägen gelang es uns, eine Lösung zu finden!

Danksagungen und Fazit

So verlief meine erste Erfahrung mit der Wiederherstellung einer echten Postgres-Datenbank. An dieses Erlebnis werde ich mich noch lange erinnern.

Abschließend möchte ich PostgresPro für die Übersetzung der Dokumentation ins Russische danken völlig kostenlose Online-Kurse, was bei der Analyse des Problems sehr hilfreich war.

Source: habr.com

Kommentar hinzufügen