Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

Mi ŝatus dividi kun vi mian unuan sukcesan sperton pri restarigo de Postgres-datumbazo al plena funkcieco. Mi konatiĝis kun Postgres DBMS antaŭ duonjaro; antaŭ tio mi tute ne havis sperton pri administrado de datumbazoj.

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

Mi laboras kiel duon-DevOps-inĝeniero en granda IT-kompanio. Nia firmao disvolvas programaron por altŝarĝaj servoj, kaj mi respondecas pri agado, prizorgado kaj disfaldiĝo. Mi ricevis norman taskon: ĝisdatigi aplikaĵon sur unu servilo. La aplikaĵo estas skribita en Django, dum la ĝisdatigo efektiviĝas migradoj (ŝanĝoj en la datumbaza strukturo), kaj antaŭ ĉi tiu procezo ni prenas plenan datumbazan rubejon per la norma programo pg_dump, ĉiaokaze.

Neatendita eraro okazis dum forĵeto (Postgres versio 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

eraro "nevalida paĝo en bloko" parolas pri problemoj je la dosiersistemo-nivelo, kio estas tre malbona. En diversaj forumoj oni sugestis fari PLENA VAKUO kun opcio nul_difektitaj_paĝoj por solvi ĉi tiun problemon. Nu, ni provu...

Preparante por resaniĝo

ATENTO! Nepre prenu sekurkopion de Postgres antaŭ ajna provo restarigi vian datumbazon. Se vi havas virtualan maŝinon, haltigu la datumbazon kaj prenu momentfoton. Se ne eblas preni momentfoton, haltigu la datumbazon kaj kopiu la enhavon de la dosierujo Postgres (inkluzive de wal-dosieroj) al sekura loko. La ĉefa afero en nia komerco estas ne plimalbonigi aferojn. Legu ĝi.

Ĉar la datumbazo ĝenerale funkciis por mi, mi limigis min al regula datumbaza rubejo, sed ekskludis la tabelon kun difektitaj datumoj (opcio -T, --exclude-table=TABLO en pg_dump).

La servilo estis fizika, estis neeble preni momentfoton. La sekurkopio estas forigita, ni pluiru.

Kontrolo de dosiersistemo

Antaŭ ol provi restarigi la datumbazon, ni devas certigi, ke ĉio estas en ordo kun la dosiersistemo mem. Kaj en kazo de eraroj, korektu ilin, ĉar alie vi povas nur plimalbonigi la aferojn.

En mia kazo, la dosiersistemo kun la datumbazo estis muntita enen "/srv" kaj la tipo estis ext4.

Ĉesigi la datumbazon: systemctl halto [retpoŝte protektita] kaj kontrolu, ke la dosiersistemo ne estas uzata de iu ajn kaj povas esti malmuntita per la komando lsof:
lsof +D /srv

Mi ankaŭ devis haltigi la redis datumbazon, ĉar ĝi ankaŭ uzis "/srv". Poste mi malmuntis / srv (malaltigi).

La dosiersistemo estis kontrolita per la ilo e2fsck kun la ŝaltilo -f (Devigu kontrolon eĉ se dosiersistemo estas markita pura):

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

Poste, uzante la ilon dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep kontrolita) vi povas kontroli, ke la kontrolo efektive estis farita:

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

e2fsck diras, ke neniuj problemoj estis trovitaj ĉe la dosiersistemo ext4, kio signifas, ke vi povas daŭrigi provi restarigi la datumbazon, aŭ prefere reveni al vakuo plena (kompreneble, vi devas munti la dosiersistemon reen kaj komenci la datumbazon).

Se vi havas fizikan servilon, nepre kontrolu la staton de la diskoj (per smartctl -a /dev/XXX) aŭ RAID-regilo por certigi, ke la problemo ne estas sur la aparatara nivelo. En mia kazo, la RAID montriĝis "aparataro", do mi petis la lokan administranton kontroli la staton de la RAID (la servilo estis kelkcent kilometroj for de mi). Li diris, ke ne estis eraroj, kio signifas, ke ni certe povas komenci restarigon.

Provo 1: nul_difektitaj_paĝoj

Ni konektas al la datumbazo per psql kun konto, kiu havas superuzantrajtojn. Ni bezonas superuziston, ĉar... opcio nul_difektitaj_paĝoj nur li povas ŝanĝi. En mia kazo ĝi estas postgres:

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

Opcio nul_difektitaj_paĝoj necesas por ignori legajn erarojn (de la retejo de postgrespro):

Kiam PostgreSQL detektas koruptan paĝan kaplinion, ĝi kutime raportas eraron kaj ĉesigas la nunan transakcion. Se zero_damaged_pages estas ebligita, la sistemo anstataŭe eldonas averton, nuligas la difektitan paĝon en memoro, kaj daŭrigas prilaboradon. Ĉi tiu konduto detruas datumojn, nome ĉiujn vicojn en la difektita paĝo.

Ni ebligas la opcion kaj provas fari plenan vakuon de la tabloj:

VACUUM FULL VERBOSE

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)
Bedaŭrinde, malbona sorto.

Ni renkontis similan eraron:

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 – mekanismo por konservi “longajn datumojn” en Poetgres se ĝi ne taŭgas sur unu paĝo (defaŭlte 8kb).

Provo 2: reindikigi

La unua konsilo de Guglo ne helpis. Post kelkaj minutoj da serĉado, mi trovis la duan konsileton - fari reindikigi difektita tablo. Mi vidis ĉi tiun konsilon en multaj lokoj, sed ĝi ne inspiris konfidon. Ni reindeksu:

reindex table ws_log_smevlog

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

reindikigi kompletigita sen problemoj.

Tamen tio ne helpis, VAKUO PLENA kraŝis kun simila eraro. Ĉar mi kutimas al malsukcesoj, mi komencis serĉi pli por konsiloj en la Interreto kaj trovis sufiĉe interesan artikolo.

Provo 3: ELEKTU, LIMIGI, OFSET

La supra artikolo sugestis rigardi la tabelvicon post vico kaj forigi problemajn datumojn. Unue ni bezonis rigardi ĉiujn liniojn:

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

En mia kazo, la tablo enhavis 1 628 991 linioj! Necesis bone prizorgi datumdisigo, sed ĉi tio estas temo por aparta diskuto. Estis sabato, mi kuris ĉi tiun komandon en tmux kaj enlitiĝis:

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

Antaŭ la mateno mi decidis kontroli kiel la aferoj iras. Je mia surprizo, mi malkovris, ke post 20 horoj, nur 2% de la datumoj estis skanitaj! Mi ne volis atendi 50 tagojn. Alia kompleta fiasko.

Sed mi ne rezignis. Mi scivolis kial la skanado daŭris tiel longe. El la dokumentado (denove ĉe postgrespro) mi eksciis:

OFFSET specifas salti la specifitan nombron da vicoj antaŭ ol komenci eligi vicojn.
Se kaj OFFSET kaj LIMIT estas specifitaj, la sistemo unue preterlasas la OFFSET-vicojn kaj tiam komencas nombri la vicojn por la LIMIT-limo.

Kiam vi uzas LIMIT, gravas ankaŭ uzi klaŭzon ORDER BY por ke la rezultaj vicoj estu redonitaj en specifa ordo. Alie, neantaŭvideblaj subaroj de vicoj estos resenditaj.

Evidente, la supra ordono estis malĝusta: unue, ne estis ordigi per, la rezulto povus esti erara. Due, Postgres unue devis skani kaj salti OFFSET-vicojn, kaj kun pliiĝo OFFSET produktiveco malpliiĝus eĉ plu.

Provo 4: prenu rubejon en tekstformo

Tiam venis al mia menso ŝajne brila ideo: fari rubujon en tekstformo kaj analizi la lastan registritan linion.

Sed unue, ni rigardu la strukturon de la tabelo. ws_log_smevlog:

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

En nia kazo ni havas kolumnon "Identigilo", kiu enhavis la unikan identigilon (nombrilo) de la vico. La plano estis tia:

  1. Ni komencas fari rubejon en teksta formo (en formo de sql-komandoj)
  2. Je certa momento, la rubejo estus interrompita pro eraro, sed la tekstdosiero ankoraŭ estus konservita sur disko.
  3. Ni rigardas la finon de la tekstdosiero, tiel ni trovas la identigilon (id) de la lasta linio kiu estis forigita sukcese

Mi komencis fari rubejon en tekstformo:

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

La rubejo, kiel atendite, estis interrompita kun la sama eraro:

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

Plu tra vosto Mi rigardis la finon de la rubejo (vosto -5 ./my_dump.dump) malkovris ke la rubejo estis interrompita sur la linio kun id 186 525. "Do la problemo estas en la linio kun id 186 526, ĝi estas rompita, kaj devas esti forigita!" - Mi pensis. Sed, farante demandon al la datumbazo:
«elektu * el ws_log_smevlog kie id=186529"Okazis, ke ĉio estis bone kun ĉi tiu linio... Vicoj kun indeksoj 186 - 530 ankaŭ funkciis senprobleme. Alia "brila ideo" malsukcesis. Poste mi komprenis kial tio okazis: kiam oni forigas kaj ŝanĝas datumojn de tabelo, ili ne estas fizike forigitaj, sed estas markitaj kiel "mortintaj opoj", tiam venas aŭtomalplena kaj markas ĉi tiujn liniojn kiel forigitaj kaj permesas ĉi tiujn liniojn esti reuzataj. Por kompreni, se la datumoj en la tabelo ŝanĝiĝas kaj aŭtomata vakuo estas ebligita, tiam ĝi ne estas konservita sinsekve.

Provo 5: SELECT, FROM, WHERE id=

Fiaskoj plifortigas nin. Vi neniam devas rezigni, vi devas iri ĝis la fino kaj kredi je vi mem kaj viaj kapabloj. Do mi decidis provi alian eblon: simple trarigardu ĉiujn registrojn en la datumbazo unu post la alia. Konante la strukturon de mia tabelo (vidu supre), ni havas id-kampon kiu estas unika (ĉefa ŝlosilo). Ni havas 1 628 991 vicojn en la tabelo kaj id estas en ordo, kio signifas, ke ni povas simple trarigardi ilin unu post alia:

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

Se iu ne komprenas, la komando funkcias jene: ĝi skanas la tabelvicon post vico kaj sendas stdout al / dev / nula, sed se la komando SELECT malsukcesas, tiam la erarteksto estas presita (stderr estas sendita al la konzolo) kaj linio enhavanta la eraron estas presita (dank' al ||, kio signifas, ke la elektita havis problemojn (la revenkodo de la komando). ne estas 0)).

Mi estis bonŝanca, mi havis indeksojn kreitajn sur la kampo id:

Mia unua sperto reakirante datumbazon Postgres post malsukceso (nevalida paĝo en bloko 4123007 de relatton bazo/16490)

Ĉi tio signifas, ke trovi linion kun la dezirata identigilo ne devus preni multe da tempo. En teorio ĝi devus funkcii. Nu, ni rulu la komandon enen tmux kaj ni enlitiĝi.

Antaŭ la mateno mi trovis, ke ĉirkaŭ 90 000 enskriboj estis rigarditaj, kio estas iom pli ol 5%. Bonega rezulto kompare kun la antaŭa metodo (2%)! Sed mi ne volis atendi 20 tagojn...

Provo 6: SELECT, FROM, WHERE id >= kaj id <

La kliento havis bonegan servilon dediĉitan al la datumbazo: du-procesoro Intel Xeon E5-2697 v2, estis eĉ 48 fadenoj en nia loko! La ŝarĝo sur la servilo estis averaĝa; ni povis elŝuti ĉirkaŭ 20 fadenojn sen problemoj. Ankaŭ estis sufiĉe da RAM: ĝis 384 gigabajtoj!

Tial, la komando devis esti paraleligita:

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

Ĉi tie eblis verki belan kaj elegantan skripton, sed mi elektis la plej rapidan paraleligan metodon: mane dividi la intervalon 0-1628991 en intervalojn de 100 000 registroj kaj ruli aparte 16 ordonojn de la formo:

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

Sed tio ne estas ĉio. En teorio, konekti al datumbazo ankaŭ prenas iom da tempo kaj sistemaj rimedoj. Konekti 1 ne estis tre saĝa, vi konsentos. Tial ni prenu 628 vicojn anstataŭ unu kontraŭ unu konekto. Kiel rezulto, la teamo transformis en ĉi tion:

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

Malfermu 16 fenestrojn en tmux-sesio kaj rulu la komandojn:

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

Tagon poste mi ricevis la unuajn rezultojn! Nome (la valoroj XXX kaj ZZZ ne plu konserviĝas):

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

Ĉi tio signifas, ke tri linioj enhavas eraron. La id-oj de la unua kaj dua problemo-registroj estis inter 829 000 kaj 830 000, la id-oj de la tria estis inter 146 000 kaj 147 000. Poste, ni simple devis trovi la precizan id-valoron de la problem-registroj. Por fari tion, ni trarigardas nian gamon kun problemaj registroj kun paŝo de 1 kaj identigas la 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

Feliĉa fino

Ni trovis la problemajn liniojn. Ni eniras la datumbazon per psql kaj provas forigi ilin:

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

Je mia surprizo, la enskriboj estis forigitaj sen problemoj eĉ sen la opcio nul_difektitaj_paĝoj.

Tiam mi konektis al la datumbazo, faris VAKUO PLENA (Mi pensas, ke ne necesis fari ĉi tion), kaj finfine mi sukcese forigis la sekurkopion uzante pg_dump. La rubejo estis prenita sen eraroj! La problemo estis solvita en tia stulta maniero. La ĝojo ne konis limojn, post tiom da malsukcesoj ni sukcesis trovi solvon!

Dankoj kaj Konkludo

Jen kiel mia unua sperto restarigi veran datumbazon de Postgres rezultis. Mi longe memoros ĉi tiun sperton.

Kaj fine, mi ŝatus diri dankon al PostgresPro pro tradukado de la dokumentaro en la rusan kaj pro tute senpagaj interretaj kursoj, kiu multe helpis dum la analizo de la problemo.

fonto: www.habr.com

Aldoni komenton