Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

Želio bih s vama podijeliti svoje prvo uspješno iskustvo vraćanja Postgres baze podataka u punu funkcionalnost. Sa Postgres DBMS-om sam se upoznao prije pola godine, prije toga nisam imao nikakvog iskustva u administraciji baza podataka.

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

Radim kao polu-DevOps inženjer u velikoj IT kompaniji. Naša kompanija razvija softver za usluge visokog opterećenja, a ja sam odgovoran za performanse, održavanje i implementaciju. Dobio sam standardni zadatak: da ažuriram aplikaciju na jednom serveru. Aplikacija je napisana u Djangu, tokom ažuriranja se vrše migracije (promjene u strukturi baze podataka), a prije ovog procesa vršimo pun dump baze podataka kroz standardni pg_dump program, za svaki slučaj.

Došlo je do neočekivane greške prilikom snimanja dump-a (Postgres verzija 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

Greška "nevažeća stranica u bloku" govori o problemima na nivou sistema datoteka, što je veoma loše. Na raznim forumima je predloženo da se to uradi FULL VACUUM sa opcijom zero_damaged_pages da riješim ovaj problem. Pa hajde da probamo...

Priprema za oporavak

PAŽNJA! Obavezno napravite Postgres sigurnosnu kopiju prije bilo kakvog pokušaja vraćanja baze podataka. Ako imate virtuelnu mašinu, zaustavite bazu podataka i napravite snimak. Ako nije moguće napraviti snimak, zaustavite bazu podataka i kopirajte sadržaj Postgres direktorija (uključujući wal datoteke) na sigurno mjesto. Glavna stvar u našem poslu je da ne pogoršavamo stvari. Čitaj to.

Pošto je baza podataka generalno radila za mene, ograničio sam se na običan dump baze podataka, ali sam isključio tabelu sa oštećenim podacima (opcija -T, --exclude-table=TABLICA u pg_dump).

Server je bio fizički, bilo je nemoguće napraviti snimak. Sigurnosna kopija je uklonjena, idemo dalje.

Provjera sistema datoteka

Prije nego pokušamo vratiti bazu podataka, moramo se uvjeriti da je sve u redu sa samim sistemom datoteka. A u slučaju grešaka, ispravite ih, jer inače možete samo pogoršati stvari.

U mom slučaju, sistem datoteka sa bazom podataka je montiran "/srv" a tip je bio ext4.

Zaustavljanje baze podataka: systemctl stop [email zaštićen] i provjerite da li sistem datoteka niko ne koristi i da se može demontirati pomoću naredbe takođe:
lsof +D /srv

Takođe sam morao da zaustavim redis bazu podataka, pošto je i ona koristila "/srv". Zatim sam se odvezao / srv (umount).

Sistem datoteka je provjeren pomoću uslužnog programa e2fsck sa prekidačem -f (Prisilna provjera čak i ako je sistem datoteka označen kao čist):

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

Zatim, pomoću uslužnog programa dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep provjereno) možete provjeriti da je provjera zaista obavljena:

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

e2fsck kaže da nisu pronađeni problemi na nivou ext4 sistema datoteka, što znači da možete nastaviti s pokušajima vraćanja baze podataka, odnosno vratiti se na vakuum pun (naravno, morate ponovo montirati sistem datoteka i pokrenuti bazu podataka).

Ako imate fizički server, obavezno provjerite status diskova (preko smartctl -a /dev/XXX) ili RAID kontroler kako biste bili sigurni da problem nije na nivou hardvera. U mom slučaju se pokazalo da je RAID “hardver”, pa sam zamolio lokalnog administratora da provjeri status RAID-a (server je bio udaljen nekoliko stotina kilometara od mene). Rekao je da nije bilo grešaka, što znači da definitivno možemo početi sa restauracijom.

Pokušaj 1: zero_damaged_pages

Povezujemo se sa bazom podataka preko psql-a sa nalogom koji ima prava superkorisnika. Potreban nam je superkorisnik, jer... opcija zero_damaged_pages samo on može da se promeni. U mom slučaju to je postgres:

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

Opcija zero_damaged_pages potrebno kako bi se ignorirale greške čitanja (sa postgrespro web stranice):

Kada PostgreSQL otkrije oštećeno zaglavlje stranice, obično prijavljuje grešku i prekida trenutnu transakciju. Ako je zero_damaged_pages omogućeno, sistem umjesto toga izdaje upozorenje, poništava oštećenu stranicu u memoriji i nastavlja obradu. Ovo ponašanje uništava podatke, odnosno sve redove na oštećenoj stranici.

Omogućavamo opciju i pokušavamo napraviti potpuni vakuum tablica:

VACUUM FULL VERBOSE

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)
Nažalost, loša sreća.

Naišli smo na sličnu grešku:

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 – mehanizam za pohranjivanje “dugih podataka” u Poetgresu ako ne stane na jednu stranicu (8kb po defaultu).

Pokušaj 2: ponovno indeksiranje

Prvi savjet od Googlea nije pomogao. Nakon nekoliko minuta traženja, pronašao sam drugi savjet - napraviti reindex oštećen sto. Vidio sam ovaj savjet na mnogim mjestima, ali nije ulivao povjerenje. Ponovo indeksirajmo:

reindex table ws_log_smevlog

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

reindex završen bez problema.

Međutim, to nije pomoglo, VACUUM FULL pao sa sličnom greškom. Pošto sam navikao na neuspjehe, počeo sam tražiti savjete na internetu i naišao na prilično zanimljiv članak.

Pokušaj 3: SELECT, LIMIT, OFFSET

Gornji članak je predložio da se tabela pogleda red po red i da se uklone problematični podaci. Prvo smo trebali pogledati sve linije:

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

U mom slučaju, tabela je sadržavala 1 628 991 linije! Trebalo se dobro brinuti particioniranje podataka, ali ovo je tema za posebnu raspravu. Bila je subota, pokrenuo sam ovu komandu u tmux-u i otišao u krevet:

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

Do jutra sam odlučio provjeriti kako stvari idu. Na moje iznenađenje, otkrio sam da je nakon 20 sati skenirano samo 2% podataka! Nisam htela da čekam 50 dana. Još jedan potpuni neuspjeh.

Ali nisam odustajao. Pitao sam se zašto skeniranje traje toliko dugo. Iz dokumentacije (opet na postgrespro) saznao sam:

OFFSET specificira preskakanje određenog broja redova prije početka ispisa redova.
Ako su navedeni i OFFSET i LIMIT, sistem prvo preskače OFFSET redove, a zatim počinje brojati redove za ograničenje LIMIT.

Kada koristite LIMIT, važno je koristiti i klauzulu ORDER BY tako da se redovi rezultata vraćaju određenim redoslijedom. U suprotnom će biti vraćeni nepredvidivi podskupovi redova.

Očigledno je gornja komanda bila pogrešna: prvo, nije bilo Poredak po, rezultat bi mogao biti pogrešan. Drugo, Postgres je prvo morao da skenira i preskoči OFFSET redove, i to sa povećanjem OFFSET produktivnost bi još više opala.

Pokušaj 4: napravite dump u tekstualnom obliku

Tada mi je pala na pamet naizgled briljantna ideja: uzeti deponiju u obliku teksta i analizirati posljednji snimljeni red.

Ali prvo, pogledajmo strukturu tabele. ws_log_smevlog:

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

U našem slučaju imamo kolonu "Id", koji je sadržavao jedinstveni identifikator (brojač) reda. Plan je bio ovakav:

  1. Počinjemo uzimati dump u tekstualnom obliku (u obliku sql naredbi)
  2. U određenom trenutku, dump bi bio prekinut zbog greške, ali bi tekstualna datoteka i dalje bila sačuvana na disku
  3. Gledamo na kraj tekstualne datoteke i tako pronalazimo identifikator (id) posljednjeg retka koji je uspješno uklonjen

Počeo sam da uzimam dump u tekstualnom obliku:

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

Dump je, očekivano, prekinut istom greškom:

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

Dalje kroz rep Pogledao sam na kraj deponije (rep -5 ./my_dump.dump) otkrio da je dump prekinut na liniji sa id 186 525. "Dakle, problem je u liniji sa ID-om 186 526, pokvaren je i treba ga izbrisati!" - Mislio sam. Ali, postavljanje upita bazi podataka:
«odaberite * iz ws_log_smevlog gdje je id=186529“Ispostavilo se da je sa ovom linijom sve u redu... Redovi sa indeksima 186 - 530 također su radili bez problema. Još jedna “briljantna ideja” je propala. Kasnije sam shvatio zašto se to dogodilo: prilikom brisanja i promjene podataka iz tabele, oni se fizički ne brišu, već su označeni kao "mrtve tuple", zatim dolazi autovacuum i označava ove redove kao izbrisane i dozvoljava ponovnu upotrebu ovih redova. Da biste razumjeli, ako se podaci u tabeli promijene i automatski vakuum je omogućen, onda se ne pohranjuju sekvencijalno.

Pokušaj 5: SELECT, FROM, WHERE id=

Neuspjesi nas čine jačima. Nikada ne treba odustati, treba ići do kraja i vjerovati u sebe i svoje mogućnosti. Stoga sam odlučio isprobati drugu opciju: samo pregledajte sve zapise u bazi podataka jedan po jedan. Poznavajući strukturu moje tabele (vidi gore), imamo id polje koje je jedinstveno (primarni ključ). Imamo 1 red u tabeli i id su u redu, što znači da ih možemo proći jedan po jedan:

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

Ako neko ne razumije, naredba radi na sljedeći način: skenira tablicu red po red i šalje stdout na / dev / null, ali ako naredba SELECT ne uspije, tada se ispisuje tekst greške (stderr se šalje na konzolu) i ispisuje se red koji sadrži grešku (zahvaljujući ||, što znači da je odabir imao problema (povratni kod naredbe nije 0)).

Imao sam sreće, napravio sam indekse na terenu id:

Moje prvo iskustvo oporavljanja Postgres baze podataka nakon greške (nevažeća stranica u bloku 4123007 relatton base/16490)

To znači da pronalaženje linije sa željenim ID-om ne bi trebalo puno vremena. U teoriji bi trebalo da funkcioniše. Pa, pustimo komandu tmux i idemo u krevet.

Do jutra sam otkrio da je pogledano oko 90 unosa, što je nešto više od 000%. Odličan rezultat u poređenju sa prethodnom metodom (5%)! Ali nisam htela da čekam 2 dana...

Pokušaj 6: SELECT, FROM, WHERE id >= i id <

Kupac je imao odličan server posvećen bazi podataka: dvostruki procesor Intel Xeon E5-2697 v2, bilo je čak 48 tema na našoj lokaciji! Opterećenje servera je bilo prosječno, bez problema smo mogli preuzeti oko 20 niti. Bilo je i dovoljno RAM-a: čak 384 gigabajta!

Stoga je naredbu trebalo paralelizirati:

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

Ovdje je bilo moguće napisati lijepu i elegantnu skriptu, ali sam odabrao najbržu metodu paralelizacije: ručno podijeliti raspon 0-1628991 u intervale od 100 zapisa i zasebno pokrenuti 000 naredbi u obliku:

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

Ali to nije sve. U teoriji, povezivanje s bazom podataka također zahtijeva određeno vrijeme i sistemske resurse. Povezivanje 1 nije bilo baš pametno, složićete se. Stoga, dohvatimo 628 redova umjesto veze jedan na jedan. Kao rezultat, tim se transformirao u ovo:

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

Otvorite 16 prozora u tmux sesiji i pokrenite naredbe:

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

Dan kasnije dobila sam prve rezultate! Naime (vrijednosti XXX i ZZZ više nisu sačuvane):

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

To znači da tri reda sadrže grešku. ID-ovi prvog i drugog zapisa problema bili su između 829 000 i 830 000, id-ovi trećeg između 146 000 i 147 000. Zatim smo jednostavno morali pronaći tačnu vrijednost ID-a zapisa problema. Da bismo to učinili, pregledavamo naš raspon s problematičnim zapisima s korakom od 1 i identificiramo 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

Sretan kraj

Pronašli smo problematične linije. Ulazimo u bazu podataka preko psql-a i pokušavamo ih izbrisati:

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

Na moje iznenađenje, unosi su izbrisani bez problema čak i bez opcije zero_damaged_pages.

Onda sam se povezao sa bazom podataka, jesam VACUUM FULL (Mislim da to nije bilo potrebno raditi), i na kraju sam uspješno uklonio rezervnu kopiju koristeći pg_dump. Dump je preuzet bez ikakvih grešaka! Problem je riješen na tako glup način. Radosti nije bilo granica, nakon toliko neuspjeha uspjeli smo pronaći rješenje!

Priznanja i zaključak

Ovako se pokazalo moje prvo iskustvo obnavljanja prave Postgres baze podataka. Ovo iskustvo ću dugo pamtiti.

I na kraju, želeo bih da se zahvalim PostgresPro na prevodu dokumentacije na ruski i na potpuno besplatni online kursevi, što je mnogo pomoglo prilikom analize problema.

izvor: www.habr.com

Dodajte komentar