Перейти до вмісту

L6. Відновлення на момент часу

просунутийспирається на модуль 11

Пройти на практиці те, що модуль 11 описує словами: знайти в журналі мить помилкової команди, налаштувати restore_command і мету відновлення, підняти сервер із копії й архіву та переконатися, що він працює на новій timeline (гілці історії журналу). Після роботи ви знаєте, що робити, коли хтось видалив таблицю, а репліка й нічний дамп уже не допоможуть.

У середовищі лабораторної працює PostgreSQL із archive_mode = on, а сценарій incident.sh робить фізичний бекап, приймає позначені замовлення, видаляє order_items і продовжує приймати замовлення вже без неї. Ваше завдання: відновити базу в окремому сервісі restore так, щоб

  • таблиця order_items існувала, а всі замовлення, закомічені до DROP TABLE, були на місці: і ті, що зроблені до бекапу, і ті, що між бекапом і аварією (вони є лише в WAL);
  • жодного замовлення, записаного після аварії, не було;
  • сервер вийшов з відновлення (pg_is_in_recovery() = f) на новій timeline.

Результат: заповнений labs/l06-pitr/student/recovery.conf. Готово, коли ./labs/l06-pitr/check.sh виводить «усе гаразд».

Межі: не змінюйте compose.yml, скрипти й сервіс postgres; бекап і архів для restore змонтовано лише для читання. Каталог labs/l06-pitr/.truth/ читає чекер, у нього заглядати не треба: знайти час аварії самим і є завданням. Відновлювати з pg_dump чи переносити таблицю з іншого місця не треба: лабораторна про WAL.

Прочитайте розділи модуля про фізичний бекап, архівування й відновлення на момент часу. Потрібні Docker Desktop (або Docker на Linux) і bash; docker context ls має вказувати на локальний Docker. Середовище використовує той самий образ PostgreSQL, що й кореневий docker-compose.yml, але окремий файл:

Terminal window
docker compose -f labs/l06-pitr/compose.yml up -d --wait
./labs/l06-pitr/incident.sh

Перший запуск завантажує датасет small (Датасет; десятки секунд), incident.sh працює близько півхвилини. Порти (PG_PORT, PG_RESTORE_PORT) і назву проєкту (COMPOSE_PROJECT_NAME) можна перевизначити змінними середовища, скрипти їх враховують. Далі для стислості: dc() { docker compose -f labs/l06-pitr/compose.yml "$@"; }.

Що є після сценарію. У томі backup лежить базовий бекап (/backup/base, зроблений pg_basebackup), у томі archive сегменти WAL, які копіює archive_command. Замовлення сценарій позначив у колонці customer_note: l06:before:N (до бекапу), l06:mid:N (після бекапу, до аварії) і l06:after:N (після аварії). Сервіс restore із профілю restore розгортає бекап заново при кожному запуску restore.sh і підключає ваш student/recovery.conf.

  1. Огляд. Подивіться, що є: dc exec postgres cat /backup/base/backup_label і dc exec postgres ls -l /archive. З якого сегмента починається бекап і чому в архіві є файл ….backup? Що в pg_stat_archiver? Перевірте стан основного сервера: dc exec postgres psql -U shop -d shop -c "\d order_items" і підрахунок замовлень за позначками. Чого бракує?

  2. Два наївні підходи. Спершу запустіть ./labs/l06-pitr/restore.sh з порожнім student/recovery.conf. Що скаже сервер і чому? Потім додайте тільки restore_command = 'cp /archive/%f %p' (шлях до архіву в контейнері restore такий самий, /archive), знову restore.sh і перегляньте стан таблиці. Запишіть, що сталося з order_items, і поясніть, чому відновлення «до кінця архіву» не годиться.

  3. Знайти мить аварії. Лог основного сервера записує DDL: dc logs postgres | grep "DROP TABLE". Час у лозі це початок команди, а нам потрібен коміт, тож знайдіть його в самому журналі, шукаючи запис COMMIT з переліком файлів, що видаляються (rels:):

    Terminal window
    dc exec postgres sh -c 'for f in $(ls /archive | grep -E "^[0-9A-F]{24}$"); do pg_waldump -r Transaction /archive/$f 2>/dev/null | grep "rels:" | cut -c1-150; done'

    Запишіть час коміту й LSN. На скільки час коміту пізніший за час у лозі? Чи міг би в такий проміжок потрапити інший коміт?

  4. Налаштувати відновлення. У student/recovery.conf задайте restore_command і одну мету: recovery_target_time або recovery_target_lsn. Обміркуйте, яке значення recovery_target_inclusive потрібне для вашої мети і що станеться з типовим. Вирішіть також, що робити, коли сервер дійде до мети (recovery_target_action). Запустіть ./labs/l06-pitr/restore.sh: він покаже хвіст логу відновленого сервера. Шукайте рядки recovery stopping before commit чи …after commit і selected new timeline ID.

  5. Перевірити руками. Запити до відновленого сервера: dc exec restore psql -U shop -d shop. Підрахуйте позначені замовлення за стадіями:

    SELECT split_part(customer_note, ':', 2) AS stage, count(*) FROM orders WHERE customer_note LIKE 'l06:%' GROUP BY 1 ORDER BY 1;

    Очікуєте before 20 і mid 30, без after. Перевірте також SELECT pg_is_in_recovery() і SELECT pg_walfile_name(pg_current_wal_lsn()): перші вісім цифр імені сегмента показують timeline.

  6. Здача. ./labs/l06-pitr/check.sh. Кожен пункт, що не пройшов, підказує, де шукати причину.

Terminal window
./labs/l06-pitr/check.sh

Чекер працює з сервісом restore у режимі читання й нічого не змінює. Він перевіряє, що:

  • сервер працює й вийшов з відновлення (pg_is_in_recovery() = f);
  • імена сегментів WAL починаються не з 00000001, тобто відкрито нову timeline;
  • таблиця order_items існує, а позиції позначених і початкових замовлень цілі;
  • усі 20 замовлень before і усі 30 mid на місці, жодного after, а замовлення датасету не постраждали.

Чекер не залежить від того, як ви знайшли мету: час чи LSN, promote чи pause з pg_wal_replay_resume(), recovery_target_inclusive = off із точним часом коміту чи мета трохи раніша.

Сервер «застряг» у відновленні. recovery_target_action типово pause: після досягнення мети сервер лишається доступним для читання, а pg_is_in_recovery() дає t. Задайте promote або викличіть pg_wal_replay_resume() від імені суперкористувача (dc exec restore psql -U postgres -c "SELECT pg_wal_replay_resume()").

Таблиці немає. Мета лежить на самому коміті DROP TABLE або за ним. Час коміту з pg_waldump разом із типовим recovery_target_inclusive = on відтворює і цей коміт. Задайте off або змістіть мету трохи назад.

Сервер падає з «recovery ended before configured recovery target was reached». Мета лежить за кінцем архіву, або збігається з часом останнього коміту в ньому: потрібен ще один запис, щоб сервер побачив, що мету перейдено. Перевірте мету й те, що restore_command знаходить сегменти.

Сервер не стартує: «must specify “restore_command”». У recovery.conf немає restore_command, а сервер запущено з recovery.signal.

Зміни в recovery.conf нічого не дали. Сервер читає файл лише при старті, і копіювання бекапу відбувається при створенні каталогу даних. Після кожної зміни запускайте restore.sh, який створює сервер заново.

Час без зони. Лог і pg_waldump показують UTC, тож додавайте +00 до значення мети.

cp: cannot stat '/archive/00000002.history' у лозі. Це нормально: сервер питає архів про файли історії timeline, яких ще немає.

incident.sh не стартує вдруге. Сценарій одноразовий. Почніть спочатку: dc --profile "*" down -v && rm -rf labs/l06-pitr/.truth, потім знову up і incident.sh.

Docker дивиться на віддалену машину. docker context ls: зірочка біля SSH-адреси означає, що docker compose працює там. Переключіться на desktop-linux чи default.

У реальній аварії відновлений сервер майже завжди лише джерело: таблицю переносять у бойову базу. Витягніть order_items із restore і завантажте в postgres: dc exec -T restore pg_dump -U shop -d shop -t order_items | dc exec -T postgres psql -U shop -d shop. Чим це відрізняється від підміни бойового сервера відновленим, і що станеться із замовленнями l06:after, зробленими вже після аварії? Спробуйте також мету recovery_target_name: додайте в сценарій pg_create_restore_point('before_drop') перед DROP TABLE і відновіться на цю мітку. Прибрати за собою: dc --profile "*" down -v.