MySQL проти PostgreSQL: тренування PITR — перевірте відновлення до того, як воно знадобиться

Було корисно?

День, коли ви дізнаєтеся, чи працюють ваші резервні копії, ніколи не буває спокійним. Це день із панічним потоком у Slack, фінансовим директором, який раптом дбає про політики збереження, і інженером, який клянеться: «ми завжди так робили».

Відновлення до точки в часі (PITR) має бути вашим аварійним виходом: відновіть базу даних саме перед невдалим деплоєм, випадковим видаленням таблиці або помилковою правкою даних, що «торкнулася лише 12 рядків». Але PITR не стане ваша власність, поки ви не відновите, не заміряєте час і не поясните результат тому, хто не хоче вислуховувати виправдання.

PITR — це тренування, а не галочка

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

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

Операційна істина така: резервна копія, яку ви не відновлювали, — це просто стиснутий купа надії.

Цитата, яку варто тримати на стікері (парафраз): «Надія — не стратегія» — часто приписують в операційних колах генералу Гордонові Р. Саллівану.
Парафраз чи ні, сенс болісно точний.

Жарт №1: Єдина річ більш оптимістична за нетестоване відновлення — віра в те, що інцидент почекає до офісних годин.

Факти та невелика історія, які мають значення в продакшені

Деталі про binlog у MySQL та WAL у PostgreSQL — це не дрібниці. Вони пояснюють, чому певні практики PITR працюють, а інші тихо розкладаються до інциденту.

  1. PostgreSQL додав WAL у версії 7.1 (2001), що забезпечило відновлення після збоїв і пізніше потокову реплікацію. PITR ґрунтується на цьому механізмі.
  2. Бінарний журнал MySQL став ключовим для реплікації ще на ранніх стадіях, і PITR у MySQL фактично означає «відновити бекап + застосувати binlog». Операційно реплікація і PITR мають спільні режими відмов.
  3. У PostgreSQL є поняття «таймлайнів», бо відновлення може розгалужувати історію. Якщо ви відновили і підняли вузол, ви створюєте новий таймлайн; ігнорування цього призводить до відтворення неправильного майбутнього.
  4. MySQL підтримує statement-based, row-based і mixed формати binlog. Надійність і детермінованість PITR значно змінюються в залежності від обраного формату.
  5. Архівація WAL через archive_command у PostgreSQL з’явилася до широкого поширення хмарного об’єктного сховища, тому багато команд досі обгортають її shell-скриптами — і саме через це скрипти іноді падають креативно.
  6. MySQL GTID (глобальні ідентифікатори транзакцій) були введені, щоб зробити реплікацію і відновлення більш надійними; при послідовному використанні вони також полегшують міркування про PITR.
  7. PostgreSQL додав механіку backup_label і recovery.signal (замінивши старий recovery.conf), щоб зробити стан відновлення явнішим. Старі інструкції, що посилаються на recovery.conf, досі існують і досі викликають нічні плутанини.
  8. Зміни за замовчуванням binlog_row_image у різних дистрибутивах MySQL впливають на те, скільки даних потрапляє в binlog. Це впливає на час відтворення, витрати на зберігання і на те, чи можна коректно відновити певні рядки.

PITR у MySQL і PostgreSQL: у чому реальна різниця

Що означає «відтворення журналу» в кожній системі

PITR у MySQL відтворює бінарні логи: логічні зміни на рівні SQL (statement-based), зміни на рівні рядків (row-based) або їхню комбінацію. Інструмент відтворення зазвичай mysqlbinlog.

PITR у PostgreSQL відтворює WAL (Write-Ahead Log): фізично-орієнтовану інформацію для редо, яку застосовує сервер під час відновлення. Ви не «застосовуєте WAL» клієнтським інструментом; налаштуйте відновлення, подайте сегменти WAL (з архіву або через стрімінг), і PostgreSQL виконає відтворення.

Очікування щодо базової резервної копії

Базові резервні копії MySQL сильно варіюються: знімки файлової системи (LVM/ZFS), xtrabackup або логічні дампи. PITR з binlog сильно залежить від наявності консистентного базового бекапу, синхронізованого з координатами binlog.

У PostgreSQL історія більш стандартизована: pg_basebackup (або правильний файловий знімок) плюс архівовані WAL — звичний, добре відпрацьований шлях.

Час — брехун (якщо не змусити його поводитися)

У MySQL PITR часто використовує позиції binlog, імена файлів, часові мітки в подіях або GTID. Якщо годинники машин розходяться, «відновити на 14:03:00» перетворюється на інтерпретаційний танець.

У PostgreSQL є recovery_target_time, але він також залежить від обробки часових зон у кластері та таймлайну WAL, на якому ви знаходитесь. Якщо ви відновитеся до точки, якої немає на обраному таймлайні, ви або отримаєте помилку, або опинитесь у несподіваному місці.

Спостережуваність під час відновлення

PostgreSQL відкриває стан відновлення через представлення типу pg_stat_wal_receiver, а логи кажуть, який WAL застосовується. Це досить інспектовано.

Відтворення MySQL через mysqlbinlog | mysql прозоре в сенсі «ви бачите конвеєр», але жахливе в сенсі «який у мене точний прогрес», якщо ви не обгорнете його власною інструментацією.

Одна операційна рекомендація (суб’єктивно)

Якщо ви працюєте з MySQL і дбаєте про PITR, віддавайте перевагу row-based binlogging, якщо немає конкретної причини інакше. Statement-based binlog та недетерміновані функції — це пенсія для адвоката розлучень.

Математика RPO/RTO, яку не можна відмахнутися

Тренування PITR — це не просто «чи можна відновити?» Це «чи можна відновити достатньо швидко і достатньо точно

  • RPO (Recovery Point Objective): скільки даних ви можете втратити. PITR зазвичай націлений на майже нуль, але лише якщо логи повні і доступні.
  • RTO (Recovery Time Objective): скільки часу ви можете бути недоступними. Це визначається часом відновлення бази + часом відтворення логів + часом на валідацію.

Вбивця RTO майже завжди — це ввід/вивід: розпакування бекапів, запис терабайтів на диск і відтворення логів, які ніколи не були призначені для швидкого читання. Якщо ви робите тренування PITR на маленькій тестовій ВМ зі слабким сховищем, ви або панікуватимете без причини, або, ще гірше, повірите в RTO, який не витримає продакшен.

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

Проєктування тренування PITR, яке знаходить реальні баги

Виберіть цільовий інцидент, який можна симулювати

Не робіть абстрактних тренувань. Симулюйте реалістичну відмову:

  • Випадкове DELETE без WHERE (класика, вічна).
  • Погана міграція, яка видаляє індекс і все таймаутиться.
  • Баг в додатку, який записує неправильний tenant ID протягом 15 хвилин.
  • Помилка оператора: виконання «безпечного» скрипту на чужому кластері.

Визначте критерії успіху заздалегідь

Успіх — це не «база даних запустилася». Успіх — це:

  • Відновлено до правильної точки (підтверджено відомими маркерними запитами).
  • Критично важливі таблиці додатку присутні і консистентні.
  • Зареєстровані числа RTO і RPO.
  • Оновлено runbook згідно з тим, що зламалося.

Обов’язково додайте крок «хто має ключі?»

Якщо бекапи зашифровані (а так і має бути), ваше тренування має включати отримання ключа дешифрування з місця, де організація ховає його за квитковою чергою. Якщо для відновлення потрібна конкретна IAM-роль або дозвіл KMS — протестуйте й це.

Тримайте одне нудним: іменування та метадані

Кожен артефакт резервної копії має містити: ID кластера, час початку/кінця, мітку базового бекапу, початкову позицію binlog/WAL і версію програмного забезпечення. Метадані, які сьогодні «очевидні», через шість місяців стають археологією.

Практичні завдання: команди, виводи, рішення

Команди нижче не декоративні. Це саме той вид м’язової пам’яті, який вам потрібен до інциденту. Кожне завдання включає: команду, що означає вивід, і рішення, яке ви приймаєте.

Завдання 1 — Перевірити, чи увімкнено binlog у MySQL і чи він адекватний

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |
+---------------+-------+
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| binlog_row_image | FULL  |
+------------------+-------+

Значення: PITR залежить від binlog; якщо log_bin=OFF, ви не зможете нічого відтворити. binlog_format=ROW — безпечніший вибір за замовчуванням для PITR.

Рішення: Якщо binlog вимкнено або використовується statement-based формат, вважайте PITR «на краще» і виправте конфігурацію перед тим, як оголошувати низький RPO.

Завдання 2 — Перевірити збереження binlog у MySQL (і чи не бреше воно)

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'; SHOW VARIABLES LIKE 'expire_logs_days';"
+--------------------------+--------+
| Variable_name            | Value  |
+--------------------------+--------+
| binlog_expire_logs_seconds | 259200 |
+--------------------------+--------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| expire_logs_days| 0     |
+-----------------+-------+

Значення: Тут binlog видаляються через 3 дні. Якщо ваша політика вимагає 14 днів PITR, ви не відповідаєте вимогам; ви просто тихо живете.

Рішення: Встановіть утримання згідно з вимогами бізнесу і реальністю зберігання. Якщо ви відправляєте binlog кудись ще, локальне утримання може бути коротшим — але тільки якщо відправка перевірена.

Завдання 3 — Перелічити файли binlog і вибрати вікно відтворення

cr0x@server:~$ mysql -e "SHOW BINARY LOGS;"
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000421 |  52428800 |
| mysql-bin.000422 |  52428800 |
| mysql-bin.000423 |   8388608 |
+------------------+-----------+

Значення: На сервері є сегменти binlog. Ви все ще не знаєте, чи вони покривають час інциденту.

Рішення: Визначте, який binlog містить вікно інциденту за допомогою часових міток mysqlbinlog (наступне завдання) або GTID.

Завдання 4 — Перевірити часові мітки binlog навколо інциденту

cr0x@server:~$ mysqlbinlog --base64-output=DECODE-ROWS --verbose --start-datetime="2025-12-30 13:55:00" --stop-datetime="2025-12-30 14:10:00" /var/lib/mysql/mysql-bin.000423 | head -n 25
# at 4
#251230 13:55:02 server id 1  end_log_pos 123 CRC32 0x4f2b1a77  Start: binlog v 4, server v 8.0.36 created 251230 13:55:02
# at 123
#251230 14:02:11 server id 1  end_log_pos 456 CRC32 0x9c4d6e10  Query   thread_id=812 exec_time=0 error_code=0
SET TIMESTAMP=1735567331/*!*/;
BEGIN
# at 456
#251230 14:02:11 server id 1  end_log_pos 892 CRC32 0x19a0f2cb  Table_map: `app`.`users` mapped to number 108

Значення: Ви можете знайти часовий вікно й побачити, які таблиці торкалися. Це допоможе встановити точну точку зупинки.

Рішення: Оберіть --stop-datetime трохи перед шкідливою інструкцією або знайдіть точну позицію/GTID для точного контролю.

Завдання 5 — Витягнути події binlog у файл (не підключайте сліпо через pipe)

cr0x@server:~$ mysqlbinlog --start-datetime="2025-12-30 13:55:00" --stop-datetime="2025-12-30 14:02:10" /var/lib/mysql/mysql-bin.000423 > /tmp/pitr_replay.sql
cr0x@server:~$ tail -n 5 /tmp/pitr_replay.sql
# at 3321
#251230 14:02:10 server id 1  end_log_pos 3456 CRC32 0x2f1b8c3a  Xid = 991228
COMMIT/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /*!*/;
DELIMITER ;
# End of log file

Значення: У вас є детермінований скрипт відтворення для цього вікна. Це аудитується і дозволяє шукати «DROP» перед натисканням Enter.

Рішення: Перевірте на небезпечні інструкції і підтвердіть точку зупинки. У відновленнях високих ставок «довіряти конвеєру» — це те, як пізніше ви будете пояснюватися.

Завдання 6 — Відновити базовий бекап MySQL у staging datadir

cr0x@server:~$ sudo systemctl stop mysql
cr0x@server:~$ sudo rsync -aH --delete /backups/mysql/base/2025-12-30_1200/ /var/lib/mysql/
cr0x@server:~$ sudo chown -R mysql:mysql /var/lib/mysql
cr0x@server:~$ sudo systemctl start mysql
cr0x@server:~$ mysqladmin ping
mysqld is alive

Значення: Базове відновлення пройшло і MySQL запустився. Це не говорить нічого про те, чи дані відповідають потрібній точці відновлення.

Рішення: Зафіксуйте координати binlog або стан GTID із метаданих бекапу, потім застосуйте binlog до цільового часу.

Завдання 7 — Застосувати скрипт відтворення MySQL і слідкувати за помилками

cr0x@server:~$ mysql --show-warnings < /tmp/pitr_replay.sql
Warning (Code 1287): 'SET @@SESSION.GTID_NEXT' is deprecated and will be removed in a future release.

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

Рішення: Якщо бачите помилки типу «Table doesn’t exist», зупиніться. Ваш бекап + діапазон binlog неконсистентні; виправте початкові координати.

Завдання 8 — Перевірити стан GTID у MySQL після PITR (якщо використовуєте GTID)

cr0x@server:~$ mysql -e "SHOW VARIABLES LIKE 'gtid_mode'; SELECT @@global.gtid_executed\G"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_mode     | ON    |
+---------------+-------+
*************************** 1. row ***************************
@@global.gtid_executed: 8b3f9b75-9c67-11ee-9b7d-0242ac120002:1-889122

Значення: GTID показують, які транзакції існують. Після PITR це важливо, якщо ви маєте намір реплікуватися від/до цього інстансу.

Рішення: Якщо плануєте повертатися в топологію реплікації, переконайтеся, що набори GTID узгоджені з вашим планом. Інакше ви створите реплікаційну плутанину, що виглядатиме як корупція даних.

Завдання 9 — Перевірити, чи налаштовано архівацію WAL у PostgreSQL і чи вона дійсно працює

cr0x@server:~$ psql -d postgres -c "SHOW wal_level; SHOW archive_mode; SHOW archive_command; SHOW archive_timeout;"
 wal_level 
-----------
 replica
(1 row)

 archive_mode 
--------------
 on
(1 row)

                     archive_command                     
---------------------------------------------------------
 test ! -f /wal_archive/%f && cp %p /wal_archive/%f
(1 row)

 archive_timeout 
-----------------
 60s
(1 row)

Значення: Архівація увімкнена, а команда копіює WAL в локальний каталог архіву. Це початок, але не доказ.

Рішення: Підтвердіть, що файли WAL дійсно з’являються і що відмови фіксуються алертами. «Налаштовано» — ще не означає «працює».

Завдання 10 — Перевірити backlog архівації PostgreSQL і недавню активність WAL

cr0x@server:~$ ls -lh /wal_archive | tail -n 5
-rw------- 1 postgres postgres 16M Dec 30 14:00 000000010000002A0000009F
-rw------- 1 postgres postgres 16M Dec 30 14:01 000000010000002A000000A0
-rw------- 1 postgres postgres 16M Dec 30 14:02 000000010000002A000000A1
-rw------- 1 postgres postgres 16M Dec 30 14:03 000000010000002A000000A2
-rw------- 1 postgres postgres 16M Dec 30 14:04 000000010000002A000000A3

Значення: Сегменти WAL архівуються, імена послідовні, часові мітки виглядають здоровими.

Рішення: Якщо архів застарів, припиніть робити вигляд, що PITR існує. Виправте архівацію; самі базові бекапи без WAL — це не PITR.

Завдання 11 — Створити базовий бекап PostgreSQL, який дійсно можна відновити

cr0x@server:~$ sudo -u postgres pg_basebackup -D /backups/pg/base/2025-12-30_1200 -Fp -Xs -P -R
waiting for checkpoint
22994/22994 kB (100%), 1/1 tablespace
22994/22994 kB (100%), 1/1 tablespace

Значення: Ви зробили фізичний базовий бекап і включили стрімінг WAL (-Xs), плюс записали конфігурацію для реплікації (-R). Для PITR вам все ще потрібні архівовані WAL або доступні стрімінги WAL.

Рішення: Зафіксуйте мітку бекапу і часову мітку. Переконайтеся, що відповідний діапазон архіву WAL існує і зберігається.

Завдання 12 — Відновити базовий бекап PostgreSQL у новий datadir

cr0x@server:~$ sudo systemctl stop postgresql
cr0x@server:~$ sudo rm -rf /var/lib/postgresql/16/main
cr0x@server:~$ sudo rsync -aH /backups/pg/base/2025-12-30_1200/ /var/lib/postgresql/16/main/
cr0x@server:~$ sudo chown -R postgres:postgres /var/lib/postgresql/16/main

Значення: Файли кластера розміщені. Тепер ви вирішуєте, чи робите PITR (відновлення), чи просто стартуєте клона.

Рішення: Для PITR налаштуйте цілі відновлення і restore_command; не запускайте як звичайний primary, якщо не збираєтесь форкати таймлайн.

Завдання 13 — Налаштувати PITR у PostgreSQL: restore_command і цільовий час

cr0x@server:~$ sudo -u postgres bash -c 'cat >> /var/lib/postgresql/16/main/postgresql.conf <<EOF
restore_command = '\''cp /wal_archive/%f %p'\''
recovery_target_time = '\''2025-12-30 14:02:10+00'\''
recovery_target_action = '\''pause'\''
EOF'
cr0x@server:~$ sudo -u postgres touch /var/lib/postgresql/16/main/recovery.signal
cr0x@server:~$ sudo systemctl start postgresql

Значення: PostgreSQL запуститься в режимі відновлення, витягне WAL з архіву, відтворить до цільового часу і призупиниться.

Рішення: Пауза — навмисна: ви перевіряєте дані перед промоцією. Якщо ви автоматично промотуєте, можна випадково затвердити неправильну точку як «істину».

Завдання 14 — Спостерігати прогрес відновлення PostgreSQL і підтвердити досягнення цілі

cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
 pg_is_in_recovery 
-------------------
 t
(1 row)

cr0x@server:~$ sudo -u postgres psql -c "SELECT now(), pg_last_wal_replay_lsn();"
              now              | pg_last_wal_replay_lsn 
------------------------------+-------------------------
 2025-12-30 14:02:12.123+00   | 2A/A100F2B0
(1 row)

Значення: Ви в режимі відновлення і маєте LSN останнього відтвореного WAL. Логи зазвичай повідомляють, що ви досягли recovery_target_time і призупинилися.

Рішення: Зараз виконайте перевірочні запити. Якщо все правильно — промотуйте; якщо ні — підкоригуйте ціль і перезапустіть відновлення.

Завдання 15 — Промотувати PostgreSQL після валідації

cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_promote(wait_seconds => 60);"
 pg_promote 
------------
 t
(1 row)

cr0x@server:~$ sudo -u postgres psql -c "SELECT pg_is_in_recovery();"
 pg_is_in_recovery 
-------------------
 f
(1 row)

Значення: Сервер тепер записувальний, і створився новий таймлайн.

Рішення: Вирішіть, як цей інстанс повертається в топологію. Промотований PITR відновлений інстанс не є автоматично безпечним для підключення так, ніби нічого не сталося.

Завдання 16 — Перевірити дані на межі додатку (не лише «кількість рядків»)

cr0x@server:~$ psql -d appdb -c "SELECT COUNT(*) FROM users; SELECT MAX(updated_at) FROM users;"
  count  
---------
 1284932
(1 row)

          max          
-----------------------
 2025-12-30 14:02:05+00
(1 row)

Значення: Кількість і часові позначки дають швидку санітарну перевірку. Максимальна мітка часу, що перед інцидентом, — добрий знак.

Рішення: Якщо дані виглядають неправильно, не промотуйте (PostgreSQL) і не перемикайте трафік (MySQL). Підкоригуйте точку відновлення й запустіть заново.

Завдання 17 — Заміряти вузькі місця I/O при відновленні (Linux-базові інструменти, що вирішують RTO)

cr0x@server:~$ iostat -xm 2 3
Linux 6.5.0 (server) 	12/31/2025 	_x86_64_	(16 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          12.10    0.00    6.20   38.70    0.00   42.00

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz  aqu-sz  %util
nvme0n1        1200.0  98000.0     0.0    0.0    1.2    81.7   2200.0 180000.0     0.0    0.0    6.8    81.8   16.2   99.0

Значення: %util близько 99% і високий iowait означають, що сховище — вузьке місце. Ваше відновлення не пришвидшиться додаванням CPU.

Рішення: Перемістіть відновлення на швидші томи, використайте локальний NVMe для тимчасових даних, паралелізуйте розпакування або зменшіть обсяг відтворення, роблячи частіші базові бекапи.

Завдання 18 — Підтвердити синхронізацію часу перед відновленням за часом

cr0x@server:~$ timedatectl
               Local time: Wed 2025-12-31 10:12:03 UTC
           Universal time: Wed 2025-12-31 10:12:03 UTC
                 RTC time: Wed 2025-12-31 10:12:03
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Значення: Годинник системи синхронізований. Без цього «відновити до 14:02» стає гаданням.

Рішення: Якщо годинники не синхронізовані, надавайте перевагу відновленню за позицією (MySQL) або LSN (PostgreSQL) і виправте NTP до наступного тренування.

Три міні-історії з корпоративного життя

Міні-історія 1: Інцидент, спричинений неправильним припущенням

Середня SaaS-компанія запускала MySQL на керованих ВМ. У них «був PITR», бо існували нічні базові бекапи і binlog був увімкнений. Припущення: агент копіює binlog в об’єктне сховище, а утримання у бакеті «навічно-ish».

Розробник прокинув міграцію, яка неправильно перебудувала велику таблицю. Це не зламало додаток одразу; просто почали записуватися неправильно дані. Через чотири години клієнт помітив проблему. Інцидент-командир попросив PITR «на 30 хвилин до деплою».

Відновлення почалося гладко: бекап відновили, MySQL запустився, і команда почала застосовувати binlog. Потім відтворення натрапило на прогалину. Файл binlog для 40-хвилинного вікна був відсутній. Не пошкоджений. Відсутній.

Корінь проблеми був не екзотичним. Агент копіювання мовчки падав через помилки дозволів після зміни політики бакета. На первинному вузлі binlog вже встигли видалити через занадто коротке утримання. Їхній «PITR» мав чотиригодинну діру посеред дня, ніби погана пам’ять.

Виправлення не вимагало нового вендора. Воно було нудним: алерти на помилки архівації, продовжити утримання до моменту, поки відправка не буде доведена, і додати крок у тренуванні: «перелічити binlog в архіві для вікна інциденту».

Міні-історія 2: Оптимізація, що зіграла проти

Інша організація запускала PostgreSQL з архівацією WAL. Тренування відновлення були «занадто повільні», тож хтось оптимізував, сильно стискаючи WAL і пропихуючи їх через однопотоковий pipeline шифрування+завантаження на вузлі бази. CPU виріс, але архів став меншим. Усі раділи.

Радість тривала до першої справжньої спроби PITR. Кластер відновлення міг витягти WAL, але декомпресія стала вузьким місцем, а однопотоковий pipeline відставав у пікові періоди запису. Під час інциденту найсвіжіші сегменти WAL ще не були заархівовані. Відновлення зупинилося перед цільовим часом.

Команда намагалася «просто почекати, поки архів наздожене». Це не RTO; це молитва з зустріччю в календарі.

Після інциденту виправлення були парадоксальними: знизити рівень стиснення, перемістити шифрування/завантаження з первинного вузла, паралелізувати pipeline і встановити явні SLO типу «WAL архівується протягом X секунд». Вони прийняли більші витрати на зберігання і повернули передбачуваність.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Компанія, пов’язана з фінансами, мала щотижневе тренування PITR. Воно не було гламурним. Це був чекліст, квиток і короткий звіт. Воно також дратувало всіх настільки, щоб бути ефективним.

Під час рутинного тренування інженер помітив, що відновлення працює, але завжди приземляється на п’ять-сім хвилин пізніше вказаного часу. Дані «виглядали нормально», але невідповідність його непокоїла. Він порився і знайшов плутанину в часових зонах: runbook використовував локальний час, а recovery_target_time очікував UTC. Вони виправили runbook, стандартизували UTC і додали маркерну таблицю з транзакцією, яка вставляє часову мітку під час деплою.

Через два місяці сталася продукційна подія: пакетна задача оновлювала неправильний партиціон протягом восьми хвилин. Команда відновилася до маркера і швидко верифікувала. Вони вклалися в RTO, бо вже обговорювали дурну деталь часових зон у низько-ставковому тренуванні.

Жарт №2: Часові зони як лаг реплікації — усі погоджуються, що вони існують, а потім вони псують ваш післяобід.

План швидкої діагностики

Відновлення зазвичай ламаються або повільніють з кількох передбачуваних причин. Трюк — визначити яку саме за хвилини, а не години. Ось порядок, який я використовую, коли хтось каже «PITR повільний/зламаний».

Перше: чи відсутні потрібні логи?

  • MySQL: підтвердіть покриття binlog для цільового часу. Чи є файли binlog локально або в архіві для всього вікна?
  • PostgreSQL: підтвердіть покриття архіву WAL і що restore_command може дістати наступний потрібний сегмент.

Якщо логи відсутні, припиніть оптимізації. У вас не проблема з продуктивністю; у вас межа втрати даних.

Друге: чи консистентний базовий бекап із початком відтворення?

  • MySQL: бекап має відповідати координатам binlog/набору GTID, з яких ви починаєте. Невідповідність призводить до помилок про відсутні таблиці або duplicate key під час відтворення.
  • PostgreSQL: бекап має бути повним і включати необхідні файли; WAL, потрібний від початку бекапу, має існувати.

Третє: це I/O, CPU чи мережа?

  • I/O bound: високий iowait, диски на високому завантаженні, швидкість відновлення вирівнюється незалежно від CPU.
  • CPU bound: стиснення/декомпресія/шифрування навантажують ядра, диски не повно завантажені.
  • Network bound: повільний доступ до архіву, висока латентність, обмеження пропускної здатності.

Четверте: чи обрана правильна ціль і таймлайн?

  • PostgreSQL: невідповідність таймлайнів після промоції — класика. Відновлення з неправильного таймлайну видає «requested timeline does not contain minimum recovery point» або приземляє в несподіваному місці.
  • MySQL: відновлення за часом вразливе до зсуву годинника і плутанини часових зон; позиція/GTID надійніші, коли можливо.

П’яте: чи ви валідуєте правильне?

«База запустилася» — це не валідація. Перевіряйте набір бізнес-запитів, обмежень і ключових прикладних потоків. Якщо ви не можете їх визначити, ваш критерій успіху відновлення — переважно суб’єктивні відчуття.

Поширені помилки: симптом → корінь → виправлення

1) «PITR зупиняється раніше і не досягає цільового часу»

Симптоми: відновлення PostgreSQL призупиняється до цілі; логи згадують відсутній WAL. Відтворення MySQL закінчується, не досягнувши часу інциденту.

Корінь: прогалина в архіві WAL/binlog, занадто коротке утримання або збій відправки.

Виправлення: Алертуйте про помилки archive_command, перевіряйте повноту архіву щодня, подовжіть утримання і зберігайте логи незалежно від первинного вузла.

2) «Відтворення binlog у MySQL кидає помилки про відсутні таблиці»

Симптоми: ERROR 1146 (42S02): Table '...' doesn't exist під час відтворення.

Корінь: Базовий бекап відновлений із часу T, але відтворення почалося з binlog, створених до того, як таблиця існувала (або після її видалення/пересоздання).

Виправлення: Починайте відтворення з точної позиції binlog/GTID, зафіксованої з базовим бекапом; не вгадуйте лише за часовими мітками.

3) «Відновлення PostgreSQL застрягає на тому самому сегменті WAL»

Симптоми: Логи повторно показують спроби відновити той самий %f.

Корінь: restore_command повертає успіх, але фактично не поміщає файл (або поміщає обрізаний файл). Класика, коли скрипти гублять помилки.

Виправлення: Змушуйте restore_command падати голосно. Перевіряйте розмір файлу і checksum. Уникайте wrapper-скриптів з «always exit 0».

4) «Відновлення повільне, але CPU низький»

Симптоми: Тривалий час відновлення; iostat показує насичені диски; CPU здебільшого вільний.

Корінь: Межа пропускної здатності сховища/IOPS; занадто багато дрібних випадкових записів під час відтворення; WAL/binlog на повільному носії.

Виправлення: Використовуйте швидкі тимчасові томи для відновлення, відокремте дані від логів, подумайте про частіші базові бекапи і переконайтеся, що шлях отримання архіву не обмежений.

5) «Відновлення швидке, а потім наприкінці падає з повідомленнями про корупцію»

Симптоми: PostgreSQL скаржиться на недійсну контрольну точку; MySQL не стартує через проблеми з InnoDB log.

Корінь: Неповний/пошкоджений базовий бекап або файловий знімок зроблений без правильної паузи.

Виправлення: Використовуйте інструменти, призначені для консистентних бекапів (pg_basebackup, перевірені процедури знімків, xtrabackup) і перевіряйте цілісність бекапу в тренуванні.

6) «PITR приземляється в неправильний час»

Симптоми: Відновлений стан включає зміни, які мали бути виключені, або пропускає зміни, які мали бути включені.

Корінь: Плутанина часових зон, зсув годинника або неправильне розуміння меж включення/виключення під час відтворення.

Виправлення: Стандартизуйте UTC для часових міток інциденту, вбудуйте маркер-транзакцію і надавайте перевагу цілям за позицією/LSN/GTID, коли це можливо.

7) «Все відновлюється, але реплікація після цього — хаос»

Симптоми: Репліки PostgreSQL не можуть слідувати; MySQL реплікація падає через виконані GTID або дублікати.

Корінь: Ви промотували відновлений інстанс без планування нової топології, таймлайну або узгодження наборів GTID.

Виправлення: Розглядайте PITR як форк: вирішіть авторитетний вузол, відбудуйте репліки з нього і задокументуйте процедуру приєднання.

Чеклісти / покроковий план

Щотижневе тренування PITR (90 хвилин, навмисно нудне)

  1. Виберіть сценарій: оберіть відоме «погане змінення» з останнього тижня (або створіть нешкідливу маркерну транзакцію).
  2. Виберіть цільову точку: визначте час відновлення в UTC і зафіксуйте його.
  3. Підтвердьте покриття логів: перевірте, що binlog/WAL існують для всього вікна.
  4. Відновіть базовий бекап: на ізольованій інфраструктурі з подібним класом сховища до продакшену.
  5. Відтворіть логи до цілі: MySQL через mysqlbinlog; PostgreSQL через конфігурацію відновлення.
  6. Валідація: 5–10 критичних запитів, один smoke-тест додатку і одна перевірка цілісності.
  7. Записати часи: час відновлення бази, час відтворення, час валідації.
  8. Звіт: що впало, що було повільним, що змінилося в runbook.

Перед тим як казати «у нас є PITR» (мінімальний бар)

  • Базові резервні копії консистентні і відновлювані без героїчних зусиль.
  • Архівація логів повна, моніториться і зберігається згідно вимог.
  • Runbook відновлення включає кроки з IAM/KMS/доступом до ключів.
  • Числа RTO/RPO виміряні, а не побажані.
  • Існують валідаційні запити, за які хтось відповідає.

Кроки для дрилю, специфічні для MySQL

  1. Задокументуйте binlog файл/позицію або набір GTID базового бекапу.
  2. Підтвердьте binlog_format і утримання.
  3. Витягніть SQL для відтворення в файл і перегляньте на предмет небезпечних команд.
  4. Застосуйте відтворення до відновленого інстансу; зупиніться на першій помилці і виправте невідповідність координат.
  5. Вирішіть план топології: чи це новий primary, тимчасовий клон для аналітики чи джерело для відбудови реплік?

Кроки для дрилю, специфічні для PostgreSQL

  1. Підтвердьте архівацію WAL і свіжість архіву.
  2. Відновіть базовий бекап; переконайтеся в наявності recovery.signal і правильного restore_command.
  3. Використовуйте recovery_target_action='pause' для тренувань, щоб валідувати перед промоцією.
  4. Після промоції зафіксуйте новий таймлайн і сплануйте відбудову реплік відповідно.
  5. Перевірте, що архів містить WAL через зміни таймлайнів (поширена довгострокова пастка).

Поширені питання

1) Чи те саме PITR і висока доступність?

Ні. HA тримає вас у роботі при збої вузлів. PITR відновлює після логічних катастроф: погані деплои, видалення, корупція, спричинена людьми. Вам потрібні обидва.

2) Що «легше» для PITR: MySQL чи PostgreSQL?

Робочий процес PITR у PostgreSQL більш стандартизований: базовий бекап + архів WAL + цілі відновлення. PITR у MySQL може бути чистим, але значно залежить від методу бекапу та формату binlog.

3) Використовувати цілі за часом чи за позицією?

Позиція (позиція binlog/GTID у MySQL, LSN у PostgreSQL) зазвичай більш детермінована. Цілі за часом зручні для людей, але крихкі, якщо годинники і часові зони не дисципліновані.

4) Як часто робити базові бекапи, якщо є логи?

Так часто, як дозволяє ваш час відтворення. Великі розриви означають довше відтворення логів. Якщо відтворення WAL/binlog для доби займає шість годин, ваш RTO уже вирішений вами.

5) Чи можна робити PITR з логічних бекапів (mysqldump/pg_dump)?

Не надійно в строгому сенсі PITR. Логічні дампи фіксують стан у точці часу, але відтворення до точної миті громіздке і повільне. Використовуйте їх для портативності; для PITR використовуйте фізичні бекапи плюс логи.

6) Який найчистіший спосіб валідувати відновлення PITR?

Малий набір бізнес-інваріантів: ключові підрахунки, маркери останнього оновлення, перевірки референційної цілісності і легкий smoke-тест додатку. Якщо ваша валідація — «SELECT 1», ви практикуєте заперечення.

7) А як щодо шифрування і ключів?

Якщо бекапи або архіви зашифровані, ваше тренування має включати отримання ключів і дозволів. Найпоширеніша причина «ми не можемо відновити» — це не дані, а доступ.

8) Чи можна PITR відновити через великі версійні оновлення?

Зазвичай — ні, в місці для фізичних бекапів. Великі версійні зміни часто вимагають логічної міграції або інструментів оновлення. Ваш план PITR має передбачати «відновити на тій самій мажорній версії», а потім оновити, якщо потрібно.

9) Як не допустити, щоб відновлений інстанс випадково підключився до продакшену?

Розміщуйте відновлення в ізольованих мережах, змінюйте облікові дані і явно відключайте вихідні з’єднання до продакшн-сервісів. Також: перейменуйте кластер і додайте голосні банери в моніторингу.

10) Яка одна метрика, яку я хотів би, щоб відстежували всі команди?

«Час від початку інциденту до валідації відновлених даних». Не «бекап пройшов». Не «WAL заархівовано». Кінцевий результат — це те, що відчуває бізнес.

Наступні кроки, що справді знижують ризик

Якщо цього кварталу ви нічого більше не зробите — зробіть це: заплануйте тренування PITR, проведіть його на інфраструктурі, що нагадує продакшен, і запишіть точно, де ви заплуталися. Це заплутування — ваше майбутнє відключення.

  • Оберіть одну базу даних (ту, що болісніша) і виконайте відновлення до відомої маркерної миті.
  • Інструментуйте пайплайн: алерти на збої відправки binlog/WAL і свіжість архіву.
  • Стандартизуйте цілі: UTC-мітки часу плюс позиція/LSN/GTID, коли можливо.
  • Виміряйте RTO і вирішіть, чи витрачати гроші на швидше сховище, частіші базові бекапи або менші вікна відтворення.
  • Оновіть runbook одразу після тренування, поки біль ще свіжий і уроки чесні.

PITR — це не функція, яку ви вмикаєте. Це навичка, яку ви тренуєте. Відновлення не дбає про те, наскільки впевнено ви звучите на зустрічах.

← Попередня
Docker “invalid reference format” — опечатка, яка забирає години (і як виправити)
Наступна →
Пояснення «write hole» у ZFS: хто під загрозою і чому ZFS його уникає

Залишити коментар