Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

Jeg vil gerne dele med dig min første succesfulde oplevelse med at gendanne en Postgres-database til fuld funktionalitet. Jeg stiftede bekendtskab med Postgres DBMS for et halvt år siden, før det havde jeg slet ingen erfaring med databaseadministration.

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

Jeg arbejder som semi-DevOps-ingeniør i en stor IT-virksomhed. Vores virksomhed udvikler software til højbelastningstjenester, og jeg er ansvarlig for ydeevne, vedligeholdelse og implementering. Jeg fik en standardopgave: at opdatere en applikation på én server. Applikationen er skrevet i Django, under opdateringen udføres migreringer (ændringer i databasestrukturen), og før denne proces tager vi et komplet databasedump gennem standardprogrammet pg_dump, for en sikkerheds skyld.

Der opstod en uventet fejl under optagelse af et dump (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

Bug "ugyldig side i blok" taler om problemer på filsystemniveau, hvilket er meget slemt. På forskellige fora blev det foreslået at gøre Fuldt vakuum med mulighed nul_skadede_sider at løse dette problem. Nå, lad os prøve...

Forberedelse til bedring

ADVARSEL! Sørg for at tage en Postgres backup før ethvert forsøg på at gendanne din database. Hvis du har en virtuel maskine, skal du stoppe databasen og tage et øjebliksbillede. Hvis det ikke er muligt at tage et snapshot, skal du stoppe databasen og kopiere indholdet af Postgres-mappen (inklusive wal-filer) til et sikkert sted. Det vigtigste i vores forretning er ikke at gøre tingene værre. Læs dette.

Da databasen generelt fungerede for mig, begrænsede jeg mig til et almindeligt databasedump, men ekskluderede tabellen med beskadigede data (mulighed -T, --exclude-table=TABEL i pg_dump).

Serveren var fysisk, det var umuligt at tage et øjebliksbillede. Sikkerhedskopien er blevet fjernet, lad os gå videre.

Kontrol af filsystem

Før vi forsøger at gendanne databasen, skal vi sikre os, at alt er i orden med selve filsystemet. Og i tilfælde af fejl, ret dem, for ellers kan du kun gøre tingene værre.

I mit tilfælde var filsystemet med databasen monteret i "/srv" og typen var ext4.

Stop af databasen: systemctl stop [e-mail beskyttet] og kontroller, at filsystemet ikke er i brug af nogen og kan afmonteres ved hjælp af kommandoen lsof:
lsof +D /srv

Jeg var også nødt til at stoppe redis-databasen, da den også brugte "/srv". Dernæst afmonterede jeg / srv (umount).

Filsystemet blev kontrolleret ved hjælp af hjælpeprogrammet e2fsck med kontakten -f (Tving kontrol, selvom filsystemet er markeret som rent):

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

Dernæst ved hjælp af værktøjet dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep kontrolleret) kan du bekræfte, at kontrollen faktisk blev udført:

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

e2fsck siger, at der ikke blev fundet problemer på ext4 filsystemniveau, hvilket betyder, at du kan fortsætte med at forsøge at gendanne databasen, eller rettere vende tilbage til vakuum fuld (Selvfølgelig skal du montere filsystemet tilbage og starte databasen).

Hvis du har en fysisk server, skal du sørge for at kontrollere status på diskene (via smartctl -a /dev/XXX) eller RAID-controller for at sikre, at problemet ikke er på hardwareniveau. I mit tilfælde viste RAID'en sig at være "hardware", så jeg bad den lokale administrator om at tjekke status for RAID'en (serveren var flere hundrede kilometer væk fra mig). Han sagde, at der ikke var nogen fejl, hvilket betyder, at vi helt sikkert kan begynde restaureringen.

Forsøg 1: nul_skadede_sider

Vi forbinder til databasen via psql med en konto, der har superbrugerrettigheder. Vi har brug for en superbruger, fordi... mulighed nul_skadede_sider kun han kan ændre sig. I mit tilfælde er det postgres:

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

valgmulighed nul_skadede_sider nødvendigt for at ignorere læsefejl (fra postgrespro-webstedet):

Når PostgreSQL registrerer en korrupt sidehoved, rapporterer den typisk en fejl og afbryder den aktuelle transaktion. Hvis zero_damaged_pages er aktiveret, udsender systemet i stedet en advarsel, nulstiller den beskadigede side i hukommelsen og fortsætter behandlingen. Denne adfærd ødelægger data, nemlig alle rækker på den beskadigede side.

Vi aktiverer muligheden og forsøger at lave et fuldt vakuum af bordene:

VACUUM FULL VERBOSE

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)
Desværre uheld.

Vi stødte på en lignende fejl:

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 – en mekanisme til lagring af "lange data" i Poetgres, hvis det ikke passer på én side (8kb som standard).

Forsøg 2: genindeks

Det første råd fra Google hjalp ikke. Efter et par minutters søgen fandt jeg det andet tip – at lave genindeksere beskadiget bord. Jeg så dette råd mange steder, men det vækkede ikke tillid. Lad os genindeksere:

reindex table ws_log_smevlog

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

genindeksere gennemført uden problemer.

Dette hjalp dog ikke, VAKUUM FULD styrtede ned med en lignende fejl. Da jeg er vant til fiaskoer, begyndte jeg at lede længere efter råd på internettet og stødte på en ret interessant en artikel.

Forsøg 3: SELECT, LIMIT, OFFSET

Artiklen ovenfor foreslog at se på tabellen række for række og fjerne problematiske data. Først skulle vi se på alle linjerne:

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

I mit tilfælde indeholdt bordet 1 628 991 linjer! Det var nødvendigt at passe godt på dataopdeling, men dette er et emne for en separat diskussion. Det var lørdag, jeg kørte denne kommando i tmux og gik i seng:

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

Om morgenen besluttede jeg at tjekke, hvordan det gik. Til min overraskelse opdagede jeg, at efter 20 timer var kun 2% af dataene blevet scannet! Jeg ville ikke vente 50 dage. Endnu en fuldstændig fiasko.

Men jeg gav ikke op. Jeg spekulerede på, hvorfor scanningen tog så lang tid. Fra dokumentationen (igen på postgrespro) fandt jeg ud af:

OFFSET angiver at springe det angivne antal rækker over, før du begynder at udskrive rækker.
Hvis både OFFSET og LIMIT er angivet, springer systemet først OFFSET-rækkerne over og begynder derefter at tælle rækkerne for LIMIT-begrænsningen.

Når du bruger LIMIT, er det vigtigt også at bruge en ORDER BY-klausul, så resultatrækkerne returneres i en bestemt rækkefølge. Ellers vil uforudsigelige undersæt af rækker blive returneret.

Det er klart, at ovenstående kommando var forkert: for det første var der nej bestil af, kan resultatet være forkert. For det andet skulle Postgres først scanne og springe OFFSET rækker over, og med stigende OFFSET produktiviteten ville falde yderligere.

Forsøg 4: dump i tekstform

Så kom en tilsyneladende genial idé op i mit sind: Tag et dump i tekstform og analyser den sidst indspillede linje.

Men lad os først tage et kig på bordets struktur. ws_log_smevlog:

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

I vores tilfælde har vi en kolonne "Id", som indeholdt den unikke identifikator (tæller) for rækken. Planen var sådan her:

  1. Vi begynder at tage et dump i tekstform (i form af sql-kommandoer)
  2. På et bestemt tidspunkt ville dumpet blive afbrudt på grund af en fejl, men tekstfilen ville stadig blive gemt på disken
  3. Vi ser i slutningen af ​​tekstfilen, derved finder vi identifikatoren (id) for den sidste linje, der blev fjernet med succes

Jeg begyndte at tage et dump i tekstform:

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

Dumpet blev som forventet afbrudt med samme fejl:

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

Længere igennem hale Jeg så på enden af ​​lossepladsen (hale -5 ./min_dump.dump) opdagede, at dumpen blev afbrudt på linjen med id 186 525. "Så problemet er på linje med id 186 526, det er ødelagt og skal slettes!" - Jeg troede. Men lav en forespørgsel til databasen:
«vælg * fra ws_log_smevlog hvor id=186529"Det viste sig, at alt var i orden med denne linje... Rækker med indeks 186 - 530 fungerede også uden problemer. Endnu en "genial idé" mislykkedes. Senere forstod jeg hvorfor det skete: når man sletter og ændrer data fra en tabel, slettes de ikke fysisk, men markeres som "døde tuples", så kommer autovakuum og markerer disse linjer som slettede og gør det muligt at genbruge disse linjer. For at forstå, hvis dataene i tabellen ændres, og autovakuum er aktiveret, gemmes de ikke sekventielt.

Forsøg 5: SELECT, FROM, WHERE id=

Fejl gør os stærkere. Du bør aldrig give op, du skal gå til slutningen og tro på dig selv og dine evner. Så jeg besluttede at prøve en anden mulighed: bare se alle posterne i databasen én efter én. Ved at kende strukturen af ​​min tabel (se ovenfor), har vi et id-felt, som er unikt (primær nøgle). Vi har 1 rækker i tabellen og id er i orden, hvilket betyder, at vi bare kan gennemgå dem én efter én:

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

Hvis nogen ikke forstår, fungerer kommandoen som følger: den scanner tabellen række for række og sender stdout til / Dev / null, men hvis SELECT-kommandoen fejler, udskrives fejlteksten (stderr sendes til konsollen) og en linje indeholdende fejlen udskrives (takket være ||, hvilket betyder, at select havde problemer (kommandoens returkode) er ikke 0)).

Jeg var heldig, jeg fik oprettet indekser på banen id:

Min første oplevelse med at gendanne en Postgres-database efter en fejl (ugyldig side i blok 4123007 af relatton base/16490)

Det betyder, at det ikke bør tage meget tid at finde en linje med det ønskede id. I teorien burde det virke. Nå, lad os køre kommandoen ind tmux og lad os gå i seng.

Om morgenen fandt jeg ud af, at omkring 90 bidrag var blevet set, hvilket er lidt over 000%. Et fremragende resultat sammenlignet med den tidligere metode (5%)! Men jeg ville ikke vente 2 dage...

Forsøg 6: SELECT, FROM, WHERE id >= og id <

Kunden havde en fremragende server dedikeret til databasen: dual-processor Intel Xeon E5-2697 v2, der var så mange som 48 tråde på vores lokation! Belastningen på serveren var gennemsnitlig; vi kunne downloade omkring 20 tråde uden problemer. Der var også nok RAM: så meget som 384 gigabyte!

Derfor skulle kommandoen paralleliseres:

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

Her var det muligt at skrive et smukt og elegant script, men jeg valgte den hurtigste paralleliseringsmetode: opdel manuelt området 0-1628991 i intervaller på 100 poster og kør separat 000 kommandoer af formen:

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

Men det er ikke alt. I teorien tager det også lidt tid og systemressourcer at oprette forbindelse til en database. At forbinde 1 var ikke særlig smart, det er du enig i. Lad os derfor hente 628 rækker i stedet for én til én forbindelse. Som et resultat forvandlede holdet sig til dette:

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

Åbn 16 vinduer i en tmux-session og kør kommandoerne:

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

En dag senere modtog jeg de første resultater! Nemlig (værdierne XXX og ZZZ er ikke længere bevaret):

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

Det betyder, at tre linjer indeholder en fejl. Id'erne for den første og anden problempost var mellem 829 og 000, id'erne for den tredje var mellem 830 og 000. Dernæst skulle vi blot finde den nøjagtige id-værdi af problemposterne. For at gøre dette ser vi vores udvalg igennem med problematiske registreringer med et trin på 146 og identificerer id'et:

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

lykkelig slutning

Vi fandt de problematiske linjer. Vi går ind i databasen via psql og prøver at slette dem:

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

Til min overraskelse blev posterne slettet uden problemer selv uden muligheden nul_skadede_sider.

Så oprettede jeg forbindelse til databasen VAKUUM FULD (Jeg tror, ​​det ikke var nødvendigt at gøre dette), og til sidst fjernede jeg sikkerhedskopien vha pg_dump. Losset blev taget uden fejl! Problemet blev løst på sådan en dum måde. Glæden kendte ingen grænser, efter så mange fejl lykkedes det at finde en løsning!

Anerkendelser og konklusion

Sådan blev min første oplevelse med at gendanne en rigtig Postgres-database. Jeg vil huske denne oplevelse i lang tid.

Og til sidst vil jeg gerne sige tak til PostgresPro for at oversætte dokumentationen til russisk og for helt gratis online kurser, hvilket hjalp meget under analysen af ​​problemet.

Kilde: www.habr.com

Tilføj en kommentar