Перейти к содержимому

Бэкапы и восстановление PostgreSQL

Бэкап, который никогда не восстанавливали, — это не бэкап, а надежда. Эта фраза звучит как шутка, пока в 4 утра продакшен не отвечает, а единственная копия данных оказывается битой, созданной три месяца назад скриптом, молча падавшим с ошибкой диска. Восстановление данных — самый стрессовый сценарий в работе с БД, и единственный способ его пережить без паники — репетировать заранее, автоматизировать всё, что можно, и понимать, как именно PostgreSQL хранит и восстанавливает данные.

В краткой версии учебника ты видел обзор: pg_dump для логических копий, стратегия 3-2-1, PITR одним абзацем. Здесь мы идём глубже. Ты узнаешь, чем логический бэкап принципиально отличается от физического, как WAL превращает раз в сутки снятый снапшот в возможность восстановиться на любую секунду, почему pg_dump на большой базе — это не бэкап, а самообман, и как устроен pgBackRest — инструмент, который используют взрослые инсталляции.

Всё, что разбирается ниже, ты применишь в практике: поднимешь PostgreSQL в Docker, включишь WAL archiving, снимешь base backup, симулируешь потерю данных и восстановишься на контрольную дату через PITR. После этого «бэкапы» перестанут быть абстракцией и станут навыком.

Всё начинается не с технологий, а с двух вопросов, которые должен задать бизнес:

  • RPO (Recovery Point Objective) — сколько данных мы готовы потерять, измеряется во времени. RPO = 5 минут значит: при катастрофе мы потеряем максимум последние 5 минут записей. RPO = 24 часа значит: вчерашние заказы можно выкинуть.
  • RTO (Recovery Time Objective) — за какое время мы обязаны восстановиться. RTO = 15 минут — это совсем другая инженерия, чем RTO = 8 часов.

Эти два числа определяют всю архитектуру. RPO в минутах требует WAL archiving и реплики; RPO в сутки — достаточно ежедневного pg_dump. RTO в минутах почти всегда означает физическое восстановление (base backup + WAL) и автоматизацию, потому что логический restore на большой базе идёт часами. Паттерн «цель устойчивости к отказам» из SRE-практики разбирается подробнее в главе про инцидент-менеджмент — там же про error budget и постмортемы.

Классическое правило: минимум 3 копии данных, на 2 разных типах носителей, 1 копия — вне основной площадки (offsite). «3 копии» — это оригинал плюс два бэкапа. «2 носителя» — чтобы один дефект (например, баг в прошивке NAS) не убил всё сразу. «1 offsite» — чтобы пожар в дата-центре не стал финалом компании.

Теперь виды бэкапов PostgreSQL.

pg_dump читает данные через SQL и выдаёт текстовый или архивный файл с командами для их пересоздания. Это переносимость между версиями, между облаками, гибкий выбор объектов. Но у него есть три фатальных для продакшена свойства.

Первое — снапшот на момент старта, а не окончания. pg_dump выполняется в одной транзакции с MVCC-снапшотом: дамп внутренне согласован, таблицы между собой не расходятся, как бы долго ни шло копирование. Проблема в другом: дамп, закончившийся в 04:00, содержит состояние базы на 02:00 — всё, что произошло после старта дампа, в копию не попало, а всё, что случится до следующего дампа, при катастрофе потеряно безвозвратно. Именно поэтому один pg_dump не даёт RPO меньше своего периода. Дополнительная цена долгого дампа на большой базе — открытый снапшот мешает очистке мёртвых строк (bloat растёт), хотя согласованности это не нарушает.

Второе — скорость восстановления. pg_restore вставляет строки через INSERT или COPY, пересоздаёт индексы с нуля, проверяет внешние ключи. На базе в сотни гигабайт это часы и часы даунтайма.

Третье — размер: текстовый дамп сжатых таблиц часто больше физической копии данных на диске.

Окно терминала
# Логический бэкап — годится для миграций и малых баз
pg_dump -h localhost -U app -d shop --format=custom --compress=9 \
--file=/backups/shop-$(date +%F).dump
# Восстановление в чистую базу
createdb shop_restored
pg_restore -h localhost -U app -d shop_restored \
--jobs=4 --exit-on-error /backups/shop-2026-09-08.dump

--format=custom важен: это архив pg_restore, который умеет параллельный restore через --jobs и выборочное восстановление объектов. Простой pg_dump > file.sql — только для баз размером с песочницу.

Физический бэкап — это копия файлов каталога данных PostgreSQL (PGDATA): кучи, индексы, системные каталоги. Снять её можно только специальным механизмом — pg_basebackup или инструментом уровня pgBackRest/WAL-G, — потому что файлы в момент копирования меняются. Восстановление физического бэкапа — это остановить PostgreSQL, подменить каталог данных, стартовать: база поднимается как ни в чём не бывало, со всеми индексами, и восстановление занимает минуты вместо часов.

Цена вопроса: физический бэкап сам по себе — снимок на момент начала копирования. Чтобы он стал полезен для восстановления «вчерашнего состояния», нужен журнал изменений с этого момента — и это WAL.

Ты уже знаком с WAL из главы про транзакции и блокировки: каждая фиксация пишется в журнал. Архивирование WAL — это механизм, который складывает сегменты WAL (16 МБ каждый) в отдельное хранилище после того, как они заполнены.

Восстановление работает так: берёшь base backup (снимок каталога данных на момент T0) и проигрываешь архивные WAL поверх него — ровно тот же механизм, которым PostgreSQL переживает аварийную перезагрузку, только журнал берётся из архива, а не из своего каталога. Проигрывание можно остановить в любой момент — вот тебе PITR (Point-In-Time Recovery): восстановление на произвольную точку во времени.

base backup (T0: 02:00) ──► WAL 02:00–02:05 ──► WAL 02:05–02:10 ──► ...
▲ ▲
restore на 02:04 restore на 02:09

Конфигурация на стороне сервера:

Окно терминала
# postgresql.conf
wal_level = replica # минимум для архивирования и реплик
archive_mode = on # без этого WAL просто перезаписывается
archive_command = 'test ! -f /backups/wal/%f && cp %p /backups/wal/%f'
archive_timeout = 300 # форсировать сегмент каждые 5 минут даже без нагрузки

%p — путь к сегменту WAL внутри PGDATA, %f — его имя. Команда должна вернуть код 0, иначе PostgreSQL будет повторять архивацию бесконечно — и это правильно: лучше остановить рост каталога WAL, чем молча потерять журнал.

Дальше механика restore: копируешь base backup, создаёшь файл recovery.signal (он говорит: «при старте иди в режим восстановления») и файл postgresql.auto.conf с параметрами точки остановки:

Окно терминала
# recovery.signal — пустой файл в корне каталога данных
touch /var/lib/postgresql/data/recovery.signal
# postgresql.auto.conf — цель восстановления
cat >> /var/lib/postgresql/data/postgresql.auto.conf <<'EOF'
restore_command = 'cp /backups/wal/%f %p'
recovery_target_time = '2026-09-08 02:04:30+03'
recovery_target_action = 'pause'
EOF

После старта PostgreSQL сам найдёт, с какого WAL начинать (информация зашита в base backup), прокрутит журнал и остановится на указанной точке. recovery_target_action = 'pause' оставляет тебе шанс проверить данные до того, как сервер начнет принимать запись — дальше можно SELECT pg_wal_replay_resume() или остановиться и переснять бэкап с чистого состояния.

Ручные cp в archive_command и скрипты вокруг pg_basebackup хороши для понимания механизма, но в продакшене их заменяет специализированный инструмент. Два главных: pgBackRest и WAL-G (второй популярен в облачных средах с S3). Разберём pgBackRest — он зрелый, надёжен и стоит у большинства on-premise PostgreSQL.

pgBackRest даёт то, что ручная схема собирать ленится: инкрементные бэкапы (диффы между снимками — экономия места в разы), сжатие, параллельность, проверка контрольных сумм, шифрование, экспирация по политике, а главное — restore одной командой со всеми WAL.

/etc/pgbackrest/pgbackrest.conf
[global]
repo1-path=/backups/pgbackrest
repo1-retention-full=2 # хранить 2 полных бэкапа
compress-type=zst
process-max=4
[main]
pg1-path=/var/lib/postgresql/16/main
Окно терминала
# Полный бэкап
pgbackrest --stanza=main backup --type=full
# Инкремент (с момента последнего бэкапа любого типа)
pgbackrest --stanza=main backup --type=incr
# PITR: восстановить на точку и поднять сервер
pgbackrest --stanza=main restore \
--type=time --target="2026-09-08 02:04:30+03" \
--target-action=promote
# Статус: свежие WAL в архиве? бэкап успешен?
pgbackrest --stanza=main info
pgbackrest --stanza=main check

Стэнза (stanza) — логическое имя кластера внутри репозитория. Вся история бэкапов и WAL привязана к ней, поэтому перед первым бэкапом стэнзу создают и проверяют: pgbackrest --stanza=main stanza-create.

Единственная проверка бэкапа — его восстановление. Раз в квартал (или по регламенту) выполняй полный цикл на отдельной машине: restore → старт → psql → сверка критичных данных (число заказов, контрольная сумма по счетам). Засеки время: если восстановление заняло 3 часа, а RTO = 1 час — у тебя проблема, и лучше узнать о ней сейчас, чем в ночь инцидента.

Дешёвый вариант постоянного контроля — автоматический еженедельный restore бэкапа на тестовый инстанс с прогоном amcheck (проверка логической целостности индексов) и smoke-запросов. Плюс метрика: время с последнего успешного restore-drill должна гореть на дашборде рядом со временем с последнего бэкапа.

Бэкап — это копия всех данных компании. Правило простое: бэкап шифруется до того, как покидает доверенный периметр. pgBackRest умеет шифрование из коробки (repo-cipher-type=aes-256-cbc), для pg_dump подойдёт gpg или шифрование на стороне объектного хранилища. Ключи — в отдельном хранилище, а не рядом с бэкапом, иначе злоумышленник, добравшийся до бэкапов, доберётся и до ключа. Тема поставки ПО и защиты артефактов от подмены — в главе про supply chain security.

Offsite-копия в 2026 году — это обычно объектное хранилище (S3-совместимое) с версионированием и immutability (WORM): бэкап, который нельзя удалить или перезаписать N дней, переживёт и компрометацию учётных записей, и ransomware. pgBackRest и WAL-G умеют S3 как репозиторий напрямую.

Мониторинг успешности — отдельный пункт регламента, а не «крон пишет в лог»:

  • Каждый шаг пайплайна (dump/backup, архивация WAL, upload offsite) шлёт метрику в систему наблюдаемости и алерт при отсутствии успеха за окно.
  • Отдельно следим за лагом архивации WAL: если archive_command начала падать, каталог pg_wal/ растёт, и через некоторое время PostgreSQL остановит запись — «бэкап» убьёт прод.
  • Дашборд: возраст последнего успешного бэкапа, возраст последнего restore-drill, свободное место в репозитории (забитый диск бэкапов — классика жанра, ровно в день, когда он нужен).

В Docker бэкап живёт отдельно от volume с данными — копируй каталог данных наружу томом или через инструмент, а не «внутрь контейнера в tmpfs». Полную схему томов данных и паттерн бэкапа через Kubernetes CronJob мы разберём в разделе про Kubernetes — здесь это только перекрёстная ссылка, механика восстановления не меняется.

1. Бэкап есть, восстановление не проверялось.

Плохо: cron пишет pg_dump каждую ночь, все спокойны, три года никто не делал restore. Хорошо: ежеквартальный restore-drill на отдельной машине с засеченным временем и сверкой данных; возраст последнего drill — метрика на дашборде.

2. Есть только pg_dump на большой базе.

Плохо: база 300 ГБ, бэкап — ночной дамп. При инциденте restore займёт 10 часов при RTO = 1 час. Хорошо: физические base backup через pgBackRest + WAL archiving; pg_dump оставить для миграций и переноса схемы между средами.

3. pg_dump без --format=custom.

Плохо: pg_dump db > backup.sql — один большой SQL, restore только последовательный, без выборочного восстановления объектов. Хорошо: --format=custom --compress=9 и pg_restore --jobs=4 — параллельный restore и гранулярность до уровня таблицы.

4. Нет архивации WAL — «бэкап вчерашний, а данные сегодняшние».

Плохо: full backup раз в сутки, archive_mode=off. Утром случился инцидент — потеряны сутки транзакций. Хорошо: archive_mode=on + archive_timeout под RPO; PITR позволяет остановиться за секунду до ошибки оператора DROP TABLE.

5. Секреты и бэкапы лежат в одном месте.

Плохо: ключ шифрования лежит рядом с бэкапами на том же сервере. Хорошо: шифрование бэкапов с ключом в отдельном хранилище, offsite-копия с immutability и версионированием.

6. Молча умерший скрипт бэкапа.

Плохо: скрипт пишет в локальный лог, упал с ошибкой диска месяц назад, никто не заметил. Хорошо: каждый шаг шлёт метрику/алерт внешней системе наблюдаемости; «нет сигнала об успехе» = алерт, а не «всё ок».

7. Бэкап внутри того же volume/площадки, что и данные.

Плохо: дамп лежит на диске PostgreSQL, «для скорости». Хорошо: правило 3-2-1 — отдельный носитель и обязательная offsite-копия; RAID не заменяет бэкап.

Чем логический бэкап отличается от физического? Логический (pg_dump) читает данные через SQL и восстанавливает через INSERT/COPY: переносим между версиями, даёт согласованный MVCC-снапшот на момент старта дампа, но медленный restore и нет защиты данных между дампами. Физический (base backup) — копия файлов PGDATA, восстанавливается запуском PostgreSQL за минуты, но привязан к версии кластера и требует WAL для восстановления после момента снимка.

Как работает PITR? Берём base backup и проигрываем архивные сегменты WAL поверх него, останавливаясь на заданной точке (recovery_target_time и др.). Это тот же механизм crash-recovery, только журнал — из архива, а решаем, где остановиться.

Почему pg_dump большой базы — плохая единственная стратегия? Длится часы (и всё это время держит открытый снапшот, мешая очистке мёртвых строк), restore ещё дольше из-за пересоздания индексов и FK-проверок, а согласованный снапшот фиксирован на момент старта дампа — RPO не меньше периода дампа. Для продакшена нужен физический бэкап + WAL.

Зачем нужен archive_timeout? Сегмент WAL (16 МБ) архивируется только после заполнения. На малонагруженной базе это может занять часы, а RPO требует минут. archive_timeout форсирует отправку сегмента в архив по таймеру.

Что такое стэнза в pgBackRest? Логическое имя кластера PostgreSQL внутри репозитория бэкапов. К ней привязаны все снимки и WAL, через неё задаются политики retention и выполняются backup/restore/check.

Как ты убедишься, что бэкап рабочий? Регулярный restore-drill на изолированной машине: восстановление, старт, прогон smoke-запросов и проверок целостности, замер времени против RTO. Автоматизирую и вывожу возраст последнего успешного drill на дашборд.

RAID-10 защищает данные? Зачем тогда бэкапы? RAID защищает от отказа диска, но не от DROP TABLE, бага в приложении, ransomware, пожара и человеческой ошибки. Бэкап с PITR позволяет вернуться к состоянию до ошибки — RAID так не умеет.

Мини-проект: «Восстановись или проиграй». Подними PostgreSQL в Docker с архивацией WAL, сними base backup, устрой катастрофу и восстановись на контрольную дату.

Задание 1. Инфраструктура. Напиши docker-compose.yml с сервисом postgres:18, внешними томами: pgdata (данные), backups (base backup) и wal (архив WAL). Пробрось 5432. Критерий: контейнер стартует, psql подключается, все три каталога существуют на хосте.

Задание 2. Архивация WAL. Включи wal_level=replica, archive_mode=on, archive_command с копированием сегментов в смонтированный каталог /wal, archive_timeout=60. Критерий: после нагрузочной вставки в каталоге /wal на хосте появляются файлы 000000010000...; команда SELECT * FROM pg_stat_archiver; показывает растущий archived_count и failed_count = 0.

Задание 3. Данные и base backup. Создай таблицу orders со строками, зафиксируй контрольную дату T = now() (запиши её). Сними base backup через pg_basebackup -D /backups/base -Fp -Xs -P -U postgres. Критерий: каталог /backups/base содержит полный каталог данных, в логе — base backup completed.

Задание 4. Катастрофа. Удали строки из orders (или весь volume pgdata) — сымитируй утренний инцидент. Критерий: приложение не видит данных; pgdata пуст или повреждён.

Задание 5. PITR. Восстанови: скопируй base backup в новый pgdata, создай recovery.signal, пропиши restore_command на каталог WAL и recovery_target_time = T (контрольная дата). Стартуй контейнер. Критерий: после реплея PostgreSQL остановился на точке; SELECT count(*) FROM orders возвращает число строк на момент T — а удалённых после T строк нет.

Задание 6. Отчёт. Замерь: время создания base backup, объём бэкапа против объёма данных, время полного PITR. Сравни с воображаемыми RPO=1 мин и RTO=30 мин. Критерий: выводы письменно — какая часть бюджета съедена и что улучшить (инкременты, pgBackRest, offsite).

  1. Документация PostgreSQL: непрерывное архивирование и PITR — каноническое описание archive_command, restore_command и recovery target’ов.
  2. Документация PostgreSQL: pg_basebackup и pg_dump — все флаги, которые ты будешь использовать.
  3. pgBackRest User Guide — стэнзы, типы бэкапов, retention, шифрование и S3-репозитории.
  4. WAL-G документация — альтернатива для облачных окружений с S3.
  5. PostgreSQL Internals (Hironobu Suzuki) — глава про WAL: как устроены сегменты и реплей, даст уверенность на уровне «под капотом».
  6. Google SRE Workbook, глава Incident Response — там разобран Disaster Recovery Testing (DiRT): почему репетиции восстановления — часть инженерной культуры, а не формальность.