L6. Відновлення на момент часу
Пройти на практиці те, що модуль 11 описує словами: знайти в журналі мить помилкової команди, налаштувати restore_command і мету відновлення, підняти сервер із копії й архіву та переконатися, що він працює на новій timeline (гілці історії журналу). Після роботи ви знаєте, що робити, коли хтось видалив таблицю, а репліка й нічний дамп уже не допоможуть.
Завдання
Section titled “Завдання”У середовищі лабораторної працює 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.
Перед початком
Section titled “Перед початком”Прочитайте розділи модуля про фізичний бекап, архівування й відновлення на момент часу. Потрібні Docker Desktop (або Docker на Linux) і bash; docker context ls має вказувати на локальний Docker. Середовище використовує той самий образ PostgreSQL, що й кореневий docker-compose.yml, але окремий файл:
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.
-
Огляд. Подивіться, що є:
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"і підрахунок замовлень за позначками. Чого бракує? -
Два наївні підходи. Спершу запустіть
./labs/l06-pitr/restore.shз порожнімstudent/recovery.conf. Що скаже сервер і чому? Потім додайте тількиrestore_command = 'cp /archive/%f %p'(шлях до архіву в контейнеріrestoreтакий самий,/archive), зновуrestore.shі перегляньте стан таблиці. Запишіть, що сталося зorder_items, і поясніть, чому відновлення «до кінця архіву» не годиться. -
Знайти мить аварії. Лог основного сервера записує 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. На скільки час коміту пізніший за час у лозі? Чи міг би в такий проміжок потрапити інший коміт?
-
Налаштувати відновлення. У
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. -
Перевірити руками. Запити до відновленого сервера:
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;Очікуєте
before20 іmid30, безafter. Перевірте такожSELECT pg_is_in_recovery()іSELECT pg_walfile_name(pg_current_wal_lsn()): перші вісім цифр імені сегмента показують timeline. -
Здача.
./labs/l06-pitr/check.sh. Кожен пункт, що не пройшов, підказує, де шукати причину.
Перевірка
Section titled “Перевірка”./labs/l06-pitr/check.shЧекер працює з сервісом restore у режимі читання й нічого не змінює. Він перевіряє, що:
- сервер працює й вийшов з відновлення (
pg_is_in_recovery() = f); - імена сегментів WAL починаються не з
00000001, тобто відкрито нову timeline; - таблиця
order_itemsіснує, а позиції позначених і початкових замовлень цілі; - усі 20 замовлень
beforeі усі 30midна місці, жодногоafter, а замовлення датасету не постраждали.
Чекер не залежить від того, як ви знайшли мету: час чи LSN, promote чи pause з pg_wal_replay_resume(), recovery_target_inclusive = off із точним часом коміту чи мета трохи раніша.
Часті помилки
Section titled “Часті помилки”Сервер «застряг» у відновленні. 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.
Далі, якщо цікаво
Section titled “Далі, якщо цікаво”У реальній аварії відновлений сервер майже завжди лише джерело: таблицю переносять у бойову базу. Витягніть 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.