Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

Rád by som sa s vami podelil o moju prvú úspešnú skúsenosť s obnovením databázy Postgres na plnú funkčnosť. S Postgres DBMS som sa zoznámil pred pol rokom, predtým som nemal so správou databáz žiadne skúsenosti.

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

Pracujem ako semi-DevOps inžinier vo veľkej IT spoločnosti. Naša spoločnosť vyvíja softvér pre služby s vysokou záťažou a ja som zodpovedný za výkon, údržbu a nasadenie. Dostal som štandardnú úlohu: aktualizovať aplikáciu na jednom serveri. Aplikácia je napísaná v Django, počas aktualizácie sa vykonávajú migrácie (zmeny v štruktúre databázy) a pred týmto procesom urobíme pre každý prípad úplný výpis databázy cez štandardný program pg_dump.

Počas vytvárania výpisu sa vyskytla neočakávaná chyba (Postgres verzia 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

Chyba "neplatná stránka v bloku" hovorí o problémoch na úrovni súborového systému, čo je veľmi zlé. Na rôznych fórach to bolo navrhnuté ÚPLNÉ VÁKUUM s opciou nula_poškodených_stránok na vyriešenie tohto problému. No skúsme...

Príprava na zotavenie

UPOZORNENIE Pred akýmkoľvek pokusom o obnovenie databázy nezabudnite urobiť zálohu Postgres. Ak máte virtuálny počítač, zastavte databázu a urobte snímku. Ak nie je možné urobiť snímku, zastavte databázu a skopírujte obsah adresára Postgres (vrátane súborov wal) na bezpečné miesto. Hlavnou vecou v našom podnikaní nie je robiť veci horšie. Čítať это.

Keďže databáza vo všeobecnosti fungovala pre mňa, obmedzil som sa na bežný výpis databázy, ale vylúčil som tabuľku s poškodenými údajmi (možnosť -T, --exclude-table=TABLE v pg_dump).

Server bol fyzický, nebolo možné urobiť snímku. Záloha bola odstránená, poďme ďalej.

Kontrola súborového systému

Pred pokusom o obnovenie databázy sa musíme uistiť, že je všetko v poriadku so samotným súborovým systémom. A v prípade chýb ich opravte, pretože inak môžete veci len zhoršiť.

V mojom prípade bol súborový systém s databázou pripojený "/srv" a typ bol ext4.

Zastavenie databázy: systemctl stop [chránené e-mailom] a skontrolujte, či súborový systém nikto nepoužíva a možno ho odpojiť pomocou príkazu tiež:
lsof +D /srv

Musel som tiež zastaviť databázu redis, pretože sa tiež používala "/srv". Ďalej som odmontoval / srv (umount).

Súborový systém bol skontrolovaný pomocou pomôcky e2fsck s prepínačom -f (Vynútiť kontrolu, aj keď je súborový systém označený ako čistý):

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

Ďalej pomocou pomôcky dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep skontrolovaný) môžete overiť, či bola kontrola skutočne vykonaná:

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

e2fsck hovorí, že na úrovni súborového systému ext4 neboli nájdené žiadne problémy, čo znamená, že môžete pokračovať v pokuse o obnovu databázy, alebo sa radšej vrátiť na vákuum plné (samozrejme je potrebné pripojiť súborový systém späť a spustiť databázu).

Ak máte fyzický server, nezabudnite skontrolovať stav diskov (cez smartctl -a /dev/XXX) alebo radič RAID, aby ste sa uistili, že problém nie je na úrovni hardvéru. V mojom prípade sa ukázalo, že RAID je „hardvérový“, preto som požiadal miestneho správcu, aby skontroloval stav RAID (server bol odo mňa vzdialený niekoľko stoviek kilometrov). Povedal, že tam neboli žiadne chyby, čo znamená, že môžeme definitívne začať s reštaurovaním.

Pokus 1: zero_damaged_pages

K databáze sa pripájame cez psql s účtom, ktorý má práva superužívateľa. Potrebujeme superužívateľa, pretože... možnosť nula_poškodených_stránok len on sa môže zmeniť. V mojom prípade je to postgres:

psql -h 127.0.0.1 -U postgres -s [názov_databázy]

Možnosť nula_poškodených_stránok potrebné na ignorovanie chýb čítania (z webovej stránky postgrespro):

Keď PostgreSQL zistí poškodenú hlavičku stránky, zvyčajne ohlási chybu a preruší aktuálnu transakciu. Ak je zapnuté zero_damaged_pages, systém namiesto toho vydá varovanie, vynuluje poškodenú stránku v pamäti a pokračuje v spracovaní. Toto správanie zničí údaje, konkrétne všetky riadky na poškodenej stránke.

Povolíme možnosť a pokúsime sa urobiť úplné vákuum tabuliek:

VACUUM FULL VERBOSE

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)
Bohužiaľ, smola.

Stretli sme sa s podobnou chybou:

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 – mechanizmus na ukladanie „dlhých dát“ v Poetgrese, ak sa nezmestia na jednu stránku (štandardne 8 kb).

Pokus 2: preindexovanie

Prvé rady od Googlu nepomohli. Po pár minútach hľadania som našiel druhý tip – vyrobiť preindexovať poškodený stôl. Túto radu som videl na mnohých miestach, no nevzbudzovala dôveru. Poďme preindexovať:

reindex table ws_log_smevlog

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

preindexovať dokončené bez problémov.

Nepomohlo to však, VÁKUUM PLNÉ havaroval s podobnou chybou. Keďže som zvyknutý na neúspechy, začal som hľadať rady na internete ďalej a narazil som na celkom zaujímavú vec статью.

Pokus 3: SELECT, LIMIT, OFFSET

Vyššie uvedený článok navrhol pozrieť sa na tabuľku riadok po riadku a odstrániť problematické údaje. Najprv sme sa museli pozrieť na všetky riadky:

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 mojom prípade tabuľka obsahovala 1 628 991 linky! Bolo potrebné sa oň dobre starať rozdelenie dát, ale toto je téma na samostatnú diskusiu. Bola sobota, spustil som tento príkaz v tmux a išiel som spať:

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

Ráno som sa rozhodol skontrolovať, ako sa veci majú. Na moje prekvapenie som zistil, že po 20 hodinách boli naskenované iba 2 % údajov! Nechcelo sa mi čakať 50 dní. Ďalšie úplné zlyhanie.

Ale nevzdal som sa. Zaujímalo ma, prečo skenovanie trvalo tak dlho. Z dokumentácie (opäť na postgrespro) som zistil:

OFFSET určuje preskočenie zadaného počtu riadkov pred spustením výstupných riadkov.
Ak sú zadané OFFSET aj LIMIT, systém najprv preskočí riadky OFFSET a potom začne počítať riadky pre obmedzenie LIMIT.

Pri použití LIMIT je dôležité použiť aj klauzulu ORDER BY, aby sa riadky výsledkov vrátili v špecifickom poradí. V opačnom prípade sa vrátia nepredvídateľné podmnožiny riadkov.

Je zrejmé, že vyššie uvedený príkaz bol nesprávny: po prvé, neexistoval zoradiť podľa, výsledok môže byť chybný. Po druhé, Postgres musel najprv naskenovať a preskočiť riadky OFFSET a so zvyšovaním OFFSET produktivita by ešte viac klesla.

Pokus 4: urobte výpis v textovej forme

Potom mi prišiel na um zdanlivo geniálny nápad: urobiť skládku v textovej podobe a analyzovať posledný nahraný riadok.

Najprv sa však pozrime na štruktúru tabuľky. ws_log_smevlog:

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

V našom prípade máme stĺpec „Id“, ktorý obsahoval jedinečný identifikátor (počítadlo) riadku. Plán bol takýto:

  1. Začneme vytvárať výpis v textovej forme (vo forme príkazov sql)
  2. V určitom okamihu by sa výpis prerušil kvôli chybe, ale textový súbor by bol stále uložený na disku
  3. Pozrieme sa na koniec textového súboru, čím nájdeme identifikátor (id) posledného riadku, ktorý bol úspešne odstránený

Začal som brať výpis v textovej forme:

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

Výpis bol podľa očakávania prerušený rovnakou chybou:

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

Ďalej cez chvost Pozrel som sa na koniec skládky (chvost -5 ./my_dump.dump) zistil, že výpis bol prerušený na linke s id 186 525. "Takže problém je v riadku s id 186 526, je pokazený a treba ho vymazať!" - Myslel som. Ale zadanie dotazu do databázy:
«vyberte * z ws_log_smevlog, kde id=186529„Ukázalo sa, že s touto linkou je všetko v poriadku... Bez problémov fungovali aj riadky s indexmi 186 530 – 186 540. Ďalší „geniálny nápad“ zlyhal. Neskôr som pochopil, prečo sa to stalo: pri odstraňovaní a zmene údajov z tabuľky sa fyzicky neodstránia, ale sú označené ako „mŕtve n-tice“, potom príde autovákuum a označí tieto riadky ako vymazané a umožní opätovné použitie týchto riadkov. Aby ste pochopili, ak sa údaje v tabuľke zmenia a povolí sa automatické vákuovanie, neuložia sa postupne.

Pokus 5: SELECT, FROM, WHERE id=

Neúspechy nás robia silnejšími. Nikdy by ste sa nemali vzdávať, musíte ísť až do konca a veriť v seba a svoje schopnosti. Rozhodol som sa teda vyskúšať inú možnosť: stačí si postupne prezrieť všetky záznamy v databáze. Keď poznáme štruktúru mojej tabuľky (pozri vyššie), máme pole id, ktoré je jedinečné (primárny kľúč). V tabuľke máme 1 628 991 riadkov a id sú v poriadku, čo znamená, že ich môžeme prejsť jeden po druhom:

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

Ak niekto nerozumie, príkaz funguje nasledovne: prehľadá tabuľku riadok po riadku a odošle stdout na / Dev / null, ale ak príkaz SELECT zlyhá, vytlačí sa chybový text (stderr sa odošle do konzoly) a vypíše sa riadok obsahujúci chybu (vďaka ||, čo znamená, že select mal problémy (návratový kód príkazu nie je 0)).

Mal som šťastie, mal som na ihrisku vytvorené indexy id:

Moja prvá skúsenosť s obnovou databázy Postgres po zlyhaní (neplatná stránka v bloku 4123007 relatton base/16490)

To znamená, že nájdenie riadku s požadovaným ID by nemalo trvať veľa času. Teoreticky by to malo fungovať. No, poďme spustiť príkaz tmux a poďme spať.

Do rána som zistil, že bolo videných asi 90 000 záznamov, čo je niečo cez 5 %. Vynikajúci výsledok v porovnaní s predchádzajúcou metódou (2%)! Ale nechcelo sa mi čakať 20 dní...

Pokus 6: SELECT, FROM, WHERE id >= a id

Zákazník mal k dispozícii vynikajúci server určený pre databázu: dvojprocesorový Intel Xeon E5-2697 v2, v našej lokalite bolo až 48 vlákien! Zaťaženie servera bolo priemerné, bez problémov sme stiahli okolo 20 vlákien. RAM bolo tiež dosť: až 384 gigabajtov!

Preto bolo potrebné príkaz paralelizovať:

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 bolo možné napísať krásny a elegantný skript, ale zvolil som najrýchlejšiu metódu paralelizácie: manuálne rozdeliť rozsah 0-1628991 na intervaly 100 000 záznamov a samostatne spustiť 16 príkazov formulára:

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

To však nie je všetko. Teoreticky aj pripojenie k databáze vyžaduje určitý čas a systémové prostriedky. Pripojenie 1 628 991 nebolo veľmi chytré, budete súhlasiť. Preto získajme 1000 riadkov namiesto jedného na jedno pripojenie. V dôsledku toho sa tím zmenil na toto:

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

Otvorte 16 okien v relácii tmux a spustite príkazy:

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

O deň neskôr som dostal prvé výsledky! Konkrétne (hodnoty XXX a ZZZ už nie sú zachované):

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 znamená, že tri riadky obsahujú chybu. Id prvého a druhého problematického záznamu boli medzi 829 000 a 830 000, id tretieho medzi 146 000 a 147 000. Ďalej sme jednoducho museli nájsť presnú hodnotu id problematických záznamov. Aby sme to dosiahli, prezeráme si náš rozsah s problematickými záznamami s krokom 1 a identifikujeme 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

Šťastný koniec

Našli sme problematické čiary. Ideme do databázy cez psql a pokúsime sa ich odstrániť:

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 prekvapenie sa záznamy bez problémov vymazali aj bez voľby nula_poškodených_stránok.

Potom som sa pripojil k databáze, urobil VÁKUUM PLNÉ (Myslím, že to nebolo potrebné) a nakoniec som úspešne odstránil zálohu pomocou pg_dump. Skládka prebehla bez chýb! Problém bol vyriešený takýmto hlúpym spôsobom. Radosť nemala hraníc, po toľkých neúspechoch sa nám podarilo nájsť riešenie!

Poďakovanie a záver

Takto dopadla moja prvá skúsenosť s obnovou skutočnej databázy Postgres. Na tento zážitok budem ešte dlho spomínať.

A na záver by som rád poďakoval PostgresPro za preklad dokumentácie do ruštiny a pre úplne bezplatné online kurzy, čo veľmi pomohlo pri analýze problému.

Zdroj: hab.com

Pridať komentár