Приклад відновлення PostgreSQL таблиць за допомогою нової мега фічі pg_filedump



Дозвольте я розповім вам про одну класної фиче, яку ми з колегами з Postgres Pro нещодавно запилили в утиліті pg_filedump. Ця фіча дозволяє частково відновлювати дані з бази, навіть у разі, якщо база була сильно пошкоджена і інстанси PostgreSQL з такою базою вже не запустиш. Звичайно, хочеться вірити, що потреба в такому функціоналі виникає вкрай рідко. Але на всякий випадок щось подібне хотілося б мати під рукою. Читайте далі, і ви дізнаєтеся, як ця фіча виглядає у дії.

Часткове відновлення даних було представлено коммите 52fa0201:

commit 52fa0201f97808d518c64bcb9696f2a350678aa5
 
Author: Teodor Sigaev <teodor@sigaev.ru>
 
Date: Tue Jan 17 16:01:12 2017 +0300
 

 
Partial data recovery (-D flag).
 

 
This feature allows to partially recover data from a given segment file
 
in format suitable for using in COPY FROM statement. List of supported
 
data types is currently not full and TOAST is not yet supported, but
 
it's better than nothing. Hopefully data recovery will be improved in
 
the future.
 

 
Implemented by Aleksander Alekseev, reviewed by Dmitry Ivanov, tested
 
by Dmitry Ivanov and Grigoriy Smolkin.


Припустимо, є якась таблиця:

create table tt (int x, y bool, z text, w timestamp);

… заповнена якимись даними:

insert into tt values(123, true, 'Text test test', now());
insert into tt values(456, null, 'Ололо трооло', null);
checkpoint;

Тут я кажу checkpoint, щоб дані обов'язково потрапили на диск. Інакше вони потраплять в WAL, але buffer manager буде тримати їх у пам'яті, поки таплы (tuple, кортеж, рядок в таблиці) не будуть витіснені більш новими та/або часто використовуваними таплами. Або чекпоинтом по таймауту/накопиченню max_wal. Думаю, це найчастіший сценарій для сінка сторінки на диск. — прим. Стаса Кельвича.

Також дізнаємося ім'я сегмента, відповідного таблиці:

select relfilenode from pg_class where relname = 'tt';

В моєму випадку relfilenode у таблиці 16393. Знайдемо цей сегмент (або сегменти, якщо таблиця більше 1 Гб) на диску:

find /path/to/db/ -type f | grep 16393

Скопіюємо його куди-небудь і уявимо, що нам хочеться відновити дані, маючи на руках тільки файл сегмента.

Для цього зберемо останню версію pg_filedump:

git clone git://git.postgresql.org/git/pg_filedump.git
cd pg_filedump
make

Зазвичай схема бази відома, так як на руках є додаток, який з нею працює. Значить, ми знаємо типи стовпців в таблиці і можемо декодувати їх таким чином:

./pg_filedump -D int,bool,text,timestamp /path/to/db/base/16384/16393

Приклад виводу:

*******************************************************************
* PostgreSQL File/Block Formatted Dump Utility - Version 9.6.0
*
* File: /home/eax/work/postgrespro/postgresql-install/data-master/base/16384/16393
* Options used: -D int,bool,text,timestamp
*
* Dump created on: Tue Jan 17 16:28:07 2017
*******************************************************************

Block 0 ********************************************************
<Header> -----
Block Offset: 0x00000000 Offsets: Lower 32 (0x0020)
Block: Size 8192 Version 4 Upper 8080 (0x1f90)
LSN: logid 0 recoff 0x0301e4c0 Special 8192 (0x2000)
Items: 2 Free Space: 8048
Checksum: 0x0000 Prune XID: 0x00000000 Flags: 0x0000 ()
Length (including item array): 32

<Data> ------
Item 1 -- Length: 56 Offset: 8136 (0x1fc8) Flags: NORMAL
COPY: 123 t Text test test 2017-01-17 16:25:03.448488
Item 2 -- Length: 52 Offset: 8080 (0x1f90) Flags: NORMAL
COPY: 456 \N Ололо трооло \N

*** End of File Encountered. Last Block Read: 0 ***

Тут досить багато даних, так як pg_filedump виводить інформацію про кожній сторінці сегменті і декодує заголовок кожного тапла. На щастя, можна досить просто відділити мух від котлет, наприклад, так:

pg_fiedump -D ...як..раніше... | grep COPY | perl -lne 's/^COPY: //g; print;' > /tmp/copy.txt
cat /tmp/copy.txt

Вміст файлу copy.txt:

123 t Text test test 2017-01-17 16:25:03.448488
456 \N Ололо трооло \N

Це дані нашої таблиці у форматі, придатному для використання у запиті COPY FROM. Перевіряємо:

create table tt2 (int x, y bool, z text, w timestamp);
copy tt2 from '/tmp/copy.txt';
select * from tt2;

Результат:

x | y | z | w
-----+---+----------------+----------------------------
123 | t | Text test test | 2017-01-17 16:25:03.448488
456 | | Ололо трооло |
(2 rows)

Як бачите, всі дані успішно відновлені.

Природно, це був дещо спрощений приклад і на практиці все складніше. По-перше, список підтримуваних типів на даний момент дещо обмежений:

static ParseCallbackTableItem callback_table[] = {
{ "smallserial", &decode_smallint },
{ "smallint", &decode_smallint },
{ "int", &decode_int },
{ "serial", &decode_int },
{ "bigint", &decode_bigint },
{ "bigserial", &decode_bigint },
{ "time", &decode_time },
{ "timetz", &decode_timetz },
{ "date", &decode_date },
{ "timestamp", &decode_timestamp },
{ "float4", &decode_float4 },
{ "float8", &decode_float8 },
{ "float", &decode_float8 },
{ "bool", &decode_bool },
{ "uuid", &decode_uuid },
{ "macaddr", &decode_macaddr },

/* internally all string types are stored the same way */
{ "char", &decode_string },
{ "varchar", &decode_string },
{ "text", &decode_string },
{ "json", &decode_string },
{ "xml", &decode_string },
{ NULL, NULL},
};

По-друге, TOAST зараз не підтримується. Якщо рядок зберігається у стислому вигляді або стиснута на сторінці in-place, pg_filedump її успішно відновить (якщо стислі дані не були зіпсовані). Однак якщо рядок була перенесена в зовнішню TOAST-таблицю, замість рядка ви отримаєте просто "(TOASTED)". В принципі, підтримка TOAST — не нерозв'язна задача. Треба тільки навчити pg_filedump парсити каталог і знаходити відповідну TOAST-таблицю. Просто поки що цього ніхто не зробив. Можливо, підтримка TOAST буде додана в майбутніх версіях pg_filedump.

Нарешті, на практиці схема бази даних іноді змінюється, стовпці в таблиці з'являються і зникають. Видалення стовпців — не така вже велика проблема, так як фізично в тапле цей стовпець залишається, просто він завжди дорівнює null. Ось з додаванням трохи складніше, так як з-за нього таплы в рамках однієї таблиці можуть мати змінне число атрибутів. Якщо число атрибутів в тапле не відповідає кількості атрибутів, зазначених користувачем, pg_filedump просто показує попередження з частково декодированными даними, і переходить до наступного таплу. Це означає, що на практиці парсинг виведення pg_filedump буде трохи складніше, ну або що вам доведеться прогнати його кілька разів з різними списками атрибутів.

На мій погляд, та й не тільки мій, крайнє засіб відновлення даних, краще мати хоча б таке, ніж не мати ніякого :) Якщо у вас є ідеї щодо подальшого поліпшення представленого функціоналу, та й взагалі будь-які зауваження і доповнення, мені буде дуже цікаво ознайомитися з ними в коментарях!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.