Bad Blocks

Введение

При написании использованы

Вы для начала по имени файла выясните, что за объект испортился. Может быть, это индекс, который можно пересоздать.

CREATE EXTENSION pageinspect; - включить расширение для чтения страниц.

zero_damaged_pages — автоматически занулить все страницы, в которых будут ошибки CRC (если такая проверка включена).

Физически испорченные данные (блоки)

При отключенных checksum испорченные блоки могут приводить к разным последствиям - ошибки при чтении/записи, и даже SEGFAULT при обращении к данным. В случае с включенными checksum поведение более предсказумое (см. ignore_checksum_failure и checksum

Есть несколько способов проверять целостность данных на диске.

  1. Для того чтобы находить в рантайме нужно включать проверку checksum Для этого потребуется выполнить pg_checksums на выключенной базе.

  2. Таблицу или БД можно проверить pg_dump. Можно даже выполнять периодически и отправлять в /dev/null

  3. Быстрая проверка, выгрузит данные(!), может работать в несколько потоков

select ctid, * from bad_table where ctid>’(100,0)’ and ctid<’(200,0)’;

Как найти битый блок

  1. Искать в логах. Например при 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"
  1. Метод перебора построчко
postgres=# \set FETCH_COUNT 1
postgres=# \pset pager off
Pager usage is off.
postgres=# SELECT ctid, id FROM table;
  1. Метод молотилка (взято у 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;$$;

Восстановление таблицы удалением испрорченных данных

  1. Спасаем данные из блока построчно (какие-то из них могут не выполниться)
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)'; -- +
  1. Ищем расположение таблицы в директории /base:
select pg_relation_filepath('files');
/*
pg_relation_filepath
---------------------
base/16401/16601
*/
  1. Конвертируем номер блока в номер файла:
(38800366 * 8192) / (1024^3) = 296.0233002  /*  base/16401/16601.296 */
  1. Проверяем с помощью pg_filedump:
pg_filedump -i ./base/16401/16601.296 | grep "XMAX: 97"  /* XMIN: 0 XMAX: 97 CID|XVAC: 0 */
  1. Рассчитываем номер блока в файле: (0.0233002 * 1024^3) / 8192 = 3054.0038144 /* Блок 3054 */

  2. Для автоматизации пунктов 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
 */
  1. Останавливаем сервер PostgreSQL

  2. бэкапим файлик /base/16401/16601.296

  3. “Зануляем” весь блок:

# 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
  1. Проверяем с помощью pg_filedump:
pg_filedump -i ./base/16401/16601.296 | grep "XMAX: 97"
  1. Проверяем, с помощью 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)
*/