Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

Z vami bi rad delil svojo prvo uspešno izkušnjo obnovitve baze podatkov Postgres na polno funkcionalnost. S DBMS Postgres sem se spoznal pred pol leta, pred tem pa nisem imel nobenih izkušenj z administracijo podatkovnih baz.

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

Delam kot delno inženir DevOps v velikem IT podjetju. Naše podjetje razvija programsko opremo za visoko obremenjene storitve, jaz pa sem odgovoren za delovanje, vzdrževanje in uvajanje. Dobil sem standardno nalogo: posodobiti aplikacijo na enem strežniku. Aplikacija je napisana v Djangu, med posodabljanjem se izvajajo migracije (spremembe v strukturi baze), pred tem procesom pa za vsak slučaj naredimo celoten dump baze preko standardnega programa pg_dump.

Med ustvarjanjem izpisa (Postgres različica 9.5) je prišlo do nepričakovane napake:

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

Napaka "neveljavna stran v bloku" govori o težavah na ravni datotečnega sistema, kar je zelo slabo. Na različnih forumih je bilo predlagano, da to storite POLNI VAKUUM z možnostjo zero_damaged_pages rešiti to težavo. No, poskusimo ...

Priprave na okrevanje

POZOR Ne pozabite narediti varnostne kopije Postgres, preden poskusite obnoviti bazo podatkov. Če imate virtualni stroj, zaustavite bazo podatkov in naredite posnetek. Če posnetka ni mogoče narediti, zaustavite bazo podatkov in kopirajte vsebino imenika Postgres (vključno z datotekami wal) na varno mesto. Glavna stvar v našem poslu je, da stvari ne poslabšamo. Preberi это.

Ker mi je baza na splošno delovala, sem se omejil na običajen izpis baze podatkov, vendar sem izključil tabelo s poškodovanimi podatki (možnost -T, --exclude-table=TABELE v pg_dump).

Strežnik je bil fizičen, ni bilo mogoče narediti posnetka. Varnostna kopija je bila odstranjena, gremo naprej.

Preverjanje datotečnega sistema

Pred poskusom obnovitve baze podatkov se moramo prepričati, da je s samim datotečnim sistemom vse v redu. In v primeru napak jih popravite, saj drugače lahko le poslabšate stvari.

V mojem primeru je bil nameščen datotečni sistem z bazo podatkov "/srv" in tip je bil ext4.

Zaustavitev baze podatkov: sistemska zaustavitev [e-pošta zaščitena] in preverite, ali datotečnega sistema nihče ne uporablja in ga je mogoče odklopiti z ukazom tudi:
lsof +D /srv

Prav tako sem moral ustaviti bazo podatkov redis, saj je tudi uporabljala "/srv". Nato sem odmontiral / srv (umount).

Datotečni sistem je bil preverjen s pripomočkom e2fsck s stikalom -f (Vsili preverjanje, tudi če je datotečni sistem označen kot čist):

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

Nato z uporabo pripomočka dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep preverjeno) lahko preverite, ali je bil pregled dejansko opravljen:

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

e2fsck pravi, da na ravni datotečnega sistema ext4 ni bilo najdenih nobenih težav, kar pomeni, da lahko nadaljujete s poskusi obnovitve podatkovne baze oziroma se vrnete na vakuum poln (seveda morate znova namestiti datotečni sistem in zagnati bazo podatkov).

Če imate fizični strežnik, obvezno preverite stanje diskov (prek smartctl -a /dev/XXX) ali krmilnik RAID, da se prepričate, da težava ni na ravni strojne opreme. V mojem primeru se je izkazalo, da je RAID "strojni", zato sem prosil lokalnega administratorja, da preveri stanje RAID-a (strežnik je bil nekaj sto kilometrov stran od mene). Rekel je, da napak ni, kar pomeni, da lahko zagotovo začnemo z obnovo.

Poskus 1: zero_damaged_pages

Na bazo se povežemo preko psql z računom, ki ima pravice superuporabnika. Potrebujemo superuporabnika, ker... možnost zero_damaged_pages samo on se lahko spremeni. V mojem primeru je to postgres:

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

Možnost zero_damaged_pages potrebno za ignoriranje napak pri branju (s spletne strani postgrespro):

Ko PostgreSQL zazna poškodovano glavo strani, običajno sporoči napako in prekine trenutno transakcijo. Če je zero_damaged_pages omogočen, sistem namesto tega izda opozorilo, izbriše poškodovano stran v pomnilniku in nadaljuje z obdelavo. To vedenje uniči podatke, in sicer vse vrstice na poškodovani strani.

Omogočimo možnost in poskusimo narediti popoln vakuum tabel:

VACUUM FULL VERBOSE

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)
Na žalost smola.

Naleteli smo na podobno napako:

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 – mehanizem za shranjevanje "dolgih podatkov" v Poetgres, če ne ustrezajo eni strani (privzeto 8 kb).

2. poskus: ponovno indeksiranje

Prvi Googlov nasvet ni pomagal. Po nekaj minutah iskanja sem našla drugi nasvet - narediti reindeks poškodovana miza. Ta nasvet sem videl marsikje, a ni vzbujal zaupanja. Ponovno indeksirajmo:

reindex table ws_log_smevlog

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

reindeks zaključeno brez težav.

Vendar to ni pomagalo, VAKUUM POLN zrušil s podobno napako. Ker sem vajen neuspehov, sem začel še naprej iskati nasvete na internetu in naletel na precej zanimivega статью.

3. poskus: SELECT, LIMIT, OFFSET

Zgornji članek je predlagal ogled tabele vrstico za vrstico in odstranitev problematičnih podatkov. Najprej smo morali pogledati vse vrstice:

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

V mojem primeru je tabela vsebovala 1 628 991 črte! Treba je bilo dobro poskrbeti particioniranje podatkov, vendar je to tema za ločeno razpravo. Bila je sobota, zagnal sem ta ukaz v tmuxu in šel spat:

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

Zjutraj sem se odločil preveriti, kako stvari potekajo. Na svoje presenečenje sem ugotovil, da je bilo po 20 urah skeniranih le 2% podatkov! Nisem hotel čakati 50 dni. Še en popoln polom.

Ampak nisem odnehal. Spraševal sem se, zakaj je skeniranje trajalo tako dolgo. Iz dokumentacije (spet na postgrespro) sem ugotovil:

OFFSET podaja preskok določenega števila vrstic, preden začnete izpisovati vrstice.
Če sta podana tako OFFSET kot LIMIT, sistem najprej preskoči vrstice OFFSET in nato začne šteti vrstice za omejitev LIMIT.

Ko uporabljate LIMIT, je pomembno, da uporabite tudi klavzulo ORDER BY, tako da so vrstice rezultatov vrnjene v določenem vrstnem redu. V nasprotnem primeru bodo vrnjene nepredvidljive podmnožice vrstic.

Očitno je bil zgornji ukaz napačen: prvič, ni bilo naročite po, je lahko rezultat napačen. Drugič, Postgres je moral najprej skenirati in preskočiti vrstice OFFSET ter z naraščanjem OFFSET produktivnost bi se še bolj zmanjšala.

4. poskus: naredite izpis v besedilni obliki

Nato mi je na misel prišla navidezno briljantna ideja: narediti odlagališče v besedilni obliki in analizirati zadnjo posneto vrstico.

Toda najprej si poglejmo strukturo tabele. ws_log_smevlog:

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

V našem primeru imamo stolpec "Id", ki je vseboval enolični identifikator (števec) vrstice. Načrt je bil tak:

  1. Začnemo izvajati odlagališče v besedilni obliki (v obliki ukazov sql)
  2. V določenem trenutku bi bil izpis prekinjen zaradi napake, vendar bi bila besedilna datoteka še vedno shranjena na disku
  3. Pogledamo konec besedilne datoteke in tako najdemo identifikator (id) zadnje vrstice, ki je bila uspešno odstranjena

Začel sem jemati odlagališče v besedilni obliki:

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

Izpis je bil, kot je bilo pričakovano, prekinjen z isto napako:

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

Naprej skozi rep Pogledal sem na konec odlagališča (rep -5 ./my_dump.dump) odkril, da je bil izpis prekinjen na liniji z id 186 525. "Težava je torej v vrstici z ID-jem 186 526, pokvarjena je in jo je treba izbrisati!" - Mislil sem. Toda izvedba poizvedbe v bazi podatkov:
«izberite * iz ws_log_smevlog, kjer je id=186529»Izkazalo se je, da je s to vrstico vse v redu ... Tudi vrstice z indeksi 186 - 530 so delovale brez težav. Še ena "briljantna ideja" ni uspela. Kasneje sem razumel, zakaj se je to zgodilo: pri brisanju in spreminjanju podatkov iz tabele se ti fizično ne izbrišejo, ampak so označeni kot »mrtve tuple«, nato pride avtovakuum in te vrstice označi kot izbrisane ter omogoči ponovno uporabo teh vrstic. Za razumevanje, če se podatki v tabeli spremenijo in je omogočeno samodejno vakuumiranje, potem niso shranjeni zaporedno.

5. poskus: SELECT, FROM, WHERE id=

Neuspehi nas naredijo močnejše. Nikoli ne smeš obupati, treba je iti do konca in verjeti vase in v svoje zmožnosti. Zato sem se odločil poskusiti drugo možnost: samo enega za drugim preglejte vse zapise v bazi podatkov. Ker poznam strukturo moje tabele (glej zgoraj), imamo polje id, ki je edinstveno (primarni ključ). V tabeli imamo 1 vrstic in id so v redu, kar pomeni, da jih lahko pregledamo enega za drugim:

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

Če kdo ne razume, ukaz deluje takole: pregleda tabelo vrstico za vrstico in pošlje stdout na / dev / null, če pa ukaz SELECT ne uspe, se natisne besedilo napake (stderr se pošlje konzoli) in natisne se vrstica z napako (zahvaljujoč ||, kar pomeni, da je imela izbira težave (povratna koda ukaza ni 0)).

Imel sem srečo, indekse sem imel ustvarjene na terenu id:

Moja prva izkušnja z obnovitvijo baze podatkov Postgres po napaki (neveljavna stran v bloku 4123007 relatton base/16490)

To pomeni, da iskanje vrstice z želenim ID-jem ne bi smelo trajati veliko časa. V teoriji bi moralo delovati. No, zaženimo ukaz tmux in gremo spat.

Do jutra sem ugotovil, da je bilo ogledanih približno 90 vnosov, kar je nekaj več kot 000%. Odličen rezultat v primerjavi s prejšnjo metodo (5%)! Ampak nisem hotel čakati 2 dni ...

6. poskus: SELECT, FROM, WHERE id >= in id <

Stranka je imela odličen strežnik, namenjen bazi podatkov: dvoprocesorski Intel Xeon E5-2697 v2, na naši lokaciji je bilo kar 48 niti! Strežnik je bil povprečno obremenjen, brez težav smo prenesli okoli 20 niti. Tudi RAM-a je bilo dovolj: kar 384 gigabajtov!

Zato je bilo treba ukaz vzporediti:

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

Tu je bilo mogoče napisati lep in eleganten skript, vendar sem izbral najhitrejšo metodo paralelizacije: ročno razdelil obseg 0-1628991 v intervale po 100 zapisov in ločeno zagnal 000 ukazov obrazca:

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

A to še ni vse. V teoriji tudi povezovanje z bazo podatkov zahteva nekaj časa in sistemskih virov. Povezovanje 1 ni bilo preveč pametno, se strinjate. Zato pridobimo 628 vrstic namesto povezave ena na ena. Posledično se je ekipa preoblikovala v tole:

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

Odprite 16 oken v seji tmux in zaženite ukaze:

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 kasneje sem prejel prve rezultate! Namreč (vrednosti XXX in ZZZ niso več ohranjene):

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 pomeni, da tri vrstice vsebujejo napako. ID-ji prvega in drugega zapisa težave so bili med 829 000 in 830 000, ID-ji tretjega med 146 000 in 147 000. Nato smo preprosto morali poiskati natančno vrednost id-ja zapisov težave. Da bi to naredili, pregledamo naš obseg s problematičnimi zapisi s korakom 1 in 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

Vesel konec

Našli smo problematične vrstice. Prek psql gremo v bazo podatkov in jih poskušamo 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 presenečenje so bili vnosi brez težav izbrisani tudi brez možnosti zero_damaged_pages.

Potem sem se povezal z bazo podatkov VAKUUM POLN (Mislim, da tega ni bilo potrebno narediti) in končno sem uspešno odstranil varnostno kopijo z uporabo pg_dump. Odlagališče je bilo posneto brez napak! Problem je bil rešen na tako neumen način. Veselju ni bilo meja, po toliko neuspehih nam je uspelo najti rešitev!

Zahvala in zaključek

Tako se je izkazala moja prva izkušnja obnavljanja prave podatkovne baze Postgres. To izkušnjo si bom zapomnil še dolgo.

In na koncu bi se rad zahvalil podjetju PostgresPro za prevod dokumentacije v ruščino in za popolnoma brezplačne spletne tečaje, kar je zelo pomagalo pri analizi problema.

Vir: www.habr.com

Dodaj komentar