Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

Jeg vil gjerne dele med deg min første vellykkede erfaring med å gjenopprette en Postgres-database til full funksjonalitet. Jeg ble kjent med Postgres DBMS for et halvt år siden, før det hadde jeg ingen erfaring med databaseadministrasjon i det hele tatt.

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

Jeg jobber som semi-DevOps-ingeniør i et stort IT-selskap. Vårt firma utvikler programvare for høybelastningstjenester, og jeg er ansvarlig for ytelse, vedlikehold og distribusjon. Jeg fikk en standardoppgave: å oppdatere en applikasjon på én server. Applikasjonen er skrevet i Django, under oppdateringen utføres migreringer (endringer i databasestrukturen), og før denne prosessen tar vi en full databasedump gjennom standardprogrammet pg_dump, for sikkerhets skyld.

Det oppstod en uventet feil under dumping (Postgres versjon 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 blokk" snakker om problemer på filsystemnivå, noe som er veldig ille. På ulike fora ble det foreslått å gjøre det FULLT VAKUUM med opsjon null_skadede_sider for å løse dette problemet. Vel, la oss prøve...

Forbereder for restitusjon

FORSIKTIG Sørg for å ta en Postgres-sikkerhetskopi før du forsøker å gjenopprette databasen. Hvis du har en virtuell maskin, stopp databasen og ta et øyeblikksbilde. Hvis det ikke er mulig å ta et øyeblikksbilde, stopp databasen og kopier innholdet i Postgres-katalogen (inkludert wal-filer) til et trygt sted. Hovedsaken i vår virksomhet er ikke å gjøre ting verre. Lese dette.

Siden databasen generelt fungerte for meg, begrenset jeg meg til en vanlig databasedump, men ekskluderte tabellen med skadede data (alternativ -T, --exclude-table=TABLE i pg_dump).

Serveren var fysisk, det var umulig å ta et øyeblikksbilde. Sikkerhetskopien er fjernet, la oss gå videre.

Kontroll av filsystem

Før vi prøver å gjenopprette databasen, må vi sørge for at alt er i orden med selve filsystemet. Og i tilfelle feil, rett dem, for ellers kan du bare gjøre ting verre.

I mitt tilfelle var filsystemet med databasen montert inn "/srv" og typen var ext4.

Stoppe databasen: systemctl stopp [e-postbeskyttet] og sjekk at filsystemet ikke er i bruk av noen og kan demonteres ved hjelp av kommandoen lsof:
lsof +D /srv

Jeg måtte også stoppe redis-databasen, siden den også brukte "/srv". Deretter avmonterte jeg / srv (umount).

Filsystemet ble sjekket ved hjelp av verktøyet e2fsck med bryteren -f (Tving kontroll selv om filsystemet er merket som rent):

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

Deretter bruker du verktøyet dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep sjekket) kan du bekrefte at kontrollen faktisk ble utført:

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

e2fsck sier at ingen problemer ble funnet på ext4 filsystemnivå, noe som betyr at du kan fortsette å prøve å gjenopprette databasen, eller snarere gå tilbake til vakuum full (selvfølgelig må du montere filsystemet tilbake og starte databasen).

Hvis du har en fysisk server, sørg for å sjekke statusen til diskene (via smartctl -a /dev/XXX) eller RAID-kontroller for å sikre at problemet ikke er på maskinvarenivå. I mitt tilfelle viste RAID seg å være "maskinvare", så jeg ba den lokale administratoren sjekke statusen til RAID (serveren var flere hundre kilometer unna meg). Han sa at det ikke var noen feil, noe som betyr at vi definitivt kan starte restaurering.

Forsøk 1: zero_damaged_pages

Vi kobler til databasen via psql med en konto som har superbrukerrettigheter. Vi trenger en superbruker, fordi... alternativ null_skadede_sider bare han kan forandre seg. I mitt tilfelle er det postgres:

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

alternativ null_skadede_sider nødvendig for å ignorere lesefeil (fra postgrespro-nettstedet):

Når PostgreSQL oppdager en korrupt sidehode, rapporterer den vanligvis en feil og avbryter den gjeldende transaksjonen. Hvis zero_damaged_pages er aktivert, sender systemet i stedet en advarsel, nuller ut den skadede siden i minnet og fortsetter behandlingen. Denne oppførselen ødelegger data, nemlig alle rader på den skadede siden.

Vi aktiverer alternativet og prøver å gjøre et fullt vakuum av bordene:

VACUUM FULL VERBOSE

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)
Uflaks dessverre.

Vi oppdaget en lignende feil:

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 for lagring av "lange data" i Poetgres hvis den ikke får plass på én side (8kb som standard).

Forsøk 2: reindeks

Det første rådet fra Google hjalp ikke. Etter noen minutters leting fant jeg det andre tipset – å lage reindeksere skadet bord. Jeg så dette rådet mange steder, men det vekket ikke tillit. La oss reindeksere:

reindex table ws_log_smevlog

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

reindeksere gjennomført uten problemer.

Dette hjalp imidlertid ikke, VAKUUM FULL krasjet med en lignende feil. Siden jeg er vant til feil, begynte jeg å lete videre etter råd på Internett og kom over en ganske interessant artikkel.

Forsøk 3: SELECT, LIMIT, OFFSET

Artikkelen ovenfor foreslo å se på tabellen rad for rad og fjerne problematiske data. Først måtte vi se på alle linjene:

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 mitt tilfelle inneholdt tabellen +1 628 991 XNUMX linjer! Det var nødvendig å ta godt vare på datapartisjonering, men dette er et tema for en egen diskusjon. Det var lørdag, jeg kjørte denne kommandoen i tmux og la meg:

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 bestemte jeg meg for å sjekke hvordan det gikk. Til min overraskelse oppdaget jeg at etter 20 timer var bare 2 % av dataene skannet! Jeg ville ikke vente 50 dager. Nok en fullstendig fiasko.

Men jeg ga ikke opp. Jeg lurte på hvorfor skanningen tok så lang tid. Fra dokumentasjonen (igjen på postgrespro) fant jeg ut:

OFFSET spesifiserer å hoppe over det angitte antallet rader før du begynner å skrive ut rader.
Hvis både OFFSET og LIMIT er spesifisert, hopper systemet først over OFFSET-radene og begynner deretter å telle radene for LIMIT-begrensningen.

Ved bruk av LIMIT er det viktig å også bruke en ORDER BY-klausul slik at resultatradene returneres i en bestemt rekkefølge. Ellers vil uforutsigbare undersett av rader bli returnert.

Åpenbart var kommandoen ovenfor feil: for det første var det nei rekkefølge etter, kan resultatet bli feil. For det andre måtte Postgres først skanne og hoppe over OFFSET-rader, og med økende OFFSET produktiviteten vil synke ytterligere.

Forsøk 4: ta en dump i tekstform

Så dukket det opp en tilsynelatende genial idé: ta en dump i tekstform og analyser den siste innspilte linjen.

Men først, la oss ta en titt på strukturen til tabellen. ws_log_smevlog:

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

I vårt tilfelle har vi en spalte "Id", som inneholdt den unike identifikatoren (telleren) for raden. Planen var slik:

  1. Vi begynner å ta en dump i tekstform (i form av sql-kommandoer)
  2. På et bestemt tidspunkt vil dumpen bli avbrutt på grunn av en feil, men tekstfilen vil fortsatt være lagret på disken
  3. Vi ser på slutten av tekstfilen, og finner dermed identifikatoren (id) til den siste linjen som ble fjernet.

Jeg begynte å dumpe i tekstform:

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

Dumpen ble som forventet avbrutt med samme feil:

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

Videre gjennom hale Jeg så på enden av fyllingen (hale -5 ./min_dump.dump) oppdaget at dumpen ble avbrutt på linjen med id 186. "Så problemet er på linje med id 186 526, det er ødelagt og må slettes!" - Jeg tenkte. Men, gjør en spørring til databasen:
«velg * fra ws_log_smevlog hvor id=186529«Det viste seg at alt var bra med denne linjen... Rader med indekser 186 - 530 fungerte også uten problemer. En annen "genial idé" mislyktes. Senere forsto jeg hvorfor dette skjedde: når du sletter og endrer data fra en tabell, slettes de ikke fysisk, men merkes som "døde tuples", så kommer autovakuum og merker disse linjene som slettet og lar disse linjene gjenbrukes. For å forstå, hvis dataene i tabellen endres og autovakuum er aktivert, blir de ikke lagret sekvensielt.

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

Svikt gjør oss sterkere. Du bør aldri gi opp, du må gå til slutten og tro på deg selv og dine evner. Så jeg bestemte meg for å prøve et annet alternativ: bare se gjennom alle postene i databasen én etter én. Når vi kjenner strukturen til tabellen min (se ovenfor), har vi et id-felt som er unikt (primærnøkkel). Vi har 1 628 991 rader i tabellen og id er i orden, noe som betyr at vi bare kan gå gjennom dem én etter é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 noen ikke forstår, fungerer kommandoen som følger: den skanner tabellen rad for rad og sender stdout til / Dev / null, men hvis SELECT-kommandoen mislykkes, skrives feilteksten ut (stderr sendes til konsollen) og en linje som inneholder feilen skrives ut (takket være ||, som betyr at select hadde problemer (returkoden til kommandoen) er ikke 0)).

Jeg var heldig, jeg fikk opprettet indekser på feltet id:

Min første erfaring med å gjenopprette en Postgres-database etter en feil (ugyldig side i blokk 4123007 av relatton base/16490)

Dette betyr at det ikke bør ta mye tid å finne en linje med ønsket id. I teorien burde det fungere. Vel, la oss kjøre kommandoen inn tmux og la oss legge oss.

Om morgenen fant jeg ut at rundt 90 000 oppføringer hadde blitt sett, som er litt over 5 %. Et utmerket resultat sammenlignet med forrige metode (2%)! Men jeg ville ikke vente 20 dager...

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

Kunden hadde en utmerket server dedikert til databasen: dobbel prosessor Intel Xeon E5-2697 v2, det var så mange som 48 tråder på stedet vårt! Belastningen på serveren var gjennomsnittlig; vi kunne laste ned rundt 20 tråder uten problemer. Det var også nok RAM: så mye som 384 gigabyte!

Derfor måtte kommandoen parallelliseres:

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 mulig å skrive et vakkert og elegant manus, men jeg valgte den raskeste parallelliseringsmetoden: del opp området 0-1628991 manuelt i intervaller på 100 000 poster og kjør separat 16 kommandoer av skjemaet:

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 tar det også litt tid og systemressurser å koble til en database. Å koble til 1 628 991 var ikke særlig smart, det er du enig i. La oss derfor hente 1000 rader i stedet for én på én tilkobling. Som et resultat forvandlet teamet seg 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

Åpne 16 vinduer i en tmux-økt og kjør kommandoene:

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 fikk jeg de første resultatene! Nemlig (verdiene XXX og ZZZ er ikke lenger bevart):

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

Dette betyr at tre linjer inneholder en feil. ID-ene til den første og andre problemposten var mellom 829 000 og 830 000, ID-ene til den tredje var mellom 146 000 og 147 000. Deretter måtte vi ganske enkelt finne den nøyaktige ID-verdien til problempostene. For å gjøre dette ser vi gjennom utvalget vårt med problematiske poster med et trinn på 1 og identifiserer IDen:

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 slutt

Vi fant de problematiske linjene. Vi går inn i databasen via psql og prøver å 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 ble oppføringene slettet uten problemer selv uten alternativet null_skadede_sider.

Så koblet jeg til databasen, gjorde det VAKUUM FULL (Jeg tror det ikke var nødvendig å gjøre dette), og til slutt fjernet jeg sikkerhetskopien ved hjelp av pg_dump. Fyllingen ble tatt uten feil! Problemet ble løst på en så dum måte. Gleden visste ingen grenser, etter så mange feil klarte vi å finne en løsning!

Anerkjennelser og konklusjon

Slik ble min første erfaring med å gjenopprette en ekte Postgres-database. Jeg vil huske denne opplevelsen i lang tid.

Og til slutt vil jeg takke PostgresPro for å oversette dokumentasjonen til russisk og for helt gratis nettkurs, som hjalp mye under analysen av problemet.

Kilde: www.habr.com

Legg til en kommentar