Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

Dëshiroj të ndaj me ju përvojën time të parë të suksesshme në rikthimin e plotë të funksionalitetit të bazës së të dhënave Postgres. Kam njohuri për SGBD Postgres që prej gjashtë muajsh, përpara kësaj nuk kam pasur asnjë përvojë në administrimin e bazave të të dhënave.

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

Punoj gjysmë-inxhinier DevOps në një kompani të madhe IT. Kompania jonë merret me zhvillimin e softuerit për shërbime me ngarkesë të lartë, ndërsa unë përgjigjem për funksionimin, mirëmbajtjen dhe deploy-in. Më është vënë një detyrë standarde: të përditësoj një aplikacion në një server. Aplikacioni është shkruar në Django, gjatë përditësimit bënë migrime (ndryshimi i strukturës së bazës së të dhënave), dhe para këtij procesi ne marrë një kopje të plotë të bazës së të dhënave përmes programit standard pg_dump, për çdo rast.

Gjatë marrjes së kopjes ndodhi një gabim të papritur (versioni i Postgres – 9.5):

pg_dump: Marrja e përmbajtjes së tabelës “ws_log_smevlog” dështoi: PQgetResult() dështoi.
pg_dump: Mesazhi i gabimit nga serveri: ERROR: faqe e pavlefshme në bllokun 4123007 të bazës së të dhënave /16490/21396989
pg_dump: Urdhri ishte: COPY public.ws_log_smevlog [...]
pg_dump: [arkivimi paralel] një proces pune dështoi papritur

Gabimi «faqe e pavlefshme në bllok» flet për probleme në nivelin e sistemit të skedarëve, që nuk është aspak mirë. Në forumet e ndryshme u sugjerua të bëhej FULL VACUUM me opsionin zero_damaged_pages për të zgjidhur këtë problem. Çfarëdo, le të provoni...

Përgatitja për rikuperim

KËSHILLIM! Sigurohuni që të bëni një kopje rezervë të Postgres para çdo përpjekjeje për të rikuperuar bazën e të dhënave. Nëse keni një makinë virtuale, ndaloni bazën e të dhënave dhe bëni një snapshot. Nëse nuk keni mundësi të bëni një snapshot, ndaloni bazën dhe kopjoni përmbajtjen e katalagut të Postgres (duke përfshirë skedarët wal) në një vend të sigurt. E rëndësishme në punën tonë është të mos e krijojmë situatën më keq. Lexoni kjo.

Pasi që në përgjithësi baza punonte, u kufizova në një dump të zakonshëm të bazës së të dhënave, por përjashtova tabelën me të dhëna të dëmtuara (opsioni -T, —exclude-table=TABLE në pg_dump).

Serveri ishte fizik, nuk ishte e mundur të bëhej snapshot. Backup është marrë, le të vazhdojmë më tej.

Kontrolli i sistemit të skedarëve

Para se të përpiqeni të rikuperoni bazën e të dhënave, është e nevojshme të siguroheni që gjithçka të jetë në rregull me sistemin e skedarëve. Dhe në rast të gabimeve, t'i korrigjoni ato, sepse ndryshe mund të bëni vetëm më keq.

Në rastin tim, sistemi i skedarëve me bazën e të dhënave ishte i montuar në «/srv» dhe tipi ishte ext4.

Po ndalojmë bazën e të dhënave: systemctl stop postgresql@9.5-main.service dhe kontrollojmë që sistemi i skedarëve nuk përdoret nga askush dhe mund ta çmontojmë me komandën lsof:
lsof +D /srv

Më duhej të ndaloja gjithashtu bazën e të dhënave redis, pasi ajo gjithashtu kishte përdorur «/srv». Më pas e çmontova /srv (umount).

Kontrolli i sistemit të skedarëve u krye me ndihmën e utilitarit e2fsck me çelësin -f (Kontrollo forcërisht edhe nëse sistemi i skedarëve është shënuar si i pastër):

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

Më pas me ndihmën e utilitarit dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep checked) mund të sigurohemi se kontrolli në të vërtetë u krye:

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

e2fsck tregon se nuk ka probleme në nivelin e sistemit të skedarëve ext4, që nënkupton se mund të vazhdojmë përpjekjet për të rikthyer bazën e të dhënave, për të qenë më të saktë kthehen në vacuum full (sigurisht, duhet ta montojmë përsëri sistemin e skedarëve dhe të nisim bazën e të dhënave).

Nëse keni një server fizik, sigurohuni të kontrolloni gjendjen e disqeve (përmes smartctl -a /dev/XXX) ose RAID-kontrollerit, për t'u siguruar që problemi nuk është në nivelin harduerik. Në rastin tim, RAID doli të ishte 'hard', prandaj i kërkova adminit lokal të kontrollonte gjendjen e RAID (serveri ishte disa qindra kilometra larg meje). Ai tha se nuk kishte gabime, çka do të thotë se ne mund të fillojmë rikuperimin.

Përpjekja 1: zero_damaged_pages

Po lidhim me bazën përmes psql me një llogari që ka të drejta superuser. Na nevojitet pikërisht superuser, sepse vetëm ai mund të ndryshojë opsionin zero_damaged_pages në rastin tim, ky është postgres:

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

Opcioni zero_damaged_pages na nevojitet për të injoruar gabimet e leximit (nga faqja postgrespro):

Kur zbulon një titull të dëmtuar, Postgres Pro zakonisht njofton për një gabim dhe ndërpret transaksionin aktual. Nëse parameteri zero_damaged_pages është aktivizuar, sistemi jep një paralajmërim, zero një faqe të dëmtuar në kujtesë dhe vazhdon përpunimin. Ky sjellje shkatërron të dhënat, pikërisht të gjitha rreshtat në faqen e dëmtuar.

Aktivizojmë opsionin dhe provojmë të bëjmë vacuum të plotë të tabelës:

VACUUM FULL VERBOSE

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)
Fatkeqësisht, dështim.

U përballëm me një gabim të ngjashëm:

INFO: pastrimi "public.ws_log_smevlog"
KUJDES: faqe e pavlefshme në bllokun 4123007 të lidhjes base/16400/21396989; zero pastrimi faqe
ERROR: numër i papritur të përbërësit 573 (pritet 565) për vlerën toast 21648541 në pg_toast_106070

pg_toast – mekanizmi për ruajtjen e «të dhënave të gjata» në PostgreSQL, nëse ato nuk përfshihen në një faqe (për herë të parë 8kB).

Përpjekja 2: reindex

Këshilla e parë nga Google nuk ndihmoi. Pas disa minutash kërkimi, gjetën një këshillë të dytë – të bëni reindex tabelës së dëmtuar. Këtë këshillë e kam parë në shumë vende, por nuk frymëzoi besim. Le të bëjmë reindex:

reindex table ws_log_smevlog

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

reindex u përfundua pa probleme.

Megjithatë, kjo nuk ndihmoi, VACUUM FULL u mbyll me një gabim të ngjashëm. Duke qenë se isha mësuar me dështimet, fillova të kërkoj më shumë këshilla në internet dhe ndesha një artikullin.

Përpjekja 3: SELECT, LIMIT, OFFSET

Në artikullin e mësipërm sugjerohej të shihni tabelën rresht pas rreshti dhe të fshini të dhënat problematike. Së pari, duhej të shikoja të gjitha rreshtat:

for ((i=0; i/dev/null || echo $i; done

Në rastin tim, tabela kishte 1 628 991 rreshta! Në mënyrë ideale duhej të kujdesesha për particionimin e të dhënave, porë kjo është një temë për një diskutim të veçantë. Ishte e shtunë, unë e nisa këtë komandë në tmux dhe shkova të flija:

for ((i=0; i/dev/null || echo $i; done

Në mëngjes vendosa të kontrolloj se si ishin punët. Për habinë time, zbulova se gjatë 20 orëve ishin skanuar vetëm 2% të të dhënave! Nuk doja të prisja 50 ditë. Një dështim tjetër total.

Por nuk u dorëzova. Më porositi kurioziteti përse skanimi po zgjatej kaq shumë. Nga dokumentacioni (prapë në postgrespro) mësova:

OFFSET tregon për të anashkaluar numrin e caktuar të rreshtave, para se të fillojë të japë rreshtat.
Nëse janë dhënë dhe OFFSET, dhe LIMIT, së pari sistemi anashkalon rreshtat OFFSET, dhe pastaj fillon të numërojë rreshtat për kufizimin LIMIT.

Duke përdorur LIMIT, është e rëndësishme gjithashtu të përdoret klauzola ORDER BY, në mënyrë që rreshtat e rezultatit të jepen në një rend të caktuar. Në të kundërt, do të kthehen nën-grupe të papritura të rreshtave.

Me sa duket, komanda e lartpërmendur ishte e gabuar: së pari, nuk kishte order by, rezultati mund të ishte i pasaktë. Së dyti, Postgres fillimisht duhej të kishte skanuar dhe anashkaluar rreshtat OFFSET, dhe me rritjen e OFFSET performanca do të përkeqësohej akoma më shumë.

Përpjekja 4: të krijohet një dump në format tekstual

Më pas, më erdhi në mendje një ide që dukej gjeniale: të krijoj një dump në format tekstual dhe të analizoj rreshtin e fundit të regjistruar.

Por fillimisht, le të njohim strukturën e tabelës ws_log_smevlog:

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

Në rastin tonë kemi një kolona «id», i cili kishte një identifikues unik (nummers) për rreshtin. Plani ishte:

  1. Nisemi të krijojmë dump në format tekstual (në formë sql-commands)
  2. Në një moment të caktuar, krijimi i dump-it do të ndalej për shkak të një gabimi, megjithatë skedari tekstual do të ruhej gjithsesi në disk
  3. Shikojmë fundin e skedarit tekstual, duke gjetur kështu identifikuesin (id) të rreshtit të fundit që u krijua me sukses

Nisa të krijoj dump në format tekstual:

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

Krijimi i dump-it, siç pritej, u ndërpre me të njëjtin gabim:

pg_dump: Mesazhi i gabimit nga serveri: ERROR: faqe e pavlefshme në bllokun 4123007 të relatton base/16490/21396989

Më pas përmes tail shikova fundin e dump-it (tail -5 ./my_dump.dump) zbuloj se dump-i u ndërpre në rreshtin me id 186 525. „Pra, problemi është në rreshtin me id 186 526, ai është i prishur, duhet të fshihet!” – mendoja. Por, duke bërë një kërkesë në bazën e të dhënave:
«zgjedh * nga ws_log_smevlog ku id=186529» u zbulua se kjo linjë është në rregull… Linjat me indekset 186 530 — 186 540 gjithashtu funksionuan pa probleme. Një tjetër «ide gjeniale» dështoi. Më vonë kuptova pse ndodhi kjo: gjatë fshirjes/modifikimit të të dhënave nga tabela, ato nuk fshihen fizikisht, por markohen si «tupe të vdekur», dhe më pas vjen autovacuum dhe i shënon këto linja si të fshira dhe lejon përdorimin e këtyre linjave përsëri. Për të kuptuar, nëse të dhënat në tabelë ndryshojnë dhe autovacuum është aktiv, ato nuk ruajnë radhitjen e vazhdueshme.

Përpjekja 5: SELECT, FROM, WHERE id=

Dështimet na bëjnë më të fortë. Kurrë mos u dorëzoni, duhet të vazhdoni deri në fund dhe të besoni në veten tuaj dhe në mundësitë tuaja. Prandaj, vendosa të provoj një version tjetër: thjesht të shqyrtoj të gjitha regjistrimet në bazën e të dhënave një nga një. Duke ditur strukturën e tabelës sime (shih më sipër), kemi një fushë id, e cila është unike (çelësi primar). Në tabelë kemi 1 628 991 linja dhe id shkruhen në rend, që do të thotë se mund t'i kalojmë thjesht një nga një:

për ((i=1; i<1628991; i=$((i+1)) )); bëj psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog ku id=$i" >/dev/null || echo $i; bërë

Nëse dikush nuk e kupton, ekipi punon kështu: shqyrton rresht pas rreshti tabelën dhe dërgon stdout në /dev/null, por nëse komandë SELECT dështon, atëherë printohet teksti i gabimit (stderr dërgohet në konsolë) dhe printohet një varg që përmban gabimin (falë ||, që tregon se pati probleme me select (kodin e kthimit të komandës nuk është 0)).

Më paska ndodhur fat, kam krijuar indeksi mbi fushën id:

Eksperienca ime e parë në rikuperimin e bazës së të dhënave Postgres pas një dështimi (faqe e pavlefshme në bllok 4123007 të bazës së relacionit/16490)

Dhe kjo do të thotë se gjetja e rreshtit me id e duhur nuk do të duhej të merrte shumë kohë. Në teori, duhet të funksiononte. Pra, le ta shfrytëzojmë komandën në tmux dhe të shkojmë të flijmë.

Në mëngjes zbulova se ishin shqyrtuar rreth 90,000 regjistrime, që përbën pak më shumë se 5%. Një rezultat i shkëlqyer, në krahasim me mënyrën e mëparshme (2%)! Por nuk doja të prisja 20 ditë...

Përpjekja 6: SELECT, FROM, WHERE id >= and id <

Për klientin, nën DB ishte caktuar një server i shkëlqyer: dy procese Intel Xeon E5-2697 v2, në përbërjen tonë kishte plot 48 procese! Ngarkesa në server ishte e mesme, ne pa ndonjë problem të madh mund të merrnim rreth 20 procese. Memoria e operativës ishte gjithashtu mjaft e mjaftueshme: plot 384 gigabajt!

Prandaj, ekipi duhej të shpërndahej:

për ((i=1; i<1628991; i=$((i+1)) )); bëj psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog ku id=$i" >/dev/null || echo $i; bërë

Këtu mund të ishte shkruar një skript i bukur dhe elegant, por unë zgjodha mënyrën më të shpejtë për paralelizimin: të ndaj intervalin 0-1628991 manualisht në intervale nga 100,000 regjistrime dhe të nis 16 komanda të ndryshme të tipit:

for ((i=N; i/dev/null || echo $i; done

Por kjo nuk është gjithçka. Idealisht, lidhja me bazën e të dhënave gjithashtu merr ca kohë dhe burime sistemore. Të lidhesh 1,628,991 nuk ishte shumë e arsyeshme, prano. Prandaj le të nxjerrim 1000 rreshta në një lidhje në vend të njërit. Si rezultat, komanda u transformua në këtë:

for ((i=N; i=$i and id/dev/null || echo $i; done

Hapim 16 dritare në sesionin tmux dhe nisim komandat:

1) for ((i=0; i=$i and id/dev/null || echo $i; done
2) for ((i=100000; i=$i and id/dev/null || echo $i; done
…
15) for ((i=1400000; i=$i and id/dev/null || echo $i; done
16) for ((i=1500000; i=$i and id/dev/null || echo $i; done

Një ditë më vonë mora rezultatet e para! Konkretisht (vlerat XXX dhe ZZZ nuk u ruajtën më):

ERROR: mungon numri i rreshtit 0 për vlerën toast 37837571 në pg_toast_106070
829000
ERROR: mungon numri i rreshtit 0 për vlerën toast XXX në pg_toast_106070
829000
ERROR: mungon numri i rreshtit 0 për vlerën toast ZZZ në pg_toast_106070
146000

Kjo do të thotë se kemi tre rreshta që përmbajnë një gabim. id e regjistrimeve problematike të parë dhe të dytë ndodheshin midis 829 000 dhe 830 000, id e të tretës – midis 146 000 dhe 147 000. Më pas, ne kishim thjesht për të gjetur vlerën e saktë të id e regjistrimeve problematike. Për këtë, shqyrtojmë gamën tonë të regjistrimeve problematike me hapat 1 dhe identifikojmë id:

për ((i=829000; i<830000; i=$((i+1)) )); bëj psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; bërë
829417
KODI I GABIMIT: numri i papritur i copëzave 2 (pritet 0) për vlerën toast 37837843 në pg_toast_106070
829449
për ((i=146000; i<147000; i=$((i+1)) )); bëj psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; bërë
829417
KODI I GABIMIT: numri i papritur i copëzave ZZZ (pritet 0) për vlerën toast XXX në pg_toast_106070
146911

Fund i lumtur

Gjetëm rreshtat problematikë. Hyjmë në bazën e të dhënave përmes psql dhe provojmë t'i fshijmë ato:

my_database=# fshi nga ws_log_smevlog ku id=829417;
SHTO 1
my_database=# fshi nga ws_log_smevlog ku id=829449;
SHTO 1
my_database=# fshi nga ws_log_smevlog ku id=146911;
SHTO 1

Për habinë time, regjistrimet u fshinë pa asnjë problem, edhe pa opsionin zero_damaged_pages.

Më pas u lidhja me bazën, bëra VACUUM FULL (mendoj se nuk ishte e nevojshme), dhe, përfundimisht, tërheqja e backup-it u krye me sukses përmes pg_dump. Dumps u bënë pa asnjë gabim! Problemi u zgjidh në këtë mënyrë shumë të thjeshtë. Nuk kishte fund gëzimi, pas kaq shumë dështimesh, arritëm të gjejmë një zgjidhje!

Faleminderit dhe përfundim

Kjo ishte përvoja ime e parë në rikuperimin e një baze të dhënash reale Postgres. Ky përvojë do ta mbaj në mend për një kohë të gjatë.

Dhe përfundimisht, do doja të falenderoja kompaninë PostgresPro për dokumentacionin e përkthyer në gjuhën ruse dhe për kurse online plotësisht falas, të cilat ndihmuan shumë gjatë analizës së problemit.

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster