Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

Искам да споделя с вас първия си успешен опит за възстановяване на пълната работоспособност на база данни Postgres. Запознах се с СУБД Postgres преди шест месеца, а преди това нямам никакъв опит с администриране на бази данни.

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

Работя като полу-DevOps инженер в голяма IT компания. Нашата компания се занимава с разработка на софтуер за високонагрени услуги, а аз отговарям за работоспособността, поддръжката и деплоя. Поставена ми беше стандартна задача: да обновя приложението на един сървър. Приложението е написано на Django, по време на обновлението се извършват миграции (промяна на структурата на базата данни), и преди този процес правим пълен дамп на базата данни чрез стандартната програма pg_dump за всеки случай.

По време на генерирането на дампа възникна непредвидена грешка (версия Postgres – 9.5):

pg_dump: Копирането на съдържанието на таблица “ws_log_smevlog” е неуспешно: PQgetResult() неуспешно.
pg_dump: Съобщение за грешка от сървъра: ГРЕШКА: невалидна страница в блок 4123007 на база от данни base/16490/21396989
pg_dump: Командата беше: COPY public.ws_log_smevlog [...]
pg_dump: [паралелен архиватор] един работен процес приключи неочаквано

Грешка «невалидна страница в блок» показва проблеми на ниво файлова система, което е много лошо. На различни форуми предлагат да се направи FULL VACUUM с опцията zero_damaged_pages за решаване на този проблем. Е, да пробваме…

Подготовка за възстановяване

ВНИМАНИЕ! Задължително направете резервно копие на Postgres преди всяка опит за възстановяване на базата данни. Ако имате виртуална машина, спрете базата данни и направете snapshot. Ако нямате възможност да направите snapshot, спрете базата и копирайте съдържанието на директорията Postgres (включително wal файловете) на безопасно място. Най-важното в нашата работа е да не причиняваме допълнителни щети. Прочетете това.

Понеже общо взето базата ми работеше, се ограничих до стандартен дамп на базата данни, но изключих таблицата с повредени данни (опция -T, —exclude-table=TABLE в pg_dump).

Сървърът беше физически, не можеше да се направи snapshot. Резервното копие е направено, продължаваме напред.

Проверка на файлова система

Преди опита за възстановяване на базата данни е необходимо да се уверим, че всичко е наред с самата файлова система. И в случай на грешки да ги поправим, защото в противен случай ще можем само да влошим ситуацията.

В моя случай файлова система с базата данни беше монтирана в «/srv» и типът беше ext4.

Спираме базата данни: systemctl stop postgresql@9.5-main.service и проверявме, че файловата система не се използва и може да бъде демонтирана с помощта на командата lsof:
lsof +D /srv

Трябваше също да спра базата данни redis, тъй като тя също я използваше «/srv». След това я демонтирах /srv (umount).

Проверка на файловата система беше извършена с помощта на утилитата e2fsck с ключа -f (Force checking even if filesystem is marked clean):

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

След това с помощта на утилитата dumpe2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | grep checked) можем да се уверим, че проверката действително е извършена:

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

e2fsck казва, че проблеми на нивото на файловата система ext4 не са открити, а това означава, че можем да продължим опитите за възстановяване на базата данни, а по-точно да се върнем към vacuum full (разбира се, е необходимо да монтираме файловата система обратно и да стартираме базата данни).

Ако имате физически сървър, задължително проверете състоянието на дисковете (чрез smartctl -a /dev/XXX) или RAID контролера, за да се уверите, че проблемът не е на хардуерно ниво. В моя случай RAID беше "железен", затова помолих местния администратор да провери състоянието на RAID (сървърът беше на няколко стотин километра от мен). Той каза, че няма грешки, а това означава, че със сигурност можем да започнем възстановяването.

Опит 1: zero_damaged_pages

Свързваме се с базата чрез psql с акаунт, който има права на суперпотребител. Нужен ни е точно суперпотребителят, тъй като опцията zero_damaged_pages може да променя само той. В моя случай това е postgres:

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

Опция zero_damaged_pages е необходима, за да игнорираме грешките при четене (от сайта postgrespro):

При откриване на повреден заглавен блок Postgres Pro обикновено съобщава за грешка и прекратява текущата транзакция. Ако параметърът zero_damaged_pages е включен, вместо това системата издава предупреждение, нулира повредената страница в паметта и продължава обработката. Това поведение унищожава данни, а именно всички редове в повредената страница.

Включваме опцията и пробваме да направим full vacuum на таблицата:

VACUUM FULL VERBOSE

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)
За съжаление, неуспех.

Срещнахме подобна грешка:

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 – механизъм за съхранение на "дълги данни" в Postgres, ако те не могат да се поберат в една страница (по подразбиране 8kb).

Опит 2: reindex

Първият съвет от Гугъл не помогна. След няколко минути търсене намерих втория съвет – да направя reindex повредената таблица. Срещал съм този съвет на много места, но не вдъхваше доверие. Нека направим reindex:

reindex table ws_log_smevlog

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

reindex приключи без проблеми.

Но това не помогна, VACUUM FULL възникваше аварийно спиране с подобна грешка. Тъй като съм свикнал с неуспехите, започнах да търся съвети в интернет и попаднах на доста интересна статия.

Опит 3: SELECT, LIMIT, OFFSET

Статията по-горе предлага да се разгледа таблицата ред по ред и да се премахнат проблемните данни. Първо трябваше да прегледам всичките редове:

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

В моя случай таблицата съдържаше 1 628 991 реда! Нормално би било да се погрижа за партиционирането на данните, но това е тема за отделно обсъждане. Беше събота, стартирах тази команда в tmux и отидох да спя:

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

На сутринта реших да проверя как стоят нещата. К мое учудване, открих, че за 20 часа са сканирани само 2% от данните! Не исках да чакам 50 дни. Още един пълен провал.

Но не се предадох. Интересуваше ме защо сканирането отне толкова време. От документацията (отново на postgrespro) научих:

OFFSET указва да се пропуснат посочения брой редове, преди да започне да се издават редове.
Ако са посочени и OFFSET, и LIMIT, система първо пропуска OFFSET редове, а след това започва да брои редовете за ограничението LIMIT.

Прилагането на LIMIT е важно да се използва и клаузата ORDER BY, за да се върнат редовете в определен ред. В противен случай ще се връщат непредсказуеми подмножества от редове.

Очевидно, че написаната команда беше погрешна: първо, нямаше order by, резултатът можеше да бъде грешен. На второ място, Postgres първо трябваше да сканира и пропусне OFFSET-редовете, и с нарастващото CHECKSUM_AGG производителността щеше да намалява.

Опит 4: да се направи дамп в текстов вид

След това ми дойде наум, изглеждаща гениална идея: да направя дамп в текстов вид и да анализирам последния записан ред.

Но първо, нека се запознаем със структурата на таблицата ws_log_smevlog:

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

В нашия случай имаме колона «id», която съдържаше уникален идентификатор (брояч) на реда. Планът беше следният:

  1. Започваме да правим дамп в текстов вид (под формата на sql команди)
  2. В определен момент на изтеглянето на дампа, процесът може да се прекъсне поради грешка, но текстовият файл все пак ще бъде запазен на диска.
  3. Гледаме края на текстовия файл, така намираме идентификатора (id) на последния успешно изтеглен ред.

Започнах да извличам дампа в текстов формат:

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

Извличането на дампа, както се очакваше, се прекъсна с същата грешка:

pg_dump: Съобщение за грешка от сървъра: ERROR: невалидна страница в блок 4123007 от базата данни /16490/21396989

След това, чрез tail прегледах края на дампа (tail -5 ./my_dump.dump) открих, че дампът се е прекъснал на реда с id 186 525. "Значи, проблемът е в реда с id 186 526, той е повреден и трябва да бъде премахнат!" – pomислих си. Но когато извърших заявка в базата данни:
«select * from ws_log_smevlog where id=186529" се оказа, че с този ред всичко е наред... Редовете с индекси 186 530 — 186 540 също работеха без проблеми. Още една "гениална идея" се провали. По-късно разбрах защо се случи така: при изтриването или промените в данните от таблицата, те не се изтриват физически, а се маркират като "мъртви кортежи", след което идва autovacuum и маркира тези редове за изтриване и позволява повторната им употреба. За разбиране, ако данните в таблицата се променят и autovacuum е включен, те не се съхраняват последователно.

Опит 5: SELECT, FROM, WHERE id=

Провалите ни правят по-силни. Никога не бива да се отказваме, нужно е да се борим до края и да вярваме в себе си и своите възможности. Затова реших да пробвам още един вариант: просто да прегледам всички записи в базата данни по един. Познавайки структурата на таблицата ми (вж. по-горе), имаме поле id, което е уникално (първичен ключ). В таблицата имаме 1 628 991 ред и ид те са подредени, а това означава, че можем просто да ги прегледаме по един:

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

Ако някой не разбира, командата работи по следния начин: преглежда таблицата ред по ред и изпраща stdout в /dev/null, но ако командата SELECT се провали, се извежда текстът на грешката (stderr се изпраща в конзолата) и се извежда редът, съдържащ грешката (благодарение на ||, което означава, че с select е възникнала проблем (кодът за връщане на командата не е 0)).

Имах късмет, бяха създадени индекси по полето ид:

Моят първи опит за възстановяване на база данни Postgres след сбой (недопустима страница в блок 4123007 на базата данни /16490)

А това означава, че намирането на реда с нужния id не трябва да отнема много време. В теория трябва да сработи. Какво ще кажете, стартираме командата в tmux и отиваме да спим.

Сутринта открих, че са прочетени около 90 000 записа, което е малко над 5%. Отличен резултат в сравнение с предишния метод (2%)! Но не исках да чакам 20 дни…

Опит 6: SELECT, FROM, WHERE id >= and id <

За клиента беше осигурен отличен сървър за БД: двупроцесорен Intel Xeon E5-2697 v2, в нашето разположение имаше цели 48 потока! Натоварването на сървъра беше средно, без особени проблеми можехме да поемем около 20 потока. Оперативната памет също беше достатъчна: цели 384 гигабайта!

Затова екипът трябваше да бъде разпаралелен:

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

Тук можеше да се напише красив и елегантен скрипт, но избрах най-бързия начин за разпаралеляване: да разделя диапазона 0-1628991 на интервали от 100 000 записа и да стартирам отделно 16 команди от вида:

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

Но това не е всичко. В идеалния случай, свързването с базата данни също отнема известно време и системни ресурси. Свързването на 1 628 991 не беше много разумно, съгласете се. Затова нека при едно свързване извлечем 1000 реда вместо един. В крайна сметка командата стана така:

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

Отваряме 16 прозореца в сесия tmux и стартираме командите:

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

На следващия ден получих първите резултати! А именно (стойностите XXX и ZZZ вече не бяха запазени):

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

Това означава, че имаме три реда, които съдържат грешка. id на първия и втория проблемен запис са били между 829 000 и 830 000, id на третия – между 146 000 и 147 000. Следваше просто да намерим точното значение на id на проблемните записи. За целта преглеждаме нашия диапазон с проблемни записи със стъпка 1 и идентифицираме id:

for ((i=829000; 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 /dev/null || echo $i; done
829417
ERROR: unexpected chunk number ZZZ (expected 0) for toast value XXX in pg_toast_106070
146911

Щастлив финал

Намерихме проблемни редове. Влизаме в базата през psql и опитваме да ги изтрием:

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

К моето учудване, записите се изтриха без никакви проблеми дори без опцията zero_damaged_pages.

След това влязох в базата, направих VACUUM FULL (мисля, че не беше необходимо), и накрая успешно направих бекъп с помощта на pg_dump. Дампът беше направен без никакви грешки! Проблемът беше решен по този най-прост начин. Радостта беше безкрайна, след толкова много неуспехи успях да намеря решение!

Благодарности и заключение

Така че, това беше моят първи опит в възстановяването на реална база данни Postgres. Този опит ще запомня дълго.

И накрая, бих искал да благодаря на компанията PostgresPro за преведената документация на руски език и за напълно безплатните онлайн курсове, които много ми помогнаха по време на анализа на проблема.

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster