Моето прво искуство со враќање на базата на податоци на Postgres по неуспех (неважечка страница во блок 4123007 од relatton base/16490)

Би сакал да го споделам со вас моето прво успешно искуство за враќање на базата на податоци на Postgres на целосна функционалност. Се запознав со Postgres DBMS пред половина година, претходно немав искуство во администрација на бази на податоци.

Моето прво искуство со враќање на базата на податоци на Postgres по неуспех (неважечка страница во блок 4123007 од relatton base/16490)

Работам како полу-DevOps инженер во голема ИТ компанија. Нашата компанија развива софтвер за услуги со големо оптоварување, а јас сум одговорен за перформансите, одржувањето и распоредувањето. Ми беше дадена стандардна задача: да ажурирам апликација на еден сервер. Апликацијата е напишана во 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

Бубачки "неважечка страница во блок" зборува за проблеми на ниво на датотечен систем, што е многу лошо. На различни форуми беше предложено да се направи ПОЛН ВАКУУМ со опција нула_оштетени_страници да се реши овој проблем. Па, да се обидеме...

Подготовка за закрепнување

ВНИМАНИЕ Не заборавајте да направите резервна копија на Postgres пред секој обид да ја вратите вашата база на податоци. Ако имате виртуелна машина, запрете ја базата на податоци и направете слика. Ако не е можно да се направи слика, запрете ја базата на податоци и копирајте ја содржината на директориумот Postgres (вклучувајќи ги и датотеките вол) на безбедно место. Главната работа во нашиот бизнис е да не ги влошуваме работите. Прочитајте овој.

Бидејќи базата на податоци генерално работеше за мене, се ограничив на редовна депонија на базата на податоци, но ја исклучив табелата со оштетени податоци (опција -T, --exclude-table=ТАБЕЛА во pg_dump).

Серверот беше физички, беше невозможно да се направи снимка. Бекапот е отстранет, ајде да продолжиме понатаму.

Проверка на датотечен систем

Пред да се обидеме да ја вратиме базата на податоци, треба да се увериме дека сè е во ред со самиот датотечен систем. А во случај на грешки поправете ги, бидејќи во спротивно можете само да ги влошите работите.

Во мојот случај, датотечниот систем со базата на податоци беше монтиран "/srv" а типот бил ext4.

Запирање на базата на податоци: systemctl стоп [заштитена по е-пошта] и проверете дали датотечниот систем не го користи никого и може да се демонтира со помош на командата лсоф:
lsof +D /srv

Исто така, морав да ја прекинам базата на податоци на redis, бидејќи таа исто така користеше "/srv". Следно се демонтирав / srv (количина).

Датотечниот систем беше проверен со помош на алатката 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: нула_оштетени_страници

Се поврзуваме со базата на податоци преку psql со сметка која има права на суперкорисници. Ни треба суперкорисник, бидејќи... опција нула_оштетени_страници само тој може да се промени. Во мојот случај тоа е postgres:

psql -h 127.0.0.1 -U postgres -s [име на база на податоци]

Опција нула_оштетени_страници потребни за да се игнорираат грешките во читањето (од веб-страницата 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_тост – механизам за складирање на „долги податоци“ во Poetgres доколку не се вклопуваат на една страница (стандардно 8 kb).

Обид 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 дена. Уште еден целосен неуспех.

Но, јас не се откажав. Се прашував зошто скенирањето траеше толку долго. Од документацијата (повторно на постгреспро) дознав:

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

Кога користите LIMIT, важно е да се користи и клаузула ORDER BY, така што редовите со резултат се враќаат по одреден редослед. Во спротивно, ќе се вратат непредвидливите подмножества на редови.

Очигледно, горната команда беше погрешна: прво, немаше нарачајте од, резултатот може да биде погрешен. Второ, Postgres прво мораше да ги скенира и прескокне OFFSET редовите, а со зголемување Офсет продуктивноста би се намалила уште повеќе.

Обид 4: земете депонија во текстуална форма

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

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

Моето прво искуство со враќање на базата на податоци на Postgres по неуспех (неважечка страница во блок 4123007 од relatton base/16490)

Во нашиот случај имаме колона „ИД“, кој го содржеше единствениот идентификатор (бројач) на редот. Планот беше вака:

  1. Почнуваме да земаме депонија во текстуална форма (во форма на команди sql)
  2. Во одреден момент, депонијата би била прекината поради грешка, но текстуалната датотека сепак би била зачувана на дискот
  3. Го гледаме крајот на текстуалната датотека, со што го наоѓаме идентификаторот (ид) на последната линија што беше успешно отстранета

Почнав да земам депонија во текстуална форма:

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 редови во табелата и 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 на / dev / null, но ако командата SELECT не успее, тогаш се печати текстот на грешката (stderr се испраќа во конзолата) и се печати линија која ја содржи грешката (благодарение на ||, што значи дека селектираниот имал проблеми (повратниот код на командата не е 0)).

Имав среќа, имав индекси креирани на теренот id:

Моето прво искуство со враќање на базата на податоци на Postgres по неуспех (неважечка страница во блок 4123007 од relatton base/16490)

Ова значи дека наоѓањето линија со саканиот ид не треба да одземе многу време. Во теорија, тоа треба да функционира. Па, ајде да ја извршиме командата внатре tmux и да одиме во кревет.

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

Обид 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 команди од формата:

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 редови наместо конекција еден на еден. Како резултат на тоа, тимот се трансформираше во ова:

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 и го идентификуваме 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

Среќен крај

Ги најдовме проблематичните линии. Влегуваме во базата на податоци преку 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

На мое изненадување, записите беа избришани без никакви проблеми дури и без опцијата нула_оштетени_страници.

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

Признанија и заклучок

Вака испадна моето прво искуство за враќање на вистинска база на податоци на Postgres. Ова искуство ќе го паметам долго време.

И, конечно, би сакал да му се заблагодарам на PostgresPro за преведувањето на документацијата на руски и за потполно бесплатни онлајн курсеви, што многу помогна при анализата на проблемот.

Извор: www.habr.com

Додадете коментар