Наверное не существует в мире технической поддержки, которая бы среди прочего не занималась спасением данных клиентов. Не минула участь сия и нас в Postgres Professional. Однако особенность спасения битых данных в СУБД заключается в том, что сломаться может сразу на двух уровнях: физическом и логическом. Первое, это когда что-то случилось с физическим файлом внутри которого лежат данные, а второе это когда файл цел, но внутри него каша без смысла. Поэтому сегодня мы поговорим о том как понять что в вашей базе что-то пошло не так, выяснить почему, как оценить ущерб и минимизировать его. А в конце, бонусом, обсудим как не стать героем подобных статей. А автор этой - Камиль Каримов, старший инженер техподдержки Postgres Professional, так что тут без воды и строго по делу.
0. Не уверен - не влезай
Главное правило при попытке восстановить данные после аварии - не навреди. Будет очень обидно, если вы своими действиями только усугубите проблему и потеряете ещё больше. Поэтому, если вы не уверены в производимых пассах руками - не надо их делать. Отложите клавиатуру, отойдите от консоли и позовите старших. Возможно даже за деньги. Это будет всё ещё дешевле, чем продолжить терять данные.
И давайте договоримся - всё написанное в статье это не руководство к действию, а только лишь описание инструментов и принципов их применения. Готовых рецептов нет и быть не может. Не надо слепо идти с первого пункта и вбивать команды в консоль. Если у вас нет чёткого понимания что такое MVCC, TOAST, как устроены таблицы и так далее - лучше даже не начинайте. Если хотите метрику зрелости, то используйте наш DBA2 курс.
Также не забываем, что восстановление данных процесс не быстрый и никто вам не даст гарантий что закончится он успехом. Поэтому, пока у вас есть обоснованные сомнения в уровне своей квалификации, не надо увеличивать шанс безвозвратных потерь Приходите лучше к нам. Мы поможем.
И да, всегда делайте бекапы. Самый странный бекап всегда лучше веселой истории о склеивании базы из черепков после аварии.
Почему данные сломались?
Прежде чем начинать лечить, надо понять почему оно заболело и отвалилось. Иначе можно спутать причину со следствием и зайти в тупик.
Если совсем просто, то в мире СУБД (да и не только), мы можем остаться у разбитого корыта из-за:
Ошибок в ПО. И тут речь не только о прикладе, а о любом ПО: операционные системы, драйвера, прошивки контроллеров СХД и т.д. Баги есть везде и приводят они, в том числе, к порче данных.
Сбои оборудования. Тут все привыкли винить накопители, но как показывает практика, свою роль в увеличении степени дисперсии данных отлично играют и процессоры, и сетевые платы и что угодно из компьютерных запчастей.
Сложность современных инфраструктур, назовём это так. Бесконечные слои абстракции, перекладывание данных из одного формата в другой, кодировки, форматы записи и т.д. Всё это ведёт к тому, что в одном месте сломалось, в другом не заметили, в третьем решили что так и надо, а в итоге перестало работать у соседнего сервиса.
И завершает этот список человеческий фактор ибо как вообще без него можно? Никто его из нашей жизни не уберёт и скучать нам не даст.
Признаки повреждения данных
До того как лечить больного, надо быть уверенным в том что он заболел. Перечислить все возможные признаки того что с данными что-то случилось дело околоневозможное, поэтому пройдёмся лишь по тем, которые по нашей статистике стреляю чаще других.
Ошибки контрольной суммы при чтени блока. Самые популярные это page verification failed и calculated checksum. Каждый раз когда в страницу данных(обычно это 8 Кб) вносятся изменения и она сохраняется на диск, в её тело записывается контрольная сумма. При чтении происходит процесс верификации - она считается ещё раз и если не совпадает, то имеем эти ошибки. А постгрес останавливает все операции с данной страницей.
Ошибки с самим блоком или его заголовками. Выглядят примерно так:
invalid page in block 1989347 of relation base/16390/2901463552. То есть постгрес пытался прочитать конкретную страницу(блок данных) из файла, но по какой-то причине претерпел неудачу. Может страница была повреждена, может в ней записана некорректная структура. Причины в момент чтения не важны, главное что движок базы не понимает что делать с прочитанным.Ошибки доступа к файлу: Cannot open/read/write file/block Тут речь может идти не только о повреждении файла на уровне файловой системы, но и о том что какой-то добрый человек изменил права доступа. Всякое бывает, так что нельзя рубить с плеча. Ну и не только тут, просто тут особенно.
Невалидные статусы транзакций.
Invalid [status of] xid/mxid/xmin/xmax 123456. Посыл сообщения в том, что нарушена целостность информации о транзакциях в заголовке страницы данных или кортеже(он же строка в таблице). Хитрость в том, что речь здесь не только о случаях когда значения XID/MXID некорректны, но и содержат в себе заведомо невозможные или крайне противоречивые значения.Это приводит нас к следующему пункту - странные номера транзакций из будущего или глубокого прошлого. Тут вопрос не столько в счётчике эпох, который никто не отменял, сколько в том, когда счётчик соседних туплов вдруг начинает отличаться на порядки.
Проблемы в TOAST таблицах, обычно приводящие к Missing chunk Х for TOAST value. То есть постгрес не может найти какой-то чанк для значения хранящегося в TOAST-таблице. Чаще всего ломается сразу начиная с первого (нулевого), а без него, очевидно, никак не получится собрать всю остальную таблицу.
И последний пунктом будет всякое, что можно охарактеризовать классическим Федя, дичь!(с) То есть когда один из процессов СУБД делает что-то странное, что делать явно не должен. Например, когда CHECKPOINT идёт уже больше суток и не может ни к чему прийти. Это, кстати, не выдумка, а такое действительно видели.
На этом с описанием признаков заканчиваем и переходим уже ближе к практике.
Инструменты диагностики
Здесь надо сразу оговориться, что инструменты для диагностики в чистом виде встречаются редко. Как и для исправления ошибок. Обычно, всё же, они идут рука об руку и редко разделяются. Но для строгости повествования мы сначала поговорим про возможности диагностировать проблему, а в следующей главе уже будем пытаться её лечить. Так что не удивляйтесь обнаружив одни и те же названия в обоих разделах. Корректней будет говорить о том, в каком виде они реализованы. Я предлагаю такое разбиение:
То что идёт вместе с СУБД. Это могут быть какие-то аргументы, или режимы работы. Главное чтобы оно шло из коробки и точно всегда работало в рамках текущего инстанса.
Отдельные утилиты. С ними тоже всё понятно. Кто-то что-то написал, оно может причинить нам пользу, поэтому мы следим за совместимостью версий, их актуальностью и так далее. ну и помним, что автор нам ничего не обещал, если это опенсорс.
Сторонние инструменты, которые ни к восстановлению данных, ни к СУБД никакого отношения не имеют, но так уже получилось что люди придумали как можно их использовать во благо администраторов СУБД.
Итак, список инструментов не ранжирован ни по частоте использования, ни по внутреннему ощущению важности утилиты. Просто вот что мы используем в своей работе на стадии анализа аварий:
pg_dump Дефолт для чтения данных. Придуман для создания логических резервных копий, но нас интересует тот факт, что он просто берёт и по рабоче-крестьянски начинает читать данные из таблиц, не блокируя доступ к базе. Не понимаешь насколько всё плохо, не знаешь как вычитать какие-то конкретные штуки - он сделает всё сам, покажет место где ломается и не сделает хуже чем есть. А может даже и вычитает всю базу, всякое бывает.
Checkdb из утилиты pg_probackup. Это уже наша разработка, позволяющая проверить целостность кластера PostgreSQL без создания резервной копии. Проводит проверку данных на логическом и физическом уровнях, включая заголовки страниц, полноту индексов, правила уникальности и так далее.
Amcheck. Ничего лучше для поиска логических несоответствий пока не придумали. Проверяет целостность данных на уровне структур хранения: индексов и таблиц. Это самое важное отличие от pg_checksums, который проверяет только контрольные суммы страниц и pg_verify_checksum для отдельных страниц. Amcheck проверит не только корректность связей между страницами индекса и их целостность, но и пройдётся по ссылкам между таблицами и индексами.
pg_filedump. Утилита для работы с файлами данных в человекочитаемом виде т.е. она даёт возможность низкоуровнево анализировать содержание блоков и обнаруживать повреждения данных на физическом уровне. И, что немаловажно, можно исследовать MVCC, чтобы найти в какой момент строка была сломана и потом откатиться на здоровую версию.
Модуль PostgreSQL - pageinspect. Ещё один представитель мира низкоуровневого анализа страниц базы данных. Работает только от суперпользователя и позволяет влезать внутрь даже служебных структур (FSM, VM). Не для слабых духом и знаниями.
И закончим это список gdb в роли ультимативного оружия. Всем прекрасно известный отладчик для Unix-подобных систем, позволяющий в пошаговом режиме раскручивать даже самые сложные сценарии, но требует недюжих знаний буквально обо всём что есть в вашем сервере.
Инструменты для исправления
Когда речь заходит о попытках как-то оживить данные которые кажутся безвозвратно потерянными, есть два глобальных подхода. В одном случае мы берём оставшиеся черепки и пытаемся их как-то склеить обратно в рабочий вид, например, с помощью кодов коррекции ошибок и зашитой избыточности. А в другом мы всеми силами стараемся сохранить хоть что-то, тем самым минимизировав ущерб.
И, как я уже говорил, сейчас будут пересечения со списком выше, но ничего ужасного в этом нет.
pg_dump. В разрезе спасения информации интересен нам тем, что будет читать данные (и сохранять их в новое место) пока не упрётся в непреодолимое препятствие. Зачастую это препятствие несовпадающая контрольная сумма, из-за чего не получается прочитать большую часть базы. Поэтому нам на помощь приходит флаг –ignore_checksum_failure, чтобы один кривой блок не мешал спасти всю остальную базу.
Для успешных господ с Postgres Pro и активными репликами доступен параметр page_repair, позволяющий в автоматическом режиме исправлять страницы с некорректной контрольной суммой за счёт получения валидных данных с реплики. Очень полезная штука, когда есть подозрения, что СХД стремительно движется в вальхаллу, а базу просто так не выключить.
А в ванильной версии есть достаточно грязных хак, позволяющий продолжать выполнение запроса при обнаружении повреждённой страницы - параметр zero_damaged_pages. Как можно догадаться из названия, при обнаружении повреждённой страницы он просто её обнуляет. Но только в памяти, ничего не сохраняя на диск. Незаменимая вещь при аварийном восстановлении, когда повреждены критические системные таблицы, сервер не запускается и все остальные методы уже использованы. Лучше уж так, чем вообще никак.
dd. Как-то даже странно что-то пытаться объяснить про живую классику. Побайтово читает данные с диска пока они читаются. Всё. Ничего больше. Если он не смог вычитать данные, значит пиши пропало. Вероятно сервер горит, а диск расплавился.
И закончим мы модулем pg_surgery, который также может помочь с низкоуровневым восстановлением данных, когда другие методы оказались бессильны. Но с ним надо быть супер аккуратным, потому что это тот самый случай, когда очень легко сломать ещё больше чем было довмешательства. Вплоть до укладывания сервера в кору, например, сделав видимым кортеж, которым таковым быть не должен.
Что делать если мы поняли что данные побились. Общая методология
Мы уже договаривались раньше, но я напоминаю - это не пошаговая инструкция на все случаи жизни. Это просто общий подход к тому, как можно попытаться восстановить свою базу данных. Поэтому не надо слепо делать всё тут описанное. Лучше приходите к нам за помощью.
Итак, погнали.
0. Золотое правило при любой аварии с данными - сними нагрузку с прода и убери руки от консоли. Надо успокоиться и работать только с холодной головой. К сожалению в мире СУБД редко когда можно вот просто так отключить её от мира и потом ковыряться в поисках причин. Однако, чем меньше будет изменений, тем больше шансы на спасение данных. Поэтому, если нельзя снять нагрузку совсем, отключаем хотя бы запись.
Восстанавливаем картину инцидента. То есть понимаем последовательность действий или событий, которые привели нас в текущую точку бытия. Без этого не получится перейти в следующий пункт.
Тут мы будем искать основную причину. С чего всё началось и что послужило тригером аварии. Это критически важная часть, чтобы минимизировать ущерб и бороться не с симптомами, а прижечь болезнь на корню. Например, чтобы не постоянно восстанавливать регулярно появляющиеся битые файлы, а вылечить сбой в дисковой подсистеме и остановить этот поток веселья.
Определить все повреждения, чтобы понять масштаб бедствия и принять решение о судьбе данных. А ещё, что немаловажно, купировать возможные проблемы в будущем. Например, в случае когда мы не смогли диагностировать битую страницу данных к которой редко обращаются, это приведёт к проблемам в произвольный момент времени где-то потом, а момент где её можно было восстановить уже упущен.
Собственно, как нам выполнить первые два пункта? Конечно, логи, что вы ещё ожидали? Точка входа для нас всегда одна - лог самого постгреса postgresql.log Альфа и омега любого расследования без которой всё остальное теряет смысл. Дальше уже начинаем действовать по обстановке, ибо подсистема логирования в линуксах широка, глубока и копаться в ней можно долго. Но если обобщать, то смело идём в dmesg посмотреть как себя чувствует ядро системы, а следом открываем journalctl. У вас же systemd, да? Правда? И только потом уже начинается хождение по сторонам, вроде чтения логов СХД и так далее.
Бывает даже что приходится анализировать WAL файлы, потому что порча данных может произойти из-за бага в самой СУБД, а нам надо найти конкретное действие и конкретную транзакцию, которые привели к аварии.
После первичной диагностики переходим к третьему пункту и ищем конкретные места с испорченными данными. Ничего лучше чем просто вычитать таблицу здесь ещё не придумали. Поэтому берём стандартный pg_dump и и запускаем выгрузку таблицы. Причём не надо мучать лишний раз диски, просто перенаправляем вывод в /dev/null. Это здорово ускорит выполнение, а мы достаточно быстро узнаем её состояние
И тут ещё здорово помогает функция со звучным названием molotilka, основанная на расширении pageinspect. Она последовательно сканирует таблицы и ищет битые блоки и туплы, с указанием на номер конкретного блока или ctid.
Возникает вопрос: а чем здесь плох SELECT и ему подобные plsql запросы? В общем случае ничем и вполне можно воспользоваться ими, однако есть неприятный нюанс возникающий на больших таблицах содержащих ошибки. SELECT направляет свой выбор в stdout и если вы читаете таблицу, ну скажем, в терабайт размером, скорее всего это закончится приходом oom killer и падением СУБД. А оно вам точно не надо, в условиях когда она может не запуститься обратно. А ещё он всегда читает системные индексы, что тоже может быть лишним.
Для всех желающих, код молотилки достаточно прозрачен:
CREATE or REPLACE FUNCTION molotilka(tbl regclass, start_page bigint) RETURNS bigint AS $$ DECLARE n_pages integer; page integer; lps_in_page integer; lp_p integer; err_count bigint = 0; BEGIN SELECT pg_relation_size(tbl) / 8192 INTO n_pages; FOR page IN start_page .. n_pages-1 LOOP BEGIN SELECT coalesce(max(lp),0) from heap_page_items(get_raw_page(tbl::text, page)) INTO lps_in_page; FOR lp_p IN 1 .. lps_in_page LOOP BEGIN EXECUTE format('select row(t.*) from %s as t where ctid=''(%s,%s)''', tbl::text, page, lp_p); EXCEPTION WHEN OTHERS THEN err_count = err_count + 1; RAISE NOTICE 'TUPLE ERROR: ctid=(%,%), SQLSTATE=% DETAIL=%', page, lp_p, SQLSTATE, SQLERRM; END; END LOOP; EXCEPTION WHEN OTHERS THEN err_count = err_count + 1; RAISE NOTICE 'PAGE ERROR: page=%, SQLSTATE=%, DETAIL=%', page, SQLSTATE, SQLERRM; END; END LOOP; RETURN err_count; END $$ language plpgsql;
По её завершению мы получаем примерно такой вывод:
test=# select molotilka('corrupt_table_name', 0); WARNING: page verification failed, calculated checksum 35306 but expected 32375 NOTICE: PAGE ERROR: page=44934781, SQLSTATE=XX001, DETAIL=invalid page in block 44934781 of relation base/16390/2872513435 molotilka ----------- 2 (1 row)
Здесь мы видим что у нас проблема с контрольной суммой и недоступен блок 44934781. То есть к нему нельзя получить доступ, что вызвало проблемы работы с таблицей.
Что можно в этом случае сделать? Думаю согласитесь, что хочется всё же попробовать заглянуть в этот блок. Вдруг туплы в нём живые и их можно спасти. Для этого используем pg_filedump:
pg_filedump-ent-16 -D int,text -i /path/of/data/file
Ключ -D указывает тип данных для вывода, которые мы получим в том порядке, как они идут в таблице. Ну и в нашем примере получаем вывод:
COPY: 1 test Item 2 -- Length: 33 Offset: 8088 (0x1f98) Flags: NORMAL XMIN: 767 XMAX: 0 CID|XVAC: 0 Block Id: 0 linp Index: 2 Attributes: 2 Size: 24 infomask: 0x0902 (HASVARWIDTH|XMIN_COMMITTED|XMAX_INVALID)
Тут важна строчка: COPY: 1 test Мы видим, что строка жива, но из за побитых метаданных страницы не можем получить доступ к ней из СУБД, но filedump дает возможность прочитать строку и понять ее содержимое.
В конце этого этапа главное для нас это понять сколько данных было повреждено. Причём, в конкретных измеряемых величинах: количество таблиц и сколько места они занимают. А после уже дать свою оценку по эфимерной метрике Насколько всё плохо ака Общий масштаб бедствия. А то может уже проще бежать в сторону канадской границы, чем разгребать это пепелище.
Морально готовимся к восстановлению
Когда мы поняли сколько данных потеряли, из-за чего и как не допустить ещё больших потерь, мы готовы к восстановительным работам. Если вы уже отключили клиентов от поврёждённой базы (или хотя бы сняли всю нагрузку на запись), ваш первый шаг это попытаться зафиксировать текущий статус. То есть сделать бекап. Тут все средства хороши, но чем более низкоуровневое средство вам доступно, тем лучше.
Бекап на логическом уровне - отличный выбор.
Бекап на физическом уровне - ещё лучше.
Снапшот файловой системы и вычитывание данных на холодную - просто супер.
Бекап из снапшота виртуальной машины или снапшота СХД - сегодня однозначно ваш день. Пусть даже и начался он не слишком весело.
В любом случае, после того как вы сохранили всё что можно сохранить и отложили это в сторонку, надо с холодной головой и максимально прагматично оценить уровень критичности повреждённых данных и сколько времени займут доступные варианты восстановления. Обычно на этом этапе привлекаются руководители бизнеса, потому что общая картина работы всей компании есть только у них, и только им решать что важно, что нет и сколько на что ресурсов тратить. Может оказаться что утерянные таблицы никому особенно не нужны и можно спокойно ждать неделю пока привезут бекап на LTO из удалённого хранилища. А возможно надо срочно поднимать копию инфраструктуры и всеми средствами разворачивать бекап, а не пытаться спасти побитые строки. В общем, тут надо принимать решение всем миром.
И совет на все времена, который ещё никому не повредил - заниматься восстановлением всегда надо на заведомо исправном оборудовании. То есть, в случае когда мы решили восстанавливаться из бекапа, делать это лучше на новом сервере, а не играть в угадайку Вылетит или нет ещё один диск на старом.
Восстанавливаемся
Поскольку это статья не про бекапы, хотя мы помним что самый худой бекап всегда лучше самой весёлой истории о восстановлении, представим что их у нас нет и что-то надо делать.
Как я уже говорил выше, если данные ещё возможно вычитать, значит этим и надо заниматься. А ничего лучше pg_dump тут не придумали. Но, поскольку речь у нас про аварии, то вероятнее всего мы получим ошибку контрольной суммы. Её мы можем обойти с помощью параметра ignore_checksum_failure и вычитать данные как есть. Если у нас ошибка в одном блоке терабайтной таблицы, то лучше пропустить один блок, чем потерять всю таблицу. Но тут каждый принимает решение для себя сам. Вдруг именно в том блоке лежит циферка вашего банковского баланса.
Но проблема может быть серьёзней и с конкретной строкой, как, например, вот здесь:
ERROR: could not access status of transaction 555 Detail: Could not open file "pg_subtrans/000000000C43": No such file or directory. Context: while locking tuple (1,5) in relation "table"
То есть мы не можем получить статус транзакции из файла pg_subtrans потому что такого файла банально нет. Что в этом случае можно попытаться сделать?
Первым делом стоит попробовать сделать fake-update:
fake-update: # update test_table set id = id
Или удалить запись по ctid:
# delete test_table where ctid = (1,5)::tid;
Последнее далеко не всегда работает, но попробовать стоит. Тем более, что разработчики PostgreSQL о нас позаботились и в версии 14 появился модуль pg_surgery. Это набор функций для работы с повреждёнными данными. Их всего две. Функция heap_forсe_kill помечающая строку как мёртвую. И heap_force_freeze помечающая строку как замороженную. Обе ничего не делают с самими данными, поэтому экспериментировать можно достаточно безопасно. После чего можно пробовать получить доступ к данным нужного тупла.
Ещё бывает так, что в блоке обнаруживается недопустимая страница и мы получаем сообщение вида: invalid page in block 12345 of relation base/16390/12345678. Здесь у нас опять два выхода из ситуации. Если есть активная реплика, то запускаем page_repair и если в реплику не уехали битые данные, то наслаждаемся результатом. Если же реплики нет, то пробуем через pg_filedump вычитать данные из файла.
После того как мы попробовали все известные нам инструменты и не смогли ничего прочитать, вывод один - блок надо признать потерянным и занулить. Делаем это или через понятный всем dd, внимательно следя за тем куда мы пишем или через zero_damaged_pages, который точно не затрёт лишнего. С другой стороны, dd это как скальпель, в том плане, что мы затираем только то, что сказали.
Как это выглядит на практике. Предположим что с molotilka выдала нам следующее сообщение: NOTICE: PAGE ERROR: page=53890, SQLSTATE=22004 Значит нам надо запустить dd со следующими аргументами:
dd bs=8192 count=1 seek=53890 of=/d01/pgdata/base/16943/20335864 if=/dev/zero conv=notrunc
Мы указываем дефолтный размер блока применяемый в постгресе - bs=8192, сколько блоков надо затереть, номер самого блока и notrunc чтобы не затереть все блоки до нужного. И не забываем /dev/zero в качестве источника нулей.
Но это работает только в случае работы с обычными страницами. Если используется сжатый TABLESPACE и CFS, то выгрузку данных придётся делать примерно такой командой, через выгрузку диапазонов.
select * from test_table where ctid < '(53891,0)' and ctid >= '(53892,0)';
Очевидно что это крайне неудобно и хочется разработать утилиту позволяющую распаковывать CFS блоки и доставать из них данные.
А если сервер PostgreSQL лежит?
Выше мы рассмотрели варианты спасения данных, которые невозможны без работающего сервера постгреса. А что делать если он упал и отказывается стартовать?
Частенько этому препятствуют какие-то беды с WAL файлами. Может они побились, может пропали - не суть проблемы. Главное что тут нет никакой консольной магии со сторонними утилитами. У нас или есть архив WAL файлов, откуда мы их перезаливаем на сервер, или мы принимаем неизбежное и запускаемся с pg_resetwal. Хотя многие специалисты обоснованно считают что её никогда нельзя использовать, но как средство последней надежды - можно. И только так! Это очень хороший и интересный инструмент с одним очень важным нюансом - СУБД запустится гарантированно в неконсистентном состоянии. То есть нам в любом случае надо будет инициализировать новый кластер и через pg_dump переливать в него данные. Самые смелые документацию не читают и так не делают, но через какое время возвращаются с грустными лицами. Поэтому просто поверьте что так делать никогда нельзя.
Хорошо, с WAL разобрались, поэтому теперь рассмотрим один из самых плохих случаев - повреждение системного каталога. Хуже этого только если в сервер ударила молния или в датацентре случился пожар. Если у вас есть реплика, то поздравляю, вы только что окупили все затраты на её создание и поддержание в рабочем виде. Поэтому не забудьте об этом доложить руководству во время следующего этапа бюджетирования. Приступайте к переливке основных таблицы, но только при условии если не было DDL операций и они идентичны.
Если реплики нет, то вам доступен ровно один вариант - пробуем запуститься без чтения системных индексов. Если запустились, то считаем что вы отделались лёгким испугом. Дайте серверу подумать, подключайтесь и переливайте таблицы в новый кластер. Кстати, бывает даже такое, что системный каталог повреждён, но к базе удаётся подключиться. В этом случае бросайте всё и начинаете по максимум выгружать пользовательские таблицы. Скорее всего больше у вас такого шанса не будет.
Если без системных индексов запустить сервер не получилось - это конечная. База данных утеряна и тут ничего не поделать.
Весёлые истории экран покажет наш
Методология это всё хорошо, но теория без практики мертва. Поэтому было решено показать вам несколько реальных историй из прода, чтобы просто закрепить полученные знания.
История пятничная, обыкновенная
В какую-то из пятниц, когда все инженеры уже видели как заканчивается их смена и предвкушали побег из офиса, на голову мне сваливается клиент с высоким приоритетом и ошибками вида:
ERROR: invalid page in block 2270062 of relation base/16620/19926 ERROR: invalid page in block 2270071 of relation base/16620/19926
То есть, как будто бы у него в прямом эфире сыпятся диски на сервере с БД и надо срочно всё эвакурировать. Но при первичном анализе обстановка оказалась интересней и тут надо сказать большое спасибо админу на стороне клиента, который чётко описал что они делали перед тем как заметить эти ошибки. Он описал масштаб проблемы, конфигурацию системы и, что заслуживает отдельных аплодисментов, оперативно предоставил всю необходимую диагностическую информацию Такие спецы однозначно заслуживают плюсика в карму и, пожалуйста, делайте всегда так же.
Итак, что же там стряслось? Оказалось, что они перевозили сервера между разными системами виртуализации, а на логическом уровне использовался стандартный набор мастер + реплика. Ну и в процессе что-то пошло не так. Возникает вопрос - данные повреждены везде или только на одной машине? Достаём молотилку, применяем поочерёдно на мастер и реплику.
Вывод с мастера:
user=# select molotilka('problem_table', 0); ... WARNING: page verification failed, calculated checksum 24600 but expected 21364 NOTICE: PAGE ERROR: page=2270336, SQLSTATE=XX001, DETAIL=invalid page in block 2270336 of relation base/16620/19926 WARNING: page verification failed, calculated checksum 63683 but expected 21280 NOTICE: PAGE ERROR: page=2270337, SQLSTATE=XX001, DETAIL=invalid page in block 2270337 of relation base/16620/19926 … molotilka ----------- 246 (1 row)
Вывод с реплики:
user=# select molotilka('problem_table', 0, false); NOTICE: TABLE: problem_table PAGES: ****** CFS molotilka ----------- 0 (1 row)
Итак, на мастере повреждено 246 блока, а на реплике ничего. Значит данные на реплике живые и с ними можно работать. А для меня это значит что можно использовать page_repair. Этот параметр включает исправление страниц с использованием потоковой репликации с ведомого сервера. Да, он требует перезапуска сервера после внесения в конфиг, но кого это волнует если мы только что выяснили что у нас есть источник здоровых данных и пятница будет спасена.
История вторая, более драматичная
Клиент приходит с жалобой что сервер не поднимается после перезагрузки. Вроде ничего этакого не делали, просто нет признаков жизни со стороны СУБД. Мы, конечно, запрашиваем наш стандартный набор логов и диагностики, начинаем анализировать.тИ тут картина мира даёт трещину - судя по логам всё работает.
СООБЩЕНИЕ: Start 1 background garbage collection workers for CFS СООБЩЕНИЕ: система БД готова принимать подключения
Возвращаемся к клиенту за деталями и оказывается что он видит ошибки входа в БД. Что делает ситуацию ещё более странной, так как мы ничего похожего не видим. Максимум это ошибки вида:
XX001: ERROR: missing chunk number 0 for toast value 2683063030 in pg_toast_2619
В воздухе повисает немой вопрос, но клиент продолжает настаивать что видит ошибки входа в БД. Ок, просим показать что же он видит. На что получаем дословный ответ: «Так вот же она: missing chunk number 0 for toast value 2683063030 in pg_toast_2619» Ну как-бы всё понятно, это ошибка, да, но не так чтобы очень похожая на ошибку входа в БД.
Ок, начинаем разбираться. Выясняется, что на их стороне просто есть некое приложение которое работает с базой. Оно что-то там пытается сделать, терпит фиаско и выдаёт единственную предусмотренную разработчиками ошибку о том, что невозможно подключиться к БД. У вас там данные как-бы потерялись, но кого это волнует вообще. Поэтому когда всё встало на свои места, был произведен ресёрч и выяснилось что стрельнул известный баг, к которому есть понятный workaround, с которым можно дожить до сервисного окна чтобы накатить патч.
Что было сделано:
Запустили molotilka по проблемной таблице
Удалили битые строки по их ctid
Успех
Можно расходиться казалось-бы нам, но тут клиент возвращается и говорит что с точки зрения приложения ничего не изменилось. Подключаемся обратно, смотрим что случилось и оказывается что он просто выбрал не ту БД. Приложение подключается к другой.
Ок, всякое бывает, ничего страшного. Стрессанул человек, да и экология вокруг ни к чёрту. Меняем БД на нужную, клиент запускает создание расширения pageinspect, мы уже в предвкушении счастливого конца, но тут процесс зависает.
Хорошо так зависает. Качественно. Даже Ctrl-C его не прерывает.
В этот момент уже становится несколько тревожно. Идём смотреть что происходит на уровне системы. А там, как будто, всё хорошо. Процесс запущен, ядро процессора чем-то занято. В stat activity тоже всё красиво - выполняется создание расширения.
Может блокировки, мелькнула дерзкая идея? Но тоже нет. Всё на месте, везде где надо granted=t.
Ладно, придётся лезть процессу прямо в мозг с помощью gdb. Подключаемся и мотаем до попытки создать функцию расширения, а именно до создания функции get_raw_page:
#23 0x0000582b495bf967 in ProcessUtilitySlow (pstate=0x582b4b0ed538, pstmt=0x582b4b0ed278, queryString=0x582b4b1e7428 "/* contrib/pageinspect/pageinspect--1.5.sql */\n\n-- complain if script is sourced in psql, rather than via CREATE EXTENSION\n\n\n--\n-- get_raw_page()\n--\nCREATE FUNCTION get_raw_page(text, int4)\nRETURNS by"..., context=PROCESS_UTILITY_QUERY, params=0x0, queryEnv=0x0, qc=0x0, dest=) at ./build/../src/backend/tcop/utility.c:1768
Открываем исходники и понимаем что при создании этой функции происходят обращения к системного каталогу. И вот это уже совсем нехорошо. Помните я выше писал, что нет ничего хуже чем повреждение системного каталог? Вот это именно тот случай.
Наступает тревожный момент, когда надо принимать решение что делать дальше, потому что на дворе девять вечера, дедлайн у нас офисное утро, а на руках база которая может умереть в любой момент. Что же, ничто не ново под луной и надо спасать данные. Вместе с клиентом формируем список самых важных таблиц которые надо увозить первыми, запускаем pg_dump, он начинает как-то работать, и продолжает работать, и судя по мониторингу работает, поэтому чтобы не просверлить взглядом монитор уходим на перекур.
По возвращении оказывается что pg_dump тоже завис. И вот это уже совсем нехорошо.
Теперь нам надо понять почему даже он завис. Подключаемся к процессу и видим что завис выполняемый запрос pg_stat_activity. Перед тем как создать дамп, pg_dump выполняет набор проверок таблиц, для чего обращается к системному каталогу. В качестве эксперимента пробуем прочитать каждую таблицу отдельно на другом бэкенде. Что-то читается, а что-то намертво подвешивает весь бэкэнд.
Судя по всему про этот кластер можно было забыть и прожать F, но оказалось что был один спасательный круг. Клиент каждый час выполнял дамп продуктовой БД и поэтому задача переформатировалась в такую: надо принять потерю данных за пару часов работы и за оставшееся время до старта офисного дня заново инициализировать кластер, перелить туда данные, подключить приложение и надеяться что всё запустится.
Прошло всё по плану, к утру мы запустились, но не было ответа на главный вопрос - а почему всё сломалось? Поэтому мы попросили клиента отложить проблемный кластер в сторонку для исследований и попытаться максимально точно описать весь порядок действий до возникновения проблемы.
Какую картину мира удалось восстановить:
Месяца за три до дня инцидента у них переполнился раздел pg_wal, так как сгенерировалось около 500 Гб wal-файлов и СУБД аварийно выключилась
Было решено расширить раздел и запустить СУБД обратно
СУБД не успевала запуститься из-за установленного тайм-аута в 5 минут в systemd unit-е
Вместо того, чтобы изменить таймаут, администратор сходил в интернет и нашёл совет выполнить pg_resetwal. Совета изучить перед этим документацию рядом не лежало, к сожалению
Запустились, но посколько документацию никто не читал, вместе с запуском была затёрта львиная доля необходимых WAL-файлов, чтобы запуститься в согласованном состоянии
Следующие три месяца они жили в режиме: на любую проблему делай pg_resetwal. Прям магичесская команда и чего её все боятся так.
Почему они не изменили тайм-аут запуска постгреса - тайна сия велика и история её умалчивает
Идеальный пример почему нельзя использовать утилиты, про которые ты знаешь только название и увидел совет в интернете. И особенно почему нельзя никогда использовать pg_resetwal без крайней необходимости.
Итого: проактивный мониторинг лучше чем восстановление
Закончить статью хочется чем-то позитивным, чтобы вся информация выше вам никогда не пригодилась. Ну или что-бы о проблемах в базе вы узнавали сами заранее, а не по репортам от пользователей.
Поэтому завершим эту историю подбором инструментов и практик для проактивного мониторинга вашего постгреса
Если вдруг по какой-то причине обнаруживается что контрольные суммы были выключены, включаем их первым делом. Не было ещё ни одного случая, где они были бы лишними и принесли с собой проблемы.
CHECKDB. Проверяет целостность кластера по целой россыпи параметров. Из положительных нюансов это работа в несколько потоков, в отличии от того же pg_dump и непрерывность его работы. То есть, при нахождении ошибки он не спотыкается, а выводит сообщение и идет дальше. С другой стороны, проверка на физическом уровне может пропускать логические несоответствия, потерю или обнуление блоков, вплоть до целых файлов.
Если всё хорошо,то при проверке на физическом уровне выдаст примерно такое сообщение:
# pg_probackup checkdb -B /var/lib/postgresql/backups/ --instance main -D /var/lib/pgpro/ent-16/data/ …. INFO: Start checking data files INFO: Data files are valid
Если данные были испорчены, как здесь чексумма, то такое:
# pg_probackup checkdb -B /var/lib/postgresql/backups/ --instance main -D /var/lib/pgpro/ent-16/data/ …. INFO: Start checking data files WARNING: Corruption detected in file "/var/lib/pgpro/ent-16/data/base/16388/24488", block 53890: page verification failed, calculated checksum 63150 but expected 1873
Результат успешной проверки на логическом уровне:
# pg_probackup checkdb --amcheck -B /var/lib/postgresql/backups/ --instance main -D /var/lib/pgpro/ent-16/data/ ….. INFO: Start checking data files INFO: Data files are valid …. INFO: Start amchecking PostgreSQL instance …. INFO: checkdb --amcheck finished successfully. All checked indexes are valid. INFO: All databases were amchecked.
А если будет ошибка с индексом, то вот так:
# pg_probackup checkdb --amcheck -B /var/lib/postgresql/backups/ --instance main -D /var/lib/pgpro/ent-16/data/ …. INFO: Start checking data files INFO: Data files are valid …. WARNING: Thread [1]. Amcheck failed in database 'demo' for index: 'bookings.boarding_passes_pkey': ERROR: cannot check index "boarding_passes_pkey" DETAIL: Index is not valid. WARNING: Amcheck failed for database 'demo' ERROR: checkdb --amcheck finished with failure. Not all checked indexes are valid. All databases were amchecked.
Другая утилита которую нельзя забывать это pg_checksums. Как несложно догадаться, утилита позволяет включить, отключить и проверить контрольные суммы данных экземпляра СУБД. Будет прочитан буквально каждый файл на кластере. Единственный важный нюанс, что проверка возможна только на выключенном экземпляре СУБД. Соответственно, если у вас организована репликация, лучше остановить все кластеры, чтобы не рисковать кораптом данных.
Ну и посмотрим ещё как выглядит результат успешной проверки:
# pg_checksums -c -v -P -D /var/lib/pgpro/ent-16/data/ pg_checksums: checksums verified in file "/var/lib/pgpro/ent-16/data//global/24700" … 716/716 MB (100%) computed Checksum operation completed Files scanned: 2003 Blocks scanned: 91745 Bad checksums: 0 Data checksum version: 1
И если нашёлся испорченный индекс:
# pg_checksums -c -v -P -D /var/lib/pgpro/ent-16/data/ pg_checksums: checksums verified in file "/var/lib/pgpro/ent-16/data//global/24700" … pg_checksums: error: checksum verification failed in file "/var/lib/pgpro/ent16/data//base/26313/26388", block 1234: calculated checksum 999 but block contains D47 … 716/716 MB (100%) computed Checksum operation completed Files scanned: 2003 Blocks scanned: 91745 Bad checksums: 1 Data checksum version: 1
На этом, кажется, всё. Помните что восстановление данных это не быстро и требует высокой квалификации того кто у руля. Но даже в этом случае никто вам ничего не будет обещать заранее. Поэтому гораздо проще составить план резервного копирования, иметь проверенные актуальные бекапы, архив WAL файлов, план восстановления на случай аварии и не стесняться регулярно проверять здоровье своего постгреса.
