Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

Ik wil graag mijn eerste succesvolle ervaring met het herstellen van een Postgres-database naar volledige functionaliteit met u delen. Een half jaar geleden maakte ik kennis met het Postgres DBMS; daarvoor had ik totaal geen ervaring met databasebeheer.

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

Ik werk als semi-DevOps engineer bij een groot IT-bedrijf. Ons bedrijf ontwikkelt software voor diensten met hoge belasting, en ik ben verantwoordelijk voor de prestaties, het onderhoud en de implementatie. Ik kreeg een standaardtaak: een applicatie op één server updaten. De applicatie is geschreven in Django, tijdens de update worden migraties uitgevoerd (veranderingen in de databasestructuur), en vóór dit proces nemen we voor de zekerheid een volledige databasedump via het standaard pg_dump-programma.

Er is een onverwachte fout opgetreden tijdens het maken van een dump (Postgres versie 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

Kever "ongeldige pagina in blok" spreekt over problemen op het niveau van het bestandssysteem, wat erg slecht is. Op diverse fora werd voorgesteld dit te doen VOL VACUÜM met optie nul_beschadigde_pagina's om dit probleem op te lossen. Nou, laten we het proberen...

Voorbereiden op herstel

LET Zorg ervoor dat u een Postgres-back-up maakt voordat u probeert uw database te herstellen. Als u een virtuele machine heeft, stopt u de database en maakt u een momentopname. Als het niet mogelijk is om een ​​momentopname te maken, stop dan de database en kopieer de inhoud van de Postgres-directory (inclusief wal-bestanden) naar een veilige plaats. Het belangrijkste in onze business is om de zaken niet nog erger te maken. Lezen het.

Omdat de database bij mij over het algemeen werkte, beperkte ik mij tot een reguliere databasedump, maar sloot ik de tabel met beschadigde gegevens uit (optie -T, --exclude-table=TABEL in pg_dump).

De server was fysiek, het was onmogelijk om een ​​momentopname te maken. De back-up is verwijderd, laten we verder gaan.

Controle van het bestandssysteem

Voordat we proberen de database te herstellen, moeten we ervoor zorgen dat alles in orde is met het bestandssysteem zelf. En als er fouten zijn, corrigeer ze dan, want anders kun je de zaken alleen maar erger maken.

In mijn geval was het bestandssysteem met de database aangekoppeld "/srv" en het type was ext4.

De database stoppen: systeemctl stopt [e-mail beveiligd] en controleer of het bestandssysteem door niemand wordt gebruikt en kan worden ontkoppeld met behulp van de opdracht lsof:
lsof +D /srv

Ik moest ook de redis-database stoppen, omdat deze ook in gebruik was "/srv". Vervolgens ontkoppelde ik / srv (umount).

Het bestandssysteem werd gecontroleerd met behulp van het hulpprogramma e2fsck met de schakelaar -f (Controle forceren, zelfs als het bestandssysteem als schoon is gemarkeerd):

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

Gebruik vervolgens het hulpprogramma dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep gecontroleerd) kunt u verifiëren dat de controle daadwerkelijk is uitgevoerd:

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

e2fsck zegt dat er geen problemen zijn gevonden op het ext4-bestandssysteemniveau, wat betekent dat u kunt blijven proberen de database te herstellen, of beter gezegd kunt terugkeren naar vacuüm vol (Je moet natuurlijk het bestandssysteem weer aankoppelen en de database starten).

Als je een fysieke server hebt, controleer dan zeker de status van de schijven (via smartctl -a /dev/XXX) of RAID-controller om er zeker van te zijn dat het probleem niet op hardwareniveau ligt. In mijn geval bleek de RAID “hardware” te zijn, dus vroeg ik de lokale beheerder om de status van de RAID te controleren (de server stond enkele honderden kilometers bij mij vandaan). Hij zei dat er geen fouten waren, wat betekent dat we zeker met de restauratie kunnen beginnen.

Poging 1: zero_damaged_pages

We maken verbinding met de database via psql met een account dat superuser-rechten heeft. We hebben een superuser nodig, omdat... keuze nul_beschadigde_pagina's alleen hij kan veranderen. In mijn geval is het postgres:

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

optie nul_beschadigde_pagina's nodig om leesfouten te negeren (van de postgrespro-website):

Wanneer PostgreSQL een beschadigde paginakop detecteert, rapporteert het doorgaans een fout en breekt het de huidige transactie af. Als zero_damaged_pages is ingeschakeld, geeft het systeem in plaats daarvan een waarschuwing, zet de beschadigde pagina in het geheugen op nul en gaat door met de verwerking. Door dit gedrag worden gegevens vernietigd, namelijk alle rijen op de beschadigde pagina.

We schakelen de optie in en proberen een volledig vacuüm van de tabellen te maken:

VACUUM FULL VERBOSE

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)
Helaas, pech.

We zijn een soortgelijke fout tegengekomen:

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 – een mechanisme voor het opslaan van “lange gegevens” in Poetgres als deze niet op één pagina passen (standaard 8 kb).

Poging 2: opnieuw indexeren

Het eerste advies van Google hielp niet. Na een paar minuten zoeken vond ik de tweede tip: om te maken herindexeren beschadigde tafel. Ik zag dit advies op veel plaatsen, maar het wekte geen vertrouwen. Laten we opnieuw indexeren:

reindex table ws_log_smevlog

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

herindexeren zonder problemen afgerond.

Dit hielp echter niet, VACUÜM VOL crashte met een soortgelijke fout. Omdat ik gewend ben aan mislukkingen, begon ik verder te zoeken naar advies op internet en kwam ik een nogal interessante tegen статью.

Poging 3: SELECTEREN, LIMIET, OFFSET

In het bovenstaande artikel werd voorgesteld de tabel rij voor rij te bekijken en problematische gegevens te verwijderen. Eerst moesten we naar alle lijnen kijken:

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 mijn geval bevatte de tabel 1 628 991 lijnen! Er moest goed voor gezorgd worden gegevenspartitionering, maar dit is een onderwerp voor een aparte discussie. Het was zaterdag, ik voerde dit commando uit in tmux en ging naar bed:

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

Tegen de ochtend besloot ik te kijken hoe het ging. Tot mijn verbazing ontdekte ik dat na 20 uur nog maar 2% van de gegevens was gescand! Ik wilde niet 50 dagen wachten. Opnieuw een complete mislukking.

Maar ik gaf niet op. Ik vroeg me af waarom het scannen zo lang duurde. Uit de documentatie (opnieuw op postgrespro) kwam ik erachter:

OFFSET geeft aan dat het opgegeven aantal rijen moet worden overgeslagen voordat met het uitvoeren van rijen wordt begonnen.
Als zowel OFFSET als LIMIT zijn opgegeven, slaat het systeem eerst de OFFSET-rijen over en begint vervolgens met het tellen van de rijen voor de LIMIT-beperking.

Wanneer u LIMIT gebruikt, is het belangrijk om ook een ORDER BY-clausule te gebruiken, zodat de resultaatrijen in een specifieke volgorde worden geretourneerd. Anders worden onvoorspelbare subsets van rijen geretourneerd.

Het is duidelijk dat het bovenstaande commando verkeerd was: ten eerste was er geen bestellen door, kan het resultaat onjuist zijn. Ten tweede moest Postgres eerst OFFSET-rijen scannen en overslaan, en met toenemende OFFSET De productiviteit zou nog verder afnemen.

Poging 4: neem een ​​dump in tekstvorm

Toen kwam er een ogenschijnlijk briljant idee bij me op: neem een ​​dump in tekstvorm en analyseer de laatst opgenomen regel.

Maar laten we eerst eens kijken naar de structuur van de tabel. ws_log_smevlog:

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

In ons geval hebben we een kolom "ID kaart", die de unieke identificatie (teller) van de rij bevatte. Het plan was als volgt:

  1. We beginnen een dump in tekstvorm te nemen (in de vorm van SQL-opdrachten)
  2. Op een bepaald moment werd de dump onderbroken vanwege een fout, maar het tekstbestand werd nog steeds op schijf opgeslagen
  3. We kijken naar het einde van het tekstbestand en vinden daarbij de identificatie (id) van de laatste regel die met succes is verwijderd

Ik begon een dump te nemen in tekstvorm:

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

De dump werd, zoals verwacht, onderbroken met dezelfde fout:

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

Verder door staart Ik keek naar het einde van de puinhoop (tail -5 ./mijn_dump.dump) ontdekte dat de dump werd onderbroken op de lijn met id 186 525. “Het probleem ligt dus in lijn met id 186 526, het is kapot en moet worden verwijderd!” - Ik dacht. Maar als u een query uitvoert op de database:
«selecteer * uit ws_log_smevlog waarbij id=186529"Het bleek dat alles in orde was met deze lijn... Rijen met indices 186 - 530 werkten ook zonder problemen. Een ander ‘briljant idee’ mislukte. Later begreep ik waarom dit gebeurde: bij het verwijderen en wijzigen van gegevens uit een tabel worden ze niet fysiek verwijderd, maar gemarkeerd als ‘dode tupels’, en dan komt autovacuüm en markeert deze regels als verwijderd en staat toe dat deze regels opnieuw worden gebruikt. Om te begrijpen: als de gegevens in de tabel veranderen en autovacuüm is ingeschakeld, worden deze niet opeenvolgend opgeslagen.

Poging 5: SELECTEER, VAN, WAAR id=

Mislukkingen maken ons sterker. Je moet nooit opgeven, je moet tot het einde gaan en in jezelf en je capaciteiten geloven. Daarom besloot ik een andere optie te proberen: gewoon één voor één door alle records in de database bladeren. Omdat we de structuur van mijn tabel kennen (zie hierboven), hebben we een id-veld dat uniek is (primaire sleutel). We hebben 1 rijen in de tabel en id zijn in volgorde, wat betekent dat we ze één voor één kunnen doornemen:

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

Als iemand het niet begrijpt, werkt het commando als volgt: het scant de tabel rij voor rij en stuurt stdout naar / Dev / null, maar als het SELECT-commando mislukt, wordt de fouttekst afgedrukt (stderr wordt naar de console verzonden) en wordt een regel met de fout afgedrukt (dankzij ||, wat betekent dat de select problemen had (de returncode van het commando is niet 0)).

Ik had geluk, ik had indexen gemaakt op het veld id:

Mijn eerste ervaring met het herstellen van een Postgres-database na een fout (ongeldige pagina in blok 4123007 van relatton base/16490)

Dit betekent dat het vinden van een regel met het gewenste ID niet veel tijd hoeft te kosten. In theorie zou het moeten werken. Laten we het commando invoeren tmux en laten we naar bed gaan.

Tegen de ochtend ontdekte ik dat ongeveer 90 inzendingen waren bekeken, wat iets meer dan 000% is. Een uitstekend resultaat vergeleken met de vorige methode (5%)! Maar ik wilde geen twintig dagen wachten...

Poging 6: SELECT, VAN, WAAR id >= en id

De klant beschikte over een uitstekende server speciaal voor de database: dual-processor Intel Xeon E5-2697 v2, waren er maar liefst 48 threads op onze locatie! De belasting op de server was gemiddeld; we konden zonder problemen ongeveer 20 threads downloaden. Ook was er voldoende RAM: maar liefst 384 gigabyte!

Daarom moest de opdracht worden geparallelliseerd:

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 was het mogelijk om een ​​mooi en elegant script te schrijven, maar ik koos voor de snelste parallellisatiemethode: het bereik 0-1628991 handmatig opsplitsen in intervallen van 100 records en afzonderlijk 000 opdrachten van het formulier uitvoeren:

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

Maar dat is niet alles. In theorie kost het verbinden met een database ook wat tijd en systeembronnen. Het aansluiten van 1 was niet erg slim, dat zult u met mij eens zijn. Laten we daarom 628 rijen ophalen in plaats van één op één verbinding. Als gevolg hiervan is het team getransformeerd in dit:

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

Open 16 vensters in een tmux-sessie en voer de opdrachten uit:

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

Een dag later ontving ik de eerste resultaten! Namelijk (de waarden XXX en ZZZ blijven niet langer behouden):

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

Dit betekent dat drie regels een fout bevatten. De ID's van de eerste en tweede probleemrecords lagen tussen 829 en 000, de ID's van de derde lagen tussen 830 en 000. Vervolgens moesten we eenvoudigweg de exacte ID-waarde van de probleemrecords vinden. Om dit te doen, doorzoeken we ons assortiment met problematische records met een stap van 146 en identificeren we de 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

gelukkig einde

We hebben de problematische lijnen gevonden. We gaan via psql de database in en proberen ze te verwijderen:

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

Tot mijn verbazing werden de vermeldingen zonder deze optie zonder problemen verwijderd nul_beschadigde_pagina's.

Vervolgens heb ik verbinding gemaakt met de database VACUÜM VOL (Ik denk dat het niet nodig was om dit te doen), en uiteindelijk heb ik met succes de back-up verwijderd met behulp van pg_dump. De dump is zonder fouten genomen! Het probleem is op zo'n stomme manier opgelost. De vreugde kende geen grenzen, na zoveel mislukkingen zijn we erin geslaagd een oplossing te vinden!

Dankbetuigingen en conclusie

Zo verliep mijn eerste ervaring met het herstellen van een echte Postgres-database. Ik zal deze ervaring nog lang herinneren.

En tot slot wil ik PostgresPro bedanken voor het vertalen van de documentatie naar het Russisch en voor volledig gratis online cursussen, wat veel heeft geholpen tijdens de analyse van het probleem.

Bron: www.habr.com

Voeg een reactie