В четвёртом уроке мы посмотрели, как WAL используется для репликации в реальном времени. Финальный важнейший use case того же WAL — Point-In-Time Recovery: возможность восстановить БД на любой момент времени в прошлом, а не только на момент последнего бэкапа.
Это критично для catastrophic recovery («admin случайно сделал DELETE FROM orders; без WHERE»), для compliance («регулятор просит показать состояние БД на дату X»), и для дебага продакшена («когда именно появилась эта повреждённая строка»). Никакой pg_dump это не даёт — он сохраняет только моментальный снимок.
Формула PITR проста:
base backup (момент T0) + архив WAL от T0 до T1 = состояние БД на момент T1
Любая дата T1 ≥ T0, до которой у нас есть непрерывный архив WAL — достижима.
Что нужно для PITR
Три компонента:
- Base backup — целостная физическая копия
$PGDATAв какой-то момент T0. Создаётся черезpg_basebackupили production-tool типа pgBackRest. Не дамп черезpg_dump(он логический и не подходит для PITR). - Archive of WAL segments — все WAL-сегменты от момента T0 до текущего момента. Каждый сегмент сохраняется куда-то «навсегда» — на S3, NFS, отдельный диск.
- Recovery target — на какой момент восстанавливать. Можно по времени (
recovery_target_time), по транзакции (recovery_target_xid), по LSN (recovery_target_lsn) или по именованной точке (recovery_target_name).
Base backup сделан в T0. Архив WAL идёт непрерывно. Recovery target T1 может быть в любой момент между T0 и текущим моментом. Recovery: восстанавливаем базу из backup'а в T0 и применяем WAL до T1.
archive_command: настройка архива WAL
На primary включается архивирование:
# postgresql.conf
wal_level = replica # или logical
archive_mode = on
archive_command = 'aws s3 cp %p s3://wal-archive/postgres/%f'
Здесь %p — путь до текущего WAL-сегмента в pg_wal/, %f — имя сегмента. Postgres вызывает archive_command для каждого готового сегмента (когда он заполнен и в archive_status/ появилась .ready-метка). После успешного выполнения (exit code 0) — переименовывает .ready в .done, и сегмент становится кандидатом на удаление при следующем checkpoint.
Критично: archive_command обязан возвращать 0 только при гарантированной durability. Если команда вернёт 0, а файл потом потеряется — recovery будет невозможен. Поэтому никаких cp /a /b && rm /a-однострочников. Используют либо production-tool (см. ниже), либо как минимум скрипт с fsync и проверкой.
Если archive_command падает (exit code != 0), Postgres будет ретраить вечно, и pg_wal/ будет расти бесконечно. Это причина 99% инцидентов «закончилось место на disk» в продакшене с архивом.
Проверка настроек архивирования. На реальной БД archive_mode = on/always, archive_command заполнен. В pglite archive_command = '' и archive_mode = off, но видеть параметры можно.
Статистика архивирования: сколько сегментов архивировано, был ли последний fail. last_failed_wal — название сегмента, на котором archive_command упал в последний раз. На реальной БД смотреть после инцидентов.
Recovery target: на какой момент возвращаться
Когда дело дошло до recovery, на новом (или сброшенном) сервере конфигурируется:
# postgresql.conf на восстанавливаемой ноде
restore_command = 'aws s3 cp s3://wal-archive/postgres/%f %p'
recovery_target_time = '2024-12-15 14:30:00 UTC'
recovery_target_action = 'promote' # после достижения target — открыть на запись
И создаётся пустой файл recovery.signal (в Postgres 12+ вместо старого recovery.conf). После старта Postgres входит в recovery mode, по очереди запрашивает WAL-сегменты через restore_command и применяет их, пока не достигнет target.
Варианты recovery_target_*:
recovery_target_time = '2024-12-15 14:30:00 UTC'— самое частое: «до этой минуты». Точность — до момента COMMIT (на reload-границах между WAL-записями).recovery_target_xid = 12345— до конкретной транзакции включительно. Используется, когда известен xid проблемной операции.recovery_target_lsn = '0/1A2B3C4D'— до конкретного LSN.recovery_target_name = 'before_migration'— до именованной точки, созданной заранее черезSELECT pg_create_restore_point('before_migration'). Используется для recovery до DDL-релиза.recovery_target = 'immediate'— остановиться как только base backup консистентен (без replay incremental WAL). Самое быстрое recovery, но «приедешь» в момент T0.
Дополнительно recovery_target_inclusive (default true) определяет, применять ли саму target-запись или остановиться до неё. По умолчанию — включительно.
Timelines: что произойдёт после promote
После того как recovery достигла target и сервер сделал pg_promote, создаётся новый timeline. Это критично, чтобы избежать confusing’а: если ты восстановился на момент T1 и пишешь новые транзакции, эти новые транзакции получат LSN’ы, которые в старом WAL-потоке уже принадлежали другим транзакциям (тем, что были после T1 на «оригинальной» БД).
Решение — timeline ID. После promote все новые WAL-сегменты получают новое имя: 00000002... вместо 00000001... (первые 8 hex). Это инкарнации.
Timeline 1 (старая primary): продолжает свой WAL после T1. Timeline 2 (восстановленная база): WAL расходится в T1. Это две параллельные вселенные, которые больше не сольются.
Postgres хранит .history-файлы в pg_wal/: каждый описывает, от какого timeline и при каком LSN произошёл fork. Это нужно для recovery, который проходит через несколько timelines (например, ты восстановил до T1, поработал, а через час понял, что нужно ещё раньше — теперь recovery должна перейти из TL2 в TL1 в правильной точке).
Production tools: pgBackRest и WAL-G
Голый pg_basebackup + archive_command = 'cp/s3 cp' работает, но в продакшене никто так не делает. Причины:
- Инкрементальные бэкапы. Полный backup БД 10 TB занимает 8 часов и 10 TB места. Хочется делать инкременты: «WAL + diff страниц с последнего полного». pgBackRest умеет.
- Параллелизация.
pg_basebackupиспользует одну connection и одну threads. На большой БД это медленно. pgBackRest качает в несколько потоков и параллелит compression. - Проверка целостности. Backup, который никто не проверил, — это не backup. Production-tool регулярно делают
verify— пытаются восстановить случайные блоки. - Retention policy. «Держать 7 полных бэкапов + WAL последние 30 дней». Голый skripts не справится без сложной логики.
- Encryption. Шифрование backup’ов прозрачно.
- Cloud-storage native. S3 multipart, retry, eventual consistency — стандартный flag.
Главные tools:
- pgBackRest — гибкий, написан на C, поддерживает delta-backup, parallel restore, encryption, S3/Azure/GCS. Производственный стандарт для on-premise и средних cloud.
- WAL-G — Go, легче в setup, оптимизирован под S3 и аналоги. Появился в Yandex, активно развивается.
- Barman — Python, чуть медленнее, но удобный для multi-cluster управления.
- Облачные managed-сервисы (RDS, Cloud SQL) — встроенное PITR с retention до 35 дней, восстановление одной командой через консоль.
Конкретные команды зависят от инструмента. Пример pgBackRest:
# initial setup на backup-сервере
pgbackrest --stanza=production --type=full backup
# восстановление до момента
pgbackrest --stanza=production \
--type=time \
--target='2024-12-15 14:30:00+00' \
restore
Архивирование настраивается через:
archive_command = 'pgbackrest --stanza=production archive-push %p'
Восстановление одной таблицы вместо всего кластера
Часто после accident’а нужно вернуть одну таблицу в состояние «до DROP», а не всю БД. PITR в стандартной поставке не умеет «выборочное» recovery — он восстанавливает весь кластер. Но финт известный:
- Делается PITR на отдельный временный сервер до момента T1.
- На временном сервере делается
pg_dump --table=ordersнужной таблицы. - На основном сервере делается
DROP TABLE IF EXISTS orders_recovered; pg_restore -t orders ...— в отдельную таблицу. - Сравниваем содержимое, переименовываем, прицельно мерджим — что-то восстанавливаем, что-то оставляем.
Это занимает больше времени (поднять второй кластер, разогнать там БД), но не трогает работающий production. Production-tools (pgBackRest) умеют ускорять этот процесс через partial restore только нужных tablespaces.
Что нельзя восстановить через PITR
- Дату до самого старого base backup’а. Если самый ранний имеющийся backup сделан в 2 утра вчера, восстановить на позавчера невозможно.
- Состояние на момент, когда archive_command падал. Если в архиве gaps в WAL — recovery остановится на gap’е. Поэтому failed_count > 0 — критический алерт.
- Точно в COMMIT момент.
recovery_target_timeопирается на timestamp в COMMIT-записях WAL. Гранулярность — единица WAL-записи, обычно достаточно для бизнес-целей. Но «остановиться между двумя SQL внутри одной транзакции» нельзя. - Хирургический rollback одной таблицы без остальных изменений. Только через временный кластер + ручной merge (см. предыдущий раздел).
RPO и RTO: сколько данных можно потерять
При планировании PITR-стратегии формулируются два параметра:
- RPO (Recovery Point Objective) — сколько данных мы готовы потерять. Если архив WAL уходит на S3 раз в 5 минут — RPO = 5 минут. Если синхронно — RPO = 0.
- RTO (Recovery Time Objective) — сколько времени восстанавливаем БД. Зависит от скорости download base backup + скорости replay WAL.
Чем меньше RPO/RTO, тем дороже инфраструктура. Realistic baseline для production: RPO = 1 минута (через archive_timeout = 60s + S3), RTO = 30 минут для 1 TB БД на NVMe (full restore + replay). Для критичных систем с RPO = 0 используют synchronous replication + PITR как «второй уровень».
Чек-лист
- PITR = base backup + архив WAL → восстановление на любой момент между T0 и now.
- archive_command — команда, вызываемая Postgres’ом для каждого готового WAL-сегмента. Обязана возвращать 0 только при гарантированной durability.
- Падение archive_command → бесконечный рост
pg_wal/. Мониторьpg_stat_archiver.failed_count. - Recovery target: time / xid / lsn / name / immediate. С
recovery_target_action = 'promote'после достижения target БД открывается на запись. - Timelines разделяют параллельные истории WAL после promote. Каждый имеет
.history-файл. - Production-tools: pgBackRest, WAL-G, Barman. Голые
pg_basebackup + s3 cp— только для proof-of-concept. - RPO/RTO — формализованные требования бизнеса, определяющие частоту backup’ов и архивирования.
- PITR требуется регулярно проверять restore — backup, который никто не восстанавливал, не существует.