Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

Rád bych se s vámi podělil o svou první úspěšnou zkušenost s obnovením databáze Postgres do plné funkčnosti. S Postgres DBMS jsem se seznámil před půl rokem, předtím jsem neměl se správou databází vůbec žádné zkušenosti.

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

Pracuji jako semi-DevOps inženýr ve velké IT společnosti. Naše společnost vyvíjí software pro vysoce vytížené služby a já jsem zodpovědný za výkon, údržbu a nasazení. Dostal jsem standardní úkol: aktualizovat aplikaci na jednom serveru. Aplikace je napsána v Django, během aktualizace se provádějí migrace (změny struktury databáze) a před tímto procesem provedeme pro každý případ úplný výpis databáze přes standardní program pg_dump.

Při vytváření výpisu došlo k neočekávané chybě (Postgres verze 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" hovoří o problémech na úrovni souborového systému, což je velmi špatné. Na různých fórech to bylo doporučeno udělat PLNÉ VAKUUM s opcí nula_poškozených_stránek k vyřešení tohoto problému. No, zkusme...

Příprava na zotavení

VAROVÁNÍ! Před jakýmkoli pokusem o obnovu databáze nezapomeňte provést zálohu Postgres. Pokud máte virtuální počítač, zastavte databázi a pořiďte snímek. Pokud není možné pořídit snímek, zastavte databázi a zkopírujte obsah adresáře Postgres (včetně souborů wal) na bezpečné místo. V našem podnikání jde především o to, abychom věci nezhoršovali. Číst tento.

Protože mi databáze obecně fungovala, omezil jsem se na běžný výpis databáze, ale vyloučil jsem tabulku s poškozenými daty (volba -T, --exclude-table=TABLE v pg_dump).

Server byl fyzický, nebylo možné pořídit snímek. Záloha byla odstraněna, pojďme dál.

Kontrola souborového systému

Před pokusem o obnovu databáze se musíme ujistit, že je vše v pořádku se samotným systémem souborů. A v případě chyb je opravte, protože jinak můžete věci jen zhoršit.

V mém případě byl souborový systém s databází připojen "/srv" a typ byl ext4.

Zastavení databáze: zastavení systemctl [chráněno e-mailem] a zkontrolujte, zda souborový systém nikdo nepoužívá a lze jej odpojit pomocí příkazu také:
lsof +D /srv

Také jsem musel zastavit databázi redis, protože se také používala "/srv". Dále jsem odpojil / srv (umount).

Systém souborů byl zkontrolován pomocí nástroje e2fsck pomocí přepínače -f (Vynutit kontrolu, i když je souborový systém označen jako čistý):

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

Dále pomocí utility dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep zkontrolován) můžete ověřit, že kontrola byla skutečně provedena:

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

e2fsck říká, že nebyly nalezeny žádné problémy na úrovni souborového systému ext4, což znamená, že můžete pokračovat ve snaze obnovit databázi, nebo se raději vrátit na vakuum plné (samozřejmě je potřeba připojit souborový systém zpět a spustit databázi).

Pokud máte fyzický server, nezapomeňte zkontrolovat stav disků (přes smartctl -a /dev/XXX) nebo řadič RAID, abyste se ujistili, že problém není na úrovni hardwaru. V mém případě se ukázalo, že RAID je „hardwarový“, takže jsem požádal místního administrátora, aby zkontroloval stav RAID (server byl několik set kilometrů ode mě). Řekl, že nebyly žádné chyby, což znamená, že můžeme definitivně začít s restaurováním.

Pokus 1: zero_damaged_pages

K databázi se připojujeme přes psql s účtem, který má práva superuživatele. Potřebujeme superuživatele, protože... volba nula_poškozených_stránek jen on se může změnit. V mém případě je to postgres:

psql -h 127.0.0.1 -U postgres -s [název_databáze]

Možnost nula_poškozených_stránek potřebné k ignorování chyb čtení (z webu postgrespro):

Když PostgreSQL detekuje poškozenou hlavičku stránky, obvykle ohlásí chybu a přeruší aktuální transakci. Pokud je povoleno zero_damaged_pages, systém místo toho vydá varování, vynuluje poškozenou stránku v paměti a pokračuje ve zpracování. Toto chování zničí data, konkrétně všechny řádky na poškozené stránce.

Povolíme možnost a pokusíme se udělat úplné vakuum tabulek:

VACUUM FULL VERBOSE

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)
Bohužel smůla.

Narazili jsme na podobnou chybu:

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 – mechanismus pro ukládání „dlouhých dat“ v Poetgresu, pokud se nevejdou na jednu stránku (standardně 8 kb).

Pokus 2: přeindexování

První rady od Googlu nepomohly. Po pár minutách hledání jsem našel druhý tip – vyrobit přeindexovat poškozený stůl. Tuto radu jsem viděl na mnoha místech, ale nevzbuzovala důvěru. Pojďme reindexovat:

reindex table ws_log_smevlog

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

přeindexovat dokončeno bez problémů.

To však nepomohlo, VAKUUM PLNÝ havaroval s podobnou chybou. Jelikož jsem na neúspěchy zvyklý, začal jsem hledat rady dál na internetu a narazil jsem na docela zajímavou věc Článek.

Pokus 3: SELECT, LIMIT, OFFSET

Výše uvedený článek navrhoval podívat se na tabulku řádek po řádku a odstranit problematická data. Nejprve jsme se museli podívat na všechny řádky:

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 mém případě tabulka obsahovala 1 628 991 linky! Bylo potřeba se o něj dobře starat rozdělení dat, ale to je téma na samostatnou diskusi. Byla sobota, spustil jsem tento příkaz v tmux a šel spát:

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 jsem se rozhodl zkontrolovat, jak se věci mají. Ke svému překvapení jsem zjistil, že po 20 hodinách byla naskenována pouze 2 % dat! Nechtěl jsem čekat 50 dní. Další naprosté selhání.

Ale nevzdal jsem to. Přemýšlel jsem, proč skenování trvalo tak dlouho. Z dokumentace (opět na postgrespro) jsem zjistil:

OFFSET určuje, že se má přeskočit zadaný počet řádků před zahájením výstupních řádků.
Pokud jsou zadány OFFSET i LIMIT, systém nejprve přeskočí řádky OFFSET a poté začne počítat řádky pro omezení LIMIT.

Při použití LIMIT je důležité použít také klauzuli ORDER BY, aby byly výsledné řádky vráceny v určitém pořadí. V opačném případě budou vráceny nepředvídatelné podmnožiny řádků.

Je zřejmé, že výše uvedený příkaz byl nesprávný: zaprvé nebyl žádný objednat podle, výsledek může být chybný. Za druhé, Postgres musel nejprve naskenovat a přeskočit OFFSET řádky, a to se zvyšováním OFFSET produktivita by ještě klesala.

Pokus 4: Udělejte výpis v textové podobě

Pak mě napadl zdánlivě geniální nápad: vzít skládku v textové podobě a analyzovat poslední nahranou větu.

Nejprve se ale podívejme na strukturu tabulky. ws_log_smevlog:

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

V našem případě máme sloupec „ID“, který obsahoval jedinečný identifikátor (počítadlo) řádku. Plán byl takový:

  1. Začneme vytvářet výpis v textové podobě (ve formě příkazů SQL)
  2. V určitém okamžiku by se výpis přerušil kvůli chybě, ale textový soubor by byl stále uložen na disku
  3. Podíváme se na konec textového souboru, a tím najdeme identifikátor (id) posledního řádku, který byl úspěšně odstraněn

Začal jsem brát výpis v textové podobě:

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

Výpis byl podle očekávání přerušen se stejnou chybou:

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

Procházím ocas Podíval jsem se na konec skládky (ocas -5 ./my_dump.dump) zjistil, že výpis byl přerušen na lince s id 186 525. "Problém je tedy v řádku s id 186 526, je nefunkční a musí být smazán!" - Myslel jsem. Ale položení dotazu do databáze:
«vyberte * z ws_log_smevlog, kde id=186529„Ukázalo se, že s touto linkou je vše v pořádku... Bez problémů fungovaly i řádky s indexy 186 530 - 186 540. Další „geniální nápad“ selhal. Později jsem pochopil, proč se to stalo: při mazání a změně dat z tabulky nejsou fyzicky odstraněna, ale jsou označena jako „mrtvé n-tice“, pak přichází autovakuum a označí tyto řádky jako smazané a umožní tyto řádky znovu použít. Pro pochopení, pokud se data v tabulce změní a je povoleno autovakuum, pak se neukládají sekvenčně.

Pokus 5: SELECT, FROM, WHERE id=

Neúspěchy nás dělají silnějšími. Nikdy byste se neměli vzdávat, musíte jít až do konce a věřit v sebe a své schopnosti. Rozhodl jsem se tedy zkusit jinou možnost: jednoduše procházet všechny záznamy v databázi jeden po druhém. Když známe strukturu mé tabulky (viz výše), máme pole id, které je jedinečné (primární klíč). V tabulce máme 1 628 991 řádků a id jsou v pořádku, což znamená, že je můžeme projít jeden po druhém:

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

Pokud někdo nerozumí, příkaz funguje následovně: prohledá tabulku řádek po řádku a odešle stdout na / dev / null, ale pokud příkaz SELECT selže, vytiskne se text chyby (stderr se odešle do konzole) a vypíše se řádek obsahující chybu (díky ||, což znamená, že select měl problémy (návratový kód příkazu není 0)).

Měl jsem štěstí, nechal jsem si na hřišti vytvořit indexy id:

Moje první zkušenost s obnovou databáze Postgres po selhání (neplatná stránka v bloku 4123007 relatton base/16490)

To znamená, že nalezení řádku s požadovaným ID by nemělo trvat dlouho. Teoreticky by to mělo fungovat. No, spustíme příkaz tmux a jdeme spát.

Do rána jsem zjistil, že bylo zhlédnuto asi 90 000 záznamů, což je něco málo přes 5 %. Vynikající výsledek ve srovnání s předchozí metodou (2%)! Ale nechtělo se mi čekat 20 dní...

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

Zákazník měl vynikající server vyhrazený pro databázi: dvouprocesorový Intel Xeon E5-2697 v2, v naší lokalitě bylo až 48 vláken! Zatížení serveru bylo průměrné, bez problémů jsme mohli stáhnout cca 20 vláken. RAM bylo také dost: celých 384 gigabajtů!

Proto bylo potřeba příkaz paralelizovat:

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

Zde bylo možné napsat krásný a elegantní skript, ale zvolil jsem nejrychlejší metodu paralelizace: ručně rozdělit rozsah 0-1628991 do intervalů po 100 000 záznamech a spustit samostatně 16 příkazů formuláře:

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

Ale to není vše. Teoreticky připojení k databázi také vyžaduje určitý čas a systémové prostředky. Připojit 1 628 991 nebylo moc chytré, budete souhlasit. Proto načtěte 1000 řádků místo jednoho na jedno připojení. V důsledku toho se tým transformoval do tohoto:

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

Otevřete 16 oken v relaci tmux a spusťte pří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 den později jsem obdržel první výsledky! Konkrétně (hodnoty XXX a ZZZ již nejsou zachovány):

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 tři řádky obsahují chybu. ID prvního a druhého záznamu problému bylo mezi 829 000 a 830 000, ID třetího bylo mezi 146 000 a 147 000. Dále jsme jednoduše museli najít přesnou hodnotu id záznamů problému. Za tímto účelem procházíme náš rozsah s problematickými záznamy s krokem 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ý konec

Našli jsme problematické linky. Jdeme do databáze přes psql a pokusíme se je smazat:

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

K mému překvapení byly záznamy bez problémů smazány i bez volby nula_poškozených_stránek.

Pak jsem se připojil k databázi, udělal VAKUUM PLNÝ (Myslím, že to nebylo nutné dělat) a nakonec jsem úspěšně odstranil zálohu pomocí pg_dump. Skládka proběhla bez chyb! Problém byl vyřešen takovým hloupým způsobem. Radost neznala mezí, po tolika nezdarech se podařilo najít řešení!

Poděkování a závěr

Tak dopadla moje první zkušenost s obnovou skutečné databáze Postgres. Na tento zážitek budu dlouho vzpomínat.

A nakonec bych rád poděkoval PostgresPro za překlad dokumentace do ruštiny a pro zcela bezplatné online kurzy, což velmi pomohlo při analýze problému.

Zdroj: www.habr.com

Přidat komentář