Learning Platform
Урок 14.05 · 24 мин
Продвинутый
PITRBackupRecoveryArchiveTimelines

В четвёртом уроке мы посмотрели, как 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

Три компонента:

  1. Base backup — целостная физическая копия $PGDATA в какой-то момент T0. Создаётся через pg_basebackup или production-tool типа pgBackRest. Не дамп через pg_dump (он логический и не подходит для PITR).
  2. Archive of WAL segments — все WAL-сегменты от момента T0 до текущего момента. Каждый сегмент сохраняется куда-то «навсегда» — на S3, NFS, отдельный диск.
  3. Recovery target — на какой момент восстанавливать. Можно по времени (recovery_target_time), по транзакции (recovery_target_xid), по LSN (recovery_target_lsn) или по именованной точке (recovery_target_name).
PITR: концепция времени

Base backup сделан в T0. Архив WAL идёт непрерывно. Recovery target T1 может быть в любой момент между T0 и текущим моментом. Recovery: восстанавливаем базу из backup'а в T0 и применяем WAL до T1.

Временная шкала PITRбаза backup'a → архив WAL → текущий момент primary
T0base backup (1 раз в день/неделю)
WAL стримarchive_command → S3
T1 (recovery target)DELETE случился в 14:42:15
WAL продолжаетсяно мы остановимся в T1
now (T2)current primary LSN
Восстановление: scratch-host → распаковать base backup (T0) → применить WAL до recovery_target_time = T1 → promoteновый timeline, отделённый от старого

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, но видеть параметры можно.

PostgreSQL

Статистика архивирования: сколько сегментов архивировано, был ли последний fail. last_failed_wal — название сегмента, на котором archive_command упал в последний раз. На реальной БД смотреть после инцидентов.

PostgreSQL

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). Это инкарнации.

Timelines: что произошло после promote

Timeline 1 (старая primary): продолжает свой WAL после T1. Timeline 2 (восстановленная база): WAL расходится в T1. Это две параллельные вселенные, которые больше не сольются.

Timeline 1 (старая primary, до катастрофы)WAL имена: 00000001...
T0 (base)до бэкапа
T1 (target)точка раскола
T2 (после T1 на TL1)прежние транзакции продолжались
Timeline 2 (recovery до T1 + promote)WAL имена: 00000002...
T0 (base)тот же base backup
T1 (promote)расходимся от TL1
T2' (новые транзакции TL2)не пересекаются с TL1
.history-файл00000002.history описывает 'forked from timeline 1 at LSN 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 — он восстанавливает весь кластер. Но финт известный:

  1. Делается PITR на отдельный временный сервер до момента T1.
  2. На временном сервере делается pg_dump --table=orders нужной таблицы.
  3. На основном сервере делается DROP TABLE IF EXISTS orders_recovered; pg_restore -t orders ... — в отдельную таблицу.
  4. Сравниваем содержимое, переименовываем, прицельно мерджим — что-то восстанавливаем, что-то оставляем.

Это занимает больше времени (поднять второй кластер, разогнать там БД), но не трогает работающий 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 как «второй уровень».

Проверка знанийKnowledge check
У вас БД 500 GB, делается ежедневный pg_basebackup + archive_command к S3. В 14:42 admin случайно сделал DROP TABLE orders. Логи показывают точное время. Как восстановиться на момент 14:41:59?
ОтветAnswer
(1) Найти base backup до этого момента — последний ежедневный (например, 02:00 того же дня). (2) Подготовить новый/чистый сервер (или новую директорию на этом же), скачать туда base backup и распаковать в $PGDATA. (3) В postgresql.conf задать: restore_command для скачивания WAL из S3 (например, 'aws s3 cp s3://wal-archive/%f %p'), recovery_target_time = '2024-XX-XX 14:41:59 UTC', recovery_target_action = 'promote'. (4) Создать файл recovery.signal в $PGDATA (для PG12+). (5) Запустить postgres. Он войдёт в recovery mode, скачает WAL-сегменты от 02:00 до 14:41:59 (это будет 500-1000 сегментов на нагруженной БД), применит их, дойдёт до 14:41:59 и сделает promote — БД откроется на запись на новом timeline (00000002...). (6) Если оригинальная primary продолжала работать, после promote приложение переключают на восстановленную. Старая primary либо изолируется, либо превращается в replica новой через pg_rewind. Главное — НЕ паниковать и НЕ трогать оригинальную primary до plana действий: иногда лучше восстановить только нужную таблицу через separate instance + pg_dump + restore конкретной таблицы, чем switchover'ить весь кластер.

Чек-лист

  • 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, который никто не восстанавливал, не существует.
Снимки, коммиты и time travel в Apache Iceberg mmap — память как файл, файл как память

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какая комбинация необходима и достаточна для Point-In-Time Recovery (PITR) до произвольного момента T1?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5