Любому IT-администратору важно знать состояние оборудования, за которое он отвечает. Сбои в хранилище или файловой системе, повреждения страниц в оперативной памяти могут отразиться на целостности данных во всей БД. В этой статье мы расскажем, какие инструменты СУБД Postgres Pro помогут защитить ваши данные и предупредить реальные проблемы.

Включаем подсчет контрольных сумм

Некорректная работа подсистемы ввода-вывода зачастую приводит к повреждению страниц данных. Выявлять битые страницы на диске можно с помощью проверки контрольных сумм — тогда при записи на диск для каждой страницы будет рассчитываться контрольная сумма, а при чтении — проверяться.

Проверку контрольных сумм можно включить при инициализации кластера командой initdb (--data-checksums) или позже с помощью утилиты pg_checksum. Если пренебречь подсчетом контрольных сумм, корректные данные могут потеряться, а повреждение данных — распространиться по всей базе и затронуть всю инфраструктуру. Вот почему мы включили в Postgres Pro по умолчанию подсчет контрольных сумм для всех страниц БД на стадии инициализации экземпляров даже несмотря на то, что это может снизить производительность.

Если при чтении данных Postgres Pro обнаружит неверную контрольную сумму, появится сообщение об ошибке и текущая транзакция прервется. Чтобы проигнорировать проблему, можно включить параметр ignore_checksum_failure — это поможет обойти ошибку и извлечь неповреждённые данные со страницы, если цел заголовок. Если же заголовок повреждён, параметр игнорируется. Включение параметра ignore_checksum_failure может привести к падению инстанса, поэтому он используется только как крайняя мера и только когда с БД никто не работает.

Ищем битые страницы

Чтобы выявлять битые страницы БД до того, как их обнаружат пользователи, служит необходимо использовать утилиту pg_checksums. Она принудительно запускает проверку контрольных сумм данных в кластере Postgres Pro, но требует остановки сервера БД, что не всегда возможно в промышленных системах. Ещё одна полезная утилита pg_checksums_ext (на основе pg_checksums) умеет проверять контрольные суммы онлайн, но разработчики ее давно не обновляли.

Другой инструмент для поиска битых страниц в Postgres — утилита резервного копирования pg_probackup, в которую встроена функция проверки контрольных сумм. Недавно она помогла нам найти 13 битых страниц в 20-терабайтной БД всего за 40 минут, при том что была развернута на весьма нестабильно работающей дисковой системе одного из наших клиентов.

Утилита pg_probackup в Postgres Pro не только проверяет контрольные суммы страниц, но и вычисляет контрольные суммы копий файлов БД  непосредственно в процессе создания резервной копии. Таким образом целостность копии проверяется сразу после создания бэкапа и перед его восстановлением.

Команда checkdb в составе pg_probackup выполняет физическую и логическую (о ней чуть дальше) проверку всех файлов БД. Физическая включает проверку целостности заголовков и контрольных сумм страниц. Если обнаружится битая страница, checkdb продолжит работу, пока не проверит все страницы в кластере. Это очень удобно, когда надо быстро выявить битые страницы.

Рассмотрим процесс проверки БД на наличие битых страниц.

1. Создадим тестовую таблицу и добавим в неё три строчки:

CREATE TABLE my_table(a int);
INSERT INTO my_table VALUES (1), (2), (3);

2. Сделаем полный бэкап:

pg_probackup backup -B ../test1/backup --instance=db1 -p 5432 -d postgres -b full --stream

3. Добавим ещё одну строку в таблицу my_table:

INSERT INTO my_table VALUES (4);

4. Намеренно испортим файл с данными таблицы: запишем в начало его заголовка нули. Заголовок файла до:

head -c 20 ../test1/db1/data/base/5/16384
b'\x00\x00\x00\x00\xe8\x07\x00\x03|\x84\x00\x00$\x00h\x1f\xe8\x1f\xfe '

Заголовок файла после:

head -c 20 ../test1/db1/data/base/5/16384
b'000000\x00\x03|\x84\x00\x00$\x00h\x1f\xe8\x1f\xfe '

5. Запустим функцию checkdb на экземпляре Postgres Pro:

pg_probackup checkdb -D ../test1/db1/data -d postgres -p 5432
INFO: This PostgreSQL instance was initialized with data block checksums.
Data block corruption will be detected
INFO: Start checking data files
WARNING: Corruption detected in file "../test1/db1/data/base/5/16384",
block 0: page verification failed, calculated checksum 33916 but expected 7065
ERROR: Checkdb failed

6. Выполним запрос к нашей таблице и ожидаемо увидим ошибку:

SELECT * FROM my_table
WARNING:  page verification failed, calculated checksum 7065 but expected 33916
ERROR:  invalid page in block 0 of relation base/5/16384

Восстанавливаем БД из резервной копии

Страницы БД, в которых обнаружатся ошибки, можно восстановить из резервной копии, созданной до их появления. Для этого понадобятся все журналы предзаписи (WAL), а значит, процесс может оказаться небыстрым. Восстановим БД из резервной копии.

1. Остановим экземпляр и восстановим базу с накатом WAL:

pg_probackup restore -B ../test1/backup -D ../test1/db1/data --instance=db1--no-sync -j 4 --incremental-mode=checksum --log-level-console=INFO --restore-command="pg_probackup" archive-get -B ../test1/backup --instance=db1--wal-file-path=%p --wal-file-name=%f

INFO: Running incremental restore into nonempty directory: "../test1/db1/data"
INFO: Validating backup SBMRTC
INFO: Backup SBMRTC data files are valid
INFO: Backup SBMRTC WAL segments are valid
INFO: Backup SBMRTC is valid.
INFO: Restoring the database from the backup starting at 2024-04-08 17:46:24+02
INFO: Extracting the content of destination directory for incremental restore
INFO: Destination directory content extracted, time elapsed: 0
INFO: Removing redundant files in destination directory
INFO: Redundant files are removed, time elapsed: 0
INFO: Start restoring backup files. PGDATA size: 39MB
WARNING: Corruption detected in file "../test1/db1/data/base/5/16384",
block 0: page verification failed, calculated checksum 33916 but expected 7065
INFO: Backup files are restored. Transferred bytes: 306kB, time elapsed: 0
INFO: Restore incremental ratio (less is better): 1% (306kB/39MB)
WARNING: Restored files are not synced to disk
INFO: Restore of backup SBMRTC completed.

Обратите внимание: утилита pg_probackup выполнила инкрементальное восстановление (incremental-mode=checksum), т. е. заменила всего 1% объёма БД — только испорченные данные (Transferred bytes: 306 kB). Кроме того, мы указали ключ --restore-command, который позволил нам накатить WAL, и полностью восстановили БД из последнего бэкапа.

2. Проверим данные после восстановления:

SELECT * FROM my_table;
1
2
3
4

3. Проверим БД ещё раз с помощью checkdb:

pg_probackup checkdb -D ../test1/db1/data -d postgres -p 5432
INFO: This PostgreSQL instance was initialized with data block checksums.
Data block corruption will be detected
INFO: Start checking data files
INFO: Data files are valid

Вывод: и страницы БД целы, и данные не потеряны.

Если восстановление из резервной копии кажется вам слишком долгим, можно переключиться на реплику при ее наличии. Реплика обычно располагается на другом сервере, и испортить те же самые страницы БД, что и на мастере, из-за неполадок оборудования маловероятно. Но реплику тоже нужно проверять, так как отказы оборудования могут случаться и на её стороне.

Также в СУБД Postgres Pro предусмотрена технология, которая автоматически исправляет страницу с некорректной контрольной суммой корректными данными с реплики. Достаточно лишь присвоить параметру page_repair на мастере значение true.

Ищем логические повреждения

Увы, проверка контрольных сумм не защищает нас от всех сбоев: команда checkdb не выявит логические несоответствия и не избавит от проблем, связанных с неисправностью оперативной памяти, даже если в ней используются стандартные коды исправления ошибок (ECC) или более совершенная защита. ECC-память обычно «ловит» только однобитовые ошибки и не гарантирует абсолютную защиту от других проблем.

Но есть и хорошие новости: команда checkdb --amcheck в составе утилиты pg_probackup возьмет на себя часть логических проверок, а опция checkunique выявит нарушения уникальности индекса. Ключ heapallindexed запустит дополнительную проверку того, что все кортежи представлены в индексе, но создаст дополнительную нагрузку на процессор, память и подсистему ввода-вывода. При запуске heapallindexed обычно значительно увеличивается вероятность обнаружения однобитовых ошибок, поскольку проверяется строгое двоичное равенство и индексированные атрибуты.

А теперь проведём небольшой тест, когда файловая система или дисковый массив не сбросил свой кеш на физический диск, неважно по какой причине.

1. Создадим тестовую таблицу lost_writes с уникальным индексом, добавим три строчки и принудительно выполним контрольную точку:

CREATE TABLE lost_writes(a int UNIQUE);
INSERT INTO lost_writes VALUES (1), (2), (3);
SELECT * FROM lost_writes;
 a 
---
 1
 2
 3
(3 строки)

CHECKPOINT;

2. Определим, в каком файле содержатся данные таблицы, и скопируем этот файл целиком во временный каталог:

postgres=# SELECT pg_relation_filepath('lost_writes');
 pg_relation_filepath 
----------------------
 base/5/16415
(1 строка)

3. Вставим четвёртую строку в таблицу lost_writes и снова принудительно выполним контрольную точку:

postgres=# INSERT INTO lost_writes VALUES (4);
INSERT 0 1
postgres=# CHECKPOINT;
CHECKPOINT
postgres=# SELECT * FROM lost_writes;
 a 
---
 1
 2
 3
 4
(4 строки)

4. Перезагрузим экземпляр Postgres, подменим файл таблицы тем самым файлом из временного каталога, который мы скопировали до вставки четвёртой строки, и снова выполним запрос:

SELECT * FROM lost_writes;
 a 
---
 1
 2
 3

Данные четвёртой строки потеряны, причём проверка контрольных сумм не сработала, потому что контрольные суммы в заголовках страниц полностью соответствуют их содержимому. Во время выполнения запроса ошибок не возникает, но очевидно, что дальнейшая работа с такой базой данных приведёт к ошибкам в логике вашего приложения.

5. Убедимся в корректности контрольных сумм:

pg_probackup checkdb -D  ../test1/db1/data -d postgres -p 5432
INFO: This PostgreSQL instance was initialized with data block checksums. Data block corruption will be detected
INFO: Start checking data files
INFO: Data files are valid

Обратите внимание, что у нашей таблицы есть уникальное поле — индекс, который хранится совсем в другом файле (мы его не трогали). 

6. Проверим БД с опцией amcheck:

pg_probackup checkdb -D  ../test1/db1/data --amcheck -d postgres -p 5432
INFO: This PostgreSQL instance was initialized with data block checksums. Data block corruption will be detected
INFO: Start checking data files
INFO: Data files are valid
INFO: This PostgreSQL instance was initialized with data block checksums. Data block corruption will be detected
INFO: Start amchecking PostgreSQL instance
INFO: Amchecking database 'postgres' using extension 'amcheck' version 1.3.1 from schema 'public'
WARNING: Thread [1]. Amcheck failed in database 'postgres' for index: 'public.lost_writes_a_key': ОШИБКА:  item order invariant violated for index "lost_writes_a_key"
ПОДРОБНОСТИ:  Lower index tid=(1,4) (points to heap tid=(0,4)) higher index tid=(1,5) (points to heap tid=(0,4)) page lsn=0/1B533B8.
WARNING: Amcheck failed for database 'postgres'
ERROR: checkdb --amcheck finished with failure. Not all checked indexes are valid. Some databases were not amchecked.

Кроме потери данных в таблице мы обнаружили ещё и нарушение порядка элементов в индексе.

Теперь рассмотрим пример с нарушением уникальности индекса.

1. Создадим тестовую таблицу bttest_unique с уникальным индексом:

CREATE TABLE bttest_unique(a int UNIQUE);
INSERT INTO bttest_unique VALUES (1), (2), (3);

2. Отключим флаг уникальности для индекса таблицы bttest_unique:

UPDATE pg_catalog.pg_index SET indisunique = false 
WHERE indrelid = 'bttest_unique'::regclass;

3. Добавим неуникальное значение в таблицу. Строка со значением 1 у нас уже есть, добавим ещё одну:

INSERT INTO bttest_unique VALUES (1);

4. Вернём флаг уникальности:

UPDATE pg_catalog.pg_index SET indisunique = true
WHERE indrelid = 'bttest_unique'::regclass;

5. Убедимся, что проверка уникальности индекса вновь действует — попробуем добавить строку со значением 2:

INSERT INTO bttest_unique VALUES (2);

Получим ошибку:

ERROR:  duplicate key value violates unique constraint "bttest_unique_a_key"
DETAIL:  Key (a)=(2) already exists.

Кажется: всё замечательно, проверка уникальности работает, но в данных есть логическая ошибка:

SELECT * FROM bttest_unique;
1
2
3
1

Выявить неправильные данные нам помог SQL-запрос, но отлавливать такие ошибки нужно как можно раньше, пока их не обнаружили пользователи. Попробуем выявить нарушения в уникальности индекса с помощью утилиты pg_probackup с командой checkdb --amcheck и опцией checkunique:

pg_probackup checkdb -D ../test1/db1/data --amcheck --checkunique -d postgres -p 5432
INFO: This PostgreSQL instance was initialized with data block checksums.
Data block corruption will be detected
INFO: Start checking data files
INFO: Data files are valid
INFO: Start amchecking PostgreSQL instance
INFO: Amchecking database 'postgres' using extension 'amcheck' version 1.3.1 from schema 'public'
WARNING: Thread [1]. Amcheck failed in database 'postgres' for index: 'public.bttest_unique_a_key': ERROR:  index uniqueness is violated for index "bttest_unique_a_key": Index tid=(1,1) and tid=(1,2) (point to heap tid=(0,1) and tid=(0,4)) page lsn=0/1495388.
WARNING: Amcheck failed for database 'postgres'
ERROR: checkdb --amcheck finished with failure. Not all checked indexes are valid. All databases were amchecked.

Ошибка найдена! Отметим, что утилита pg_probackup с командой checkdb --amcheck сообщает нам лишь о логических ошибках и затронутых ими данных. Причина  же ошибки остается неизвестной, так как транзакция давно завершилась, а исправить ошибку автоматически мы не можем.

При выявлении подобных проблем исправлять некорректные данные придётся вручную. Чтобы не упустить изменения в других таблицах, которые зависят от испорченных, нужно хорошо знать модель данных приложения. Другая опасность: интеграционные процессы сторонних приложений могут прочесть неверные данные таблицы и разнести ошибку по всей корпоративной системе.

Что дальше?

Теперь вы знаете, как выявлять битые данные в БД и исправлять их. Поскольку причин, из-за которых может пострадать БД, очень много, полностью защититься от всех не получится. Чтобы ваша система работала без сбоев и с минимальными остановками на обслуживание, рекомендуем регулярно проводить резервное копирование и пробные восстановления БД.


Соавтор статьи: Андрей Забелин. Старший технический консультант, Postgres Professional

Комментарии (5)


  1. plumqqz
    09.07.2024 09:30

    "Недавно она помогла нам найти 13 битых страниц в 20-терабайтной БД всего за 40 минут, при том что была развернута на весьма нестабильно работающей дисковой системе одного из наших клиентов."
    "Курить я буду, но пить не брошу"
    Может быть, не стоило и связываться с "весьма нестабильно работающей дисковой системой"?


    1. Loxmatiymamont
      09.07.2024 09:30
      +1

      Возможно задача была успеть любой ценой вытащить данные до того как диски окончательно рассыпятся, попутно проверив их консистентность и не создать излишней нагрузки на пациента.


    1. Sleuthhound
      09.07.2024 09:30

      Вероятно нестабильная это например развалился raid, а мониторинга этого небыло и увидили уже поздно, когда данные повредились

      Или банально сбоит диск в raid, но массив не переходит в состояние даградирован, но данные портятся


      1. Loxmatiymamont
        09.07.2024 09:30

        Если рейд развалился, то уже и проверять нечего )

        Словом, ждём автора с увлекательной историей и разгадкой.


    1. maximvf
      09.07.2024 09:30
      +2

      Клиентскую поддержку часто вызывают тогда, когда уже "поздно пить боржоми". Или почти поздно. В этот момент "что делали" - не так важно, важно "где оказались" и "что будем делать".