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

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

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

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

Възникна неочаквана грешка при вземане на дъмп (Postgres версия 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

Дървеница "невалидна страница в блок" говори за проблеми на ниво файлова система, което е много лошо. На различни форуми беше предложено да се направи ПЪЛЕН ВАКУУМ с опция zero_damaged_pages за решаване на този проблем. Е, нека опитаме...

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

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

Тъй като базата данни като цяло работеше за мен, аз се ограничих до обикновен дъмп на базата данни, но изключих таблицата с повредени данни (опция -T, --exclude-table=ТАБЛИЦА в pg_dump).

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

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

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

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

Спиране на базата данни: systemctl спиране [имейл защитен] и проверете дали файловата система не се използва от никого и може да бъде демонтирана с помощта на командата също:
lsof +D /srv

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

Файловата система беше проверена с помощта на помощната програма e2fsck с превключвателя -f (Принудителна проверка дори ако файловата система е маркирана като чиста):

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

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

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

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

Ако имате физически сървър, не забравяйте да проверите състоянието на дисковете (чрез 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 [име_на_база_данни]

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

Когато PostgreSQL открие повреден хедър на страница, той обикновено съобщава за грешка и прекъсва текущата транзакция. Ако zero_damaged_pages е разрешено, системата вместо това издава предупреждение, нулира повредената страница в паметта и продължава обработката. Това поведение унищожава данните, а именно всички редове в повредената страница.

Активираме опцията и се опитваме да направим пълен вакуум на масите:

VACUUM FULL VERBOSE

Първият ми опит с възстановяване на база данни на Postgres след повреда (невалидна страница в блок 4123007 на relatton base/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 – механизъм за съхраняване на „дълги данни“ в Poetgres, ако не се побират на една страница (8kb по подразбиране).

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

Първият съвет от Google не помогна. След няколко минути търсене открих втория съвет - да си направя преиндексиране повредена маса. Виждах този съвет на много места, но не ми вдъхваше доверие. Нека преиндексираме:

reindex table ws_log_smevlog

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

преиндексиране завършено без проблеми.

Това обаче не помогна, ВАКУУМ ПЪЛЕН се срина с подобна грешка. Тъй като съм свикнал с неуспехи, започнах да търся допълнително съвети в Интернет и попаднах на доста интересен Статия.

Опит 3: SELECT, LIMIT, OFFSET

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

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

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

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

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

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

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

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

Очевидно горната команда е грешна: първо, няма подредени по, резултатът може да е грешен. Второ, Postgres първо трябваше да сканира и пропусне OFFSET редове и с увеличаване ИЗМЕСТВАНЕ производителността ще намалее още повече.

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

Тогава ми хрумна една привидно брилянтна идея: да направя дъмп в текстова форма и да анализирам последния записан ред.

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

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

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

  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 message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989

По-нататък опашка Погледнах края на бунището (опашка -5 ./my_dump.dump) откри, че дъмпът е прекъснат на линия с id 186 525. „Така че проблемът е в линията с id 186 526, тя е повредена и трябва да бъде изтрита!“ - Мислех. Но като направите заявка към базата данни:
«изберете * от ws_log_smevlog, където id=186529„Оказа се, че с тази линия всичко е наред... Редове с индекси 186 – 530 също работеха без проблеми. Още една „брилянтна идея“ се провали. По-късно разбрах защо това се случи: когато изтривате и променяте данни от таблица, те не се изтриват физически, а се маркират като „мъртви кортежи“, след което идва автовакуум и маркира тези редове като изтрити и позволява тези редове да бъдат използвани повторно. За да разберете, ако данните в таблицата се променят и автоматичното вакуумиране е активирано, тогава те не се съхраняват последователно.

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

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

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

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

Имах късмет, имах създадени индекси на терена id:

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

Това означава, че намирането на ред с желания идентификатор не трябва да отнема много време. На теория би трябвало да работи. Е, нека стартираме командата tmux и да си лягаме.

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

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

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

Следователно командата трябваше да бъде успоредна:

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

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

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

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

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

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

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

Ден по-късно получих първите резултати! А именно (стойностите 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

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

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

Щастлив край

Открихме проблемните линии. Влизаме в базата данни чрез 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.

След това се свързах с базата данни, направих ВАКУУМ ПЪЛЕН (Мисля, че не беше необходимо да правя това) и накрая успешно премахнах архива с помощта на pg_dump. Дъмпът е взет без никакви грешки! Проблемът беше решен по такъв глупав начин. Радостта нямаше граници, след толкова много неуспехи успяхме да намерим решение!

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

Ето как се оказа първият ми опит за възстановяване на истинска база данни на Postgres. Ще помня това преживяване дълго време.

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

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

Добавяне на нов коментар