Бэкапы и восстановление PostgreSQL
Бэкап, который никогда не восстанавливали, — это не бэкап, а надежда. Эта фраза звучит как шутка, пока в 4 утра продакшен не отвечает, а единственная копия данных оказывается битой, созданной три месяца назад скриптом, молча падавшим с ошибкой диска. Восстановление данных — самый стрессовый сценарий в работе с БД, и единственный способ его пережить без паники — репетировать заранее, автоматизировать всё, что можно, и понимать, как именно PostgreSQL хранит и восстанавливает данные.
В краткой версии учебника ты видел обзор: pg_dump для логических копий, стратегия 3-2-1, PITR одним абзацем. Здесь мы идём глубже. Ты узнаешь, чем логический бэкап принципиально отличается от физического, как WAL превращает раз в сутки снятый снапшот в возможность восстановиться на любую секунду, почему pg_dump на большой базе — это не бэкап, а самообман, и как устроен pgBackRest — инструмент, который используют взрослые инсталляции.
Всё, что разбирается ниже, ты применишь в практике: поднимешь PostgreSQL в Docker, включишь WAL archiving, снимешь base backup, симулируешь потерю данных и восстановишься на контрольную дату через PITR. После этого «бэкапы» перестанут быть абстракцией и станут навыком.
RPO и RTO: от бизнеса к инженерии
Заголовок раздела «RPO и RTO: от бизнеса к инженерии»Всё начинается не с технологий, а с двух вопросов, которые должен задать бизнес:
- 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 и виды бэкапов
Заголовок раздела «Стратегия 3-2-1 и виды бэкапов»Классическое правило: минимум 3 копии данных, на 2 разных типах носителей, 1 копия — вне основной площадки (offsite). «3 копии» — это оригинал плюс два бэкапа. «2 носителя» — чтобы один дефект (например, баг в прошивке NAS) не убил всё сразу. «1 offsite» — чтобы пожар в дата-центре не стал финалом компании.
Теперь виды бэкапов PostgreSQL.
Логические бэкапы: pg_dump и pg_restore
Заголовок раздела «Логические бэкапы: pg_dump и pg_restore»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_restoredpg_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 — только для баз размером с песочницу.
Физические бэкапы: base backup
Заголовок раздела «Физические бэкапы: base backup»Физический бэкап — это копия файлов каталога данных PostgreSQL (PGDATA): кучи, индексы, системные каталоги. Снять её можно только специальным механизмом — pg_basebackup или инструментом уровня pgBackRest/WAL-G, — потому что файлы в момент копирования меняются. Восстановление физического бэкапа — это остановить PostgreSQL, подменить каталог данных, стартовать: база поднимается как ни в чём не бывало, со всеми индексами, и восстановление занимает минуты вместо часов.
Цена вопроса: физический бэкап сам по себе — снимок на момент начала копирования. Чтобы он стал полезен для восстановления «вчерашнего состояния», нужен журнал изменений с этого момента — и это WAL.
WAL archiving и PITR под капотом
Заголовок раздела «WAL archiving и PITR под капотом»Ты уже знаком с 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.confwal_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() или остановиться и переснять бэкап с чистого состояния.
pgBackRest: инструмент взрослых инженеров
Заголовок раздела «pgBackRest: инструмент взрослых инженеров»Ручные cp в archive_command и скрипты вокруг pg_basebackup хороши для понимания механизма, но в продакшене их заменяет специализированный инструмент. Два главных: pgBackRest и WAL-G (второй популярен в облачных средах с S3). Разберём pgBackRest — он зрелый, надёжен и стоит у большинства on-premise PostgreSQL.
pgBackRest даёт то, что ручная схема собирать ленится: инкрементные бэкапы (диффы между снимками — экономия места в разы), сжатие, параллельность, проверка контрольных сумм, шифрование, экспирация по политике, а главное — restore одной командой со всеми WAL.
[global]repo1-path=/backups/pgbackrestrepo1-retention-full=2 # хранить 2 полных бэкапаcompress-type=zstprocess-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 infopgbackrest --stanza=main checkСтэнза (stanza) — логическое имя кластера внутри репозитория. Вся история бэкапов и WAL привязана к ней, поэтому перед первым бэкапом стэнзу создают и проверяют: pgbackrest --stanza=main stanza-create.
Проверка бэкапов: restore drill
Заголовок раздела «Проверка бэкапов: restore drill»Единственная проверка бэкапа — его восстановление. Раз в квартал (или по регламенту) выполняй полный цикл на отдельной машине: restore → старт → psql → сверка критичных данных (число заказов, контрольная сумма по счетам). Засеки время: если восстановление заняло 3 часа, а RTO = 1 час — у тебя проблема, и лучше узнать о ней сейчас, чем в ночь инцидента.
Дешёвый вариант постоянного контроля — автоматический еженедельный restore бэкапа на тестовый инстанс с прогоном amcheck (проверка логической целостности индексов) и smoke-запросов. Плюс метрика: время с последнего успешного restore-drill должна гореть на дашборде рядом со временем с последнего бэкапа.
Шифрование, offsite и мониторинг
Заголовок раздела «Шифрование, offsite и мониторинг»Бэкап — это копия всех данных компании. Правило простое: бэкап шифруется до того, как покидает доверенный периметр. 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).
Что почитать
Заголовок раздела «Что почитать»- Документация PostgreSQL: непрерывное архивирование и PITR — каноническое описание
archive_command,restore_commandи recovery target’ов. - Документация PostgreSQL: pg_basebackup и pg_dump — все флаги, которые ты будешь использовать.
- pgBackRest User Guide — стэнзы, типы бэкапов, retention, шифрование и S3-репозитории.
- WAL-G документация — альтернатива для облачных окружений с S3.
- PostgreSQL Internals (Hironobu Suzuki) — глава про WAL: как устроены сегменты и реплей, даст уверенность на уровне «под капотом».
- Google SRE Workbook, глава Incident Response — там разобран Disaster Recovery Testing (DiRT): почему репетиции восстановления — часть инженерной культуры, а не формальность.