Введение
При написании использованы
- Восстановление повреждённых данных — Евгений Бредня, PGConf.Russia 2023 Евгений Бредня, PGConf.Russia 2023.PDF и https://www.youtube.com/watch?v=oJfT6rMM37A
- “Autovacuum. Вредные советы.” Вадим Яценко (Лаборатории Тантор) PGConf.Russia 2022 Яценко.pdf
- https://postgreshelp.com/postgresql-checksum/
- Статья mafet на habr
Вы для начала по имени файла выясните, что за объект испортился. Может быть, это индекс, который можно пересоздать.
CREATE EXTENSION pageinspect;
- включить расширение для чтения страниц.
“
zero_damaged_pages
— автоматически занулить все страницы, в которых будут ошибки CRC (если такая проверка включена).
Физически испорченные данные (блоки)
При отключенных checksum испорченные блоки могут приводить к разным последствиям - ошибки при чтении/записи, и даже SEGFAULT при обращении к данным.
В случае с включенными checksum поведение более предсказумое (см. ignore_checksum_failure и checksum
Есть несколько способов проверять целостность данных на диске.
-
Для того чтобы находить в рантайме нужно включать проверку
checksum
Для этого потребуется выполнить pg_checksums на выключенной базе. -
Таблицу или БД можно проверить
pg_dump
. Можно даже выполнять периодически и отправлять в /dev/null -
Быстрая проверка, выгрузит данные(!), может работать в несколько потоков
select ctid, * from bad_table where ctid>’(100,0)’ and ctid<’(200,0)’;
Как найти битый блок
- Искать в логах. Например при
sql vacuum freeze bad_table;
SQL Error [XX000]: ERROR: MultiXactId 808464432 has not been created yet -- apparent wraparound
Where: while scanning block 35940675 offset 12 of relation "public.bad_table"
- Метод перебора построчко
postgres=# \set FETCH_COUNT 1
postgres=# \pset pager off
Pager usage is off.
postgres=# SELECT ctid, id FROM table;
- Метод молотилка (взято у postgrespro)
select molotilka(’bad_table’, 0);
Прочитает все строки в каждом блоке из таблицы, покажет нечитаемые блоки и строки.
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;
CREATE or REPLACE FUNCTION molotilka(tbl regclass, start_page bigint, progress bool) RETURNS bigint AS $$
DECLARE
ratio float = 1;
cfs text = '';
n_pages integer;
page integer;
lps_in_page integer;
lp_in_page integer;
err_count bigint = 0;
pid text;
query text;
BEGIN
BEGIN
SELECT cfs_compression_ratio(tbl), 'CFS' INTO ratio, cfs;
IF ratio = 'NaN' THEN ratio = 1.0; cfs = ''; END IF;
EXCEPTION
WHEN others THEN NULL;
END;
SELECT round(pg_relation_size(tbl) * ratio / current_setting('block_size')::int) INTO n_pages;
RAISE NOTICE 'TABLE: % PAGES: % %', tbl::text, n_pages, cfs;
SELECT pg_backend_pid()::text INTO pid;
EXECUTE 'prepare molotilka_'||pid||'(tid) as select row(t.*) from '||tbl::text||' as t where ctid=$1';
FOR page IN start_page .. n_pages-1 LOOP
IF progress and page % 10000 = 0 THEN
RAISE NOTICE 'progress: PAGE %', page;
END IF;
BEGIN
SELECT coalesce(max(lp),0) from heap_page_items(get_raw_page(tbl::text, page)) INTO lps_in_page;
query = format('execute molotilka_%s(''(%s,%%s)'')', pid, page);
FOR lp_in_page IN 1 .. lps_in_page LOOP
BEGIN
EXECUTE format(query, lp_in_page);
EXCEPTION WHEN OTHERS THEN
err_count = err_count + 1;
RAISE NOTICE 'TUPLE ERROR: ctid=(%,%), SQLSTATE=% DETAIL=%', page, lp_in_page, 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;
EXECUTE 'deallocate molotilka_'||pid;
RETURN err_count;
END $$ language plpgsql;
или (не тестировался)
DO
$$DECLARE
c CURSOR FOR SELECT * FROM products;
r products;
cnt bigint := 0;
BEGIN
OPEN c;
LOOP
cnt := cnt + 1;
/* block to start a subtransaction for each row */
BEGIN
FETCH c INTO r;
EXIT WHEN NOT FOUND;
EXCEPTION
WHEN OTHERS THEN
/* there was data corruption fetching the row */
RAISE WARNING 'skipped corrupt data at row number %', cnt;
NOVE c;
CONTINUE;
END;
/* row is good, salvage it */
INSERT INTO products_dump VALUES (r.*);
END LOOP;
END;$$;
Восстановление таблицы удалением испрорченных данных
- Спасаем данные из блока построчно (какие-то из них могут не выполниться)
select ctid,* from public.products where ctid = '(35940675,1)'; -- +
select ctid,* from public.products where ctid = '(35940675,6)'; -- empty
select ctid,* from public.products where ctid = '(35940675,9)'; -- empty
select ctid,* from public.products where ctid = '(35940675,12)'; -- empty
select ctid,* from public.products where ctid = '(35940675,13)'; -- empty
select ctid,* from public.products where ctid = '(35940675,14)'; -- bad
select ctid,* from public.products where ctid = '(35940675,15)'; -- +
select ctid,* from public.products where ctid = '(35940675,16)'; -- bad
select ctid,* from public.products where ctid = '(35940675,17)'; -- +
select ctid,* from public.products where ctid = '(35940675,18)'; -- +
- Ищем расположение таблицы в директории /base:
select pg_relation_filepath('files');
/*
pg_relation_filepath
---------------------
base/16401/16601
*/
- Конвертируем номер блока в номер файла:
(38800366 * 8192) / (1024^3) = 296.0233002 /* base/16401/16601.296 */
- Проверяем с помощью pg_filedump:
pg_filedump -i ./base/16401/16601.296 | grep "XMAX: 97" /* XMIN: 0 XMAX: 97 CID|XVAC: 0 */
-
Рассчитываем номер блока в файле:
(0.0233002 * 1024^3) / 8192 = 3054.0038144 /* Блок 3054 */
-
Для автоматизации пунктов 1-4 (те что выше) можно использовать генератор команды dd. Обратить внимание что seek (нужен для зануления) != skip (нужен для чтения блоков), см доку на dd
with what(rel, block_no, qty) as (
select 'public.products' -- relation
, 35940675 -- starting block
, 1 -- qty of blocks to get
), params(block_size, segment_size, path) as (
select pg_size_bytes(current_setting('block_size'))
, pg_size_bytes(current_setting('segment_size'))
, current_setting('data_directory')
), calc as (
select block_no / (segment_size/block_size) as segment
, block_no % (segment_size/block_size) as offset_in_segment
, path||'/'||pg_relation_filepath(rel) as file
from params, what
)
select 'dd if='|| file || case when segment>0 then '.'||segment else '' end
|| ' of=/tmp/'||rel::text||'_b'||block_no|| ' bs='||block_size
|| ' skip='||offset_in_segment|| ' count='||qty
from calc, params, what
/*
Пример:
/pgsql/16/data/base/474828/5208699.274 of=/tmp/public.products_b35940675 bs=8192 skip=26947 count=1
*/
-
Останавливаем сервер PostgreSQL
-
бэкапим файлик
/base/16401/16601.296
-
“Зануляем” весь блок:
# dd if=/dev/zero of=/PGDATA/base/db_oid/bad_table_filenodeid bs=8k seek=XXXXX count=1 conv=notrunc
dd if=/dev/zero of=$PG_DATA/base/16401/16601.296 bs=8192 seek=3054 count=1 conv=notrunc
- Проверяем с помощью pg_filedump:
pg_filedump -i ./base/16401/16601.296 | grep "XMAX: 97"
- Проверяем, с помощью pageinspect:
postgres=# select t_xmin, t_xmax, t_ctid from heap_page_items(get_raw_page('files',38800366)) WHERE t_xmax = 97;
/*
t_xmin | t_xmax | t_ctid
---------+---------+--------
(0 rows)
*/