Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

Gusto kong ibahagi sa iyo ang aking unang matagumpay na karanasan sa pagpapanumbalik ng database ng Postgres sa ganap na paggana. Nakilala ko ang Postgres DBMS kalahating taon na ang nakalilipas; bago iyon wala akong karanasan sa pangangasiwa ng database.

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

Nagtatrabaho ako bilang isang semi-DevOps engineer sa isang malaking kumpanya ng IT. Bumubuo ang aming kumpanya ng software para sa mga serbisyong may mataas na load, at responsable ako para sa pagganap, pagpapanatili at pag-deploy. Binigyan ako ng karaniwang gawain: mag-update ng application sa isang server. Ang aplikasyon ay nakasulat sa Django, sa panahon ng pag-update ay isinasagawa ang mga paglilipat (mga pagbabago sa istraktura ng database), at bago ang prosesong ito ay kumuha kami ng buong database dump sa pamamagitan ng karaniwang pg_dump program, kung sakali.

Isang hindi inaasahang error ang naganap habang kumukuha ng isang dump (Postgres bersyon 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

Kulisap "hindi wastong pahina sa block" nagsasalita ng mga problema sa antas ng file system, na napakasama. Sa iba't ibang mga forum ito ay iminungkahi na gawin FULL VACUUM may opsyon zero_damaged_pages upang malutas ang problemang ito. Well, subukan natin...

Paghahanda para sa pagbawi

BABALA! Siguraduhing kumuha ng Postgres backup bago ang anumang pagtatangka na ibalik ang iyong database. Kung mayroon kang virtual machine, itigil ang database at kumuha ng snapshot. Kung hindi posible na kumuha ng snapshot, ihinto ang database at kopyahin ang mga nilalaman ng direktoryo ng Postgres (kabilang ang mga wal file) sa isang ligtas na lugar. Ang pangunahing bagay sa aming negosyo ay hindi magpalala ng mga bagay. Basahin это.

Dahil ang database ay karaniwang gumagana para sa akin, nilimitahan ko ang aking sarili sa isang regular na database dump, ngunit ibinukod ang talahanayan na may nasirang data (opsyon -T, --exclude-table=TABLE sa pg_dump).

Ang server ay pisikal, imposibleng kumuha ng snapshot. Inalis na ang backup, magpatuloy tayo.

Pagsusuri ng file system

Bago subukang ibalik ang database, kailangan nating tiyakin na ang lahat ay maayos sa mismong file system. At kung sakaling magkamali, itama ang mga ito, dahil kung hindi, maaari mo lamang mapalala ang mga bagay.

Sa aking kaso, ang file system na may database ay naka-mount "/srv" at ang uri ay ext4.

Paghinto sa database: huminto ang systemctl [protektado ng email] at suriin na ang file system ay hindi ginagamit ng sinuman at maaaring i-unmount gamit ang command lsof:
lsof +D /srv

Kinailangan ko ring ihinto ang redis database, dahil gumagamit din ito "/srv". Sunod kong inalis / srv (umount).

Ang file system ay nasuri gamit ang utility e2fsck gamit ang switch -f (Force checking kahit na ang filesystem ay minarkahan na malinis):

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

Susunod, gamit ang utility dumpe2fs (sudo dumpe2fs /dev/mapper/gu2β€”sys-srv | Sinuri ng grep) maaari mong i-verify na ang pagsusuri ay aktwal na ginawa:

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

e2fsck sabi na walang nakitang mga problema sa antas ng ext4 file system, na nangangahulugan na maaari mong ipagpatuloy ang pagsubok na ibalik ang database, o sa halip ay bumalik sa puno ng vacuum (siyempre, kailangan mong i-mount ang file system pabalik at simulan ang database).

Kung mayroon kang pisikal na server, tiyaking suriin ang katayuan ng mga disk (sa pamamagitan ng smartctl -a /dev/XXX) o RAID controller upang matiyak na ang problema ay wala sa antas ng hardware. Sa aking kaso, ang RAID ay naging "hardware", kaya't hiniling ko sa lokal na admin na suriin ang katayuan ng RAID (ang server ay ilang daang kilometro ang layo mula sa akin). Sinabi niya na walang mga pagkakamali, na nangangahulugan na maaari nating simulan ang pagpapanumbalik.

Pagsubok 1: zero_damaged_pages

Kumonekta kami sa database sa pamamagitan ng psql gamit ang isang account na may mga karapatan ng superuser. Kailangan natin ng superuser, dahil... opsyon zero_damaged_pages siya lang ang makakapagbago. Sa aking kaso ito ay mga postgres:

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

Pagpipilian zero_damaged_pages kailangan upang balewalain ang mga error sa pagbabasa (mula sa website ng postgrespro):

Kapag nakita ng PostgreSQL ang isang sira na header ng page, kadalasang nag-uulat ito ng error at ina-abort ang kasalukuyang transaksyon. Kung ang zero_damaged_pages ay pinagana, ang system sa halip ay magbibigay ng babala, i-zero ang nasirang pahina sa memorya, at magpapatuloy sa pagproseso. Sinisira ng gawi na ito ang data, lalo na ang lahat ng row sa nasirang page.

Pinagana namin ang opsyon at sinusubukang gawin ang buong vacuum ng mga talahanayan:

VACUUM FULL VERBOSE

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)
Sa kasamaang palad, malas.

Nakatagpo kami ng katulad na error:

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 – isang mekanismo para sa pag-iimbak ng "mahabang data" sa Poetgres kung hindi ito magkasya sa isang pahina (8kb bilang default).

Pagsubok 2: muling i-index

Ang unang payo mula sa Google ay hindi nakatulong. Pagkatapos ng ilang minuto ng paghahanap, nakita ko ang pangalawang tip - gawin muling i-index nasirang mesa. Nakita ko ang payo na ito sa maraming lugar, ngunit hindi ito nagbigay inspirasyon sa pagtitiwala. I-reindex natin:

reindex table ws_log_smevlog

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

muling i-index natapos nang walang problema.

Gayunpaman, hindi ito nakatulong, PUNO ANG VACUUM nag-crash na may katulad na error. Dahil sanay na ako sa mga pagkabigo, nagsimula akong maghanap ng karagdagang payo sa Internet at nakatagpo ng isang medyo kawili-wili isang artikulo.

Pagsubok 3: PUMILI, LIMIT, OFFSET

Iminungkahi ng artikulo sa itaas ang pagtingin sa hanay ng talahanayan sa bawat hilera at pag-alis ng may problemang data. Una kailangan naming tingnan ang lahat ng mga linya:

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

Sa aking kaso, ang talahanayan ay naglalaman +1 628 991 mga linya! Kinailangan itong alagaang mabuti paghahati ng data, ngunit ito ay isang paksa para sa isang hiwalay na talakayan. Sabado noon, pinatakbo ko ang utos na ito sa tmux at natulog:

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

Pagsapit ng umaga, nagpasya akong tingnan kung ano ang nangyayari. Sa aking sorpresa, natuklasan ko na pagkatapos ng 20 oras, 2% lamang ng data ang na-scan! Hindi ko gustong maghintay ng 50 araw. Isa pang kumpletong kabiguan.

Pero hindi ako sumuko. Nagtaka ako kung bakit ang tagal ng pag-scan. Mula sa dokumentasyon (muli sa postgrespro) nalaman ko:

Tinutukoy ng OFFSET na laktawan ang tinukoy na bilang ng mga row bago magsimulang mag-output ng mga row.
Kung parehong tinukoy ang OFFSET at LIMIT, lalaktawan muna ng system ang mga OFFSET na row at pagkatapos ay magsisimulang magbilang ng mga row para sa LIMIT constraint.

Kapag gumagamit ng LIMIT, mahalagang gumamit din ng ORDER BY clause upang maibalik ang mga row ng resulta sa isang partikular na pagkakasunud-sunod. Kung hindi, ibabalik ang mga hindi nahuhulaang subset ng mga row.

Malinaw na mali ang utos sa itaas: una, wala pagkakasunud-sunod ni, maaaring mali ang resulta. Pangalawa, kinailangan munang i-scan ng Postgres at laktawan ang mga OFFSET na hilera, at sa pagtaas Offset lalo pang bababa ang produktibidad.

Pagsubok 4: kumuha ng dump sa text form

Pagkatapos ay isang tila napakatalino na ideya ang pumasok sa aking isipan: kumuha ng dump sa text form at suriin ang huling naitalang linya.

Ngunit una, tingnan natin ang istraktura ng talahanayan. ws_log_smevlog:

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

Sa aming kaso mayroon kaming isang haligi "Id", na naglalaman ng natatanging identifier (counter) ng row. Ang plano ay ganito:

  1. Nagsisimula kaming kumuha ng dump sa text form (sa anyo ng mga sql command)
  2. Sa isang tiyak na punto ng oras, ang dump ay maaantala dahil sa isang error, ngunit ang text file ay mase-save pa rin sa disk
  3. Tinitingnan namin ang dulo ng text file, sa gayon nakita namin ang identifier (id) ng huling linya na matagumpay na naalis

Nagsimula akong kumuha ng dump sa text form:

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

Ang dump, tulad ng inaasahan, ay naantala sa parehong error:

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

Sa pamamagitan ng karagdagang buntot Tumingin ako sa dulo ng dump (buntot -5 ./my_dump.dump) natuklasan na ang dump ay nagambala sa linya na may id 186 525. "Kaya ang problema ay nasa linya na may id 186 526, ito ay sira, at kailangang tanggalin!" - Akala ko. Ngunit, gumagawa ng isang query sa database:
Β«piliin ang * mula sa ws_log_smevlog kung saan id=186529"Lumalabas na maayos ang lahat sa linyang ito... Ang mga hilera na may mga indeks na 186 - 530 ay gumana rin nang walang problema. Nabigo ang isa pang "makikinang na ideya". Nang maglaon ay naunawaan ko kung bakit ito nangyari: kapag nagtanggal at nagbabago ng data mula sa isang talahanayan, hindi sila pisikal na tinanggal, ngunit minarkahan bilang "mga patay na tuple", pagkatapos ay darating. autovacuum at minarkahan ang mga linyang ito bilang tinanggal at pinapayagan ang mga linyang ito na magamit muli. Upang maunawaan, kung ang data sa talahanayan ay nagbabago at ang autovacuum ay pinagana, hindi ito nakaimbak nang sunud-sunod.

Pagtatangka 5: SELECT, FROM, WHERE id=

Ang mga kabiguan ay nagpapalakas sa atin. Hindi ka dapat sumuko, kailangan mong pumunta sa dulo at maniwala sa iyong sarili at sa iyong mga kakayahan. Kaya nagpasya akong subukan ang isa pang opsyon: tingnan lamang ang lahat ng mga tala sa database nang paisa-isa. Alam ang istraktura ng aking talahanayan (tingnan sa itaas), mayroon kaming isang id field na natatangi (pangunahing susi). Mayroon kaming 1 na hanay sa talahanayan at id ay nasa pagkakasunud-sunod, na nangangahulugang maaari nating isa-isa ang mga ito:

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

Kung sinuman ang hindi nakakaintindi, gumagana ang command tulad ng sumusunod: ini-scan nito ang table row by row at nagpapadala ng stdout sa / dev / null, ngunit kung ang SELECT command ay nabigo, pagkatapos ay ang error text ay naka-print (stderr ay ipinadala sa console) at isang linya na naglalaman ng error ay naka-print (salamat sa ||, na nangangahulugan na ang pinili ay nagkaroon ng mga problema (ang return code ng command ay hindi 0)).

Ako ay mapalad, mayroon akong mga index na ginawa sa field id:

Ang aking unang karanasan sa pagbawi ng isang database ng Postgres pagkatapos ng isang pagkabigo (hindi wastong pahina sa block 4123007 ng relatton base/16490)

Nangangahulugan ito na ang paghahanap ng isang linya na may nais na id ay hindi dapat tumagal ng maraming oras. Sa teorya dapat itong gumana. Well, patakbuhin natin ang command tmux at matulog na tayo.

Sa umaga nalaman ko na humigit-kumulang 90 entries ang natingnan, na higit sa 000%. Isang mahusay na resulta kung ihahambing sa nakaraang pamamaraan (5%)! Ngunit ayaw kong maghintay ng 2 araw...

Pagtatangka 6: SELECT, FROM, WHERE id >= at id

Ang customer ay may mahusay na server na nakatuon sa database: dual-processor Intel Xeon E5 2697 v2, mayroong kasing dami ng 48 na mga thread sa aming lokasyon! Ang pag-load sa server ay karaniwan; maaari kaming mag-download ng humigit-kumulang 20 mga thread nang walang anumang mga problema. Nagkaroon din ng sapat na RAM: kasing dami ng 384 gigabytes!

Samakatuwid, ang utos ay kailangang parallelized:

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

Dito posible na magsulat ng isang maganda at eleganteng script, ngunit pinili ko ang pinakamabilis na paraan ng parallelization: manu-manong hatiin ang hanay na 0-1628991 sa pagitan ng 100 na mga tala at magpatakbo ng hiwalay na 000 na utos ng form:

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

Ngunit hindi lang iyon. Sa teorya, ang pagkonekta sa isang database ay tumatagal din ng ilang oras at mga mapagkukunan ng system. Ang pagkonekta sa 1 ay hindi masyadong matalino, sasang-ayon ka. Samakatuwid, kunin natin ang 628 row sa halip na one on one na koneksyon. Bilang resulta, ang koponan ay nagbago sa ganito:

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

Buksan ang 16 na bintana sa isang tmux session at patakbuhin ang mga utos:

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

Makalipas ang isang araw natanggap ko ang mga unang resulta! Namely (ang mga halaga XXX at ZZZ ay hindi na pinapanatili):

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

Nangangahulugan ito na ang tatlong linya ay naglalaman ng isang error. Ang mga id ng una at pangalawang talaan ng problema ay nasa pagitan ng 829 at 000, ang mga id ng pangatlo ay nasa pagitan ng 830 at 000. Susunod, kailangan lang naming hanapin ang eksaktong halaga ng id ng mga talaan ng problema. Upang gawin ito, tinitingnan namin ang aming hanay na may mga problemang talaan na may hakbang na 146 at tinutukoy ang 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

Maligayang pagtatapos

Natagpuan namin ang mga linyang may problema. Pumunta kami sa database sa pamamagitan ng psql at subukang tanggalin ang mga ito:

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

Sa aking sorpresa, ang mga entry ay tinanggal nang walang anumang problema kahit na walang pagpipilian zero_damaged_pages.

Pagkatapos ay nakakonekta ako sa database, ginawa PUNO ANG VACUUM (Sa tingin ko ay hindi na kailangang gawin ito), at sa wakas ay matagumpay kong naalis ang backup gamit pg_dump. Ang dump ay kinuha nang walang anumang mga pagkakamali! Ang problema ay nalutas sa isang hangal na paraan. Ang kagalakan ay walang hangganan, pagkatapos ng napakaraming kabiguan ay nakahanap kami ng solusyon!

Mga Pagkilala at Konklusyon

Ganito ang aking unang karanasan sa pagpapanumbalik ng isang tunay na database ng Postgres. Tatandaan ko ang karanasang ito sa mahabang panahon.

At sa wakas, nais kong magpasalamat sa PostgresPro para sa pagsasalin ng dokumentasyon sa Russian at para sa ganap na libreng online na mga kurso, na nakatulong nang malaki sa pagsusuri ng problema.

Pinagmulan: www.habr.com

Magdagdag ng komento