Что такое архивация WAL и PITR в PostgreSQL
WAL (Write-Ahead Log, журнал предзаписи) хранит каждое изменение до того, как оно попадёт в файлы данных. Журнал разбит на сегменты по 16 МБ; с PostgreSQL 11 размер задаётся при initdb ключом --wal-segsize, но менять его без причины не стоит.
Архивация WAL означает копирование завершённых сегментов за пределы кластера. Копирует сам сервер: когда сегмент закрыт, PostgreSQL вызывает команду из параметра archive_command и ждёт код возврата. Ноль означает, что файл лежит в архиве. Любой другой код заставит сервер повторить вызов, а сегмент останется в pg_wal.
PITR (Point-In-Time Recovery) собирает рабочее состояние базы из двух частей: физического base backup, то есть снимка файлов данных, и непрерывной цепочки WAL после него. Нет цепочки, нет PITR: base backup без архива WAL откатывает ровно к моменту своего создания.
Практический эффект выглядит так: ошибочный DELETE выполнен в 14:32, вы восстанавливаетесь на 14:31 и теряете секунды. RPO измеряется интервалом между последней заархивированной транзакцией и моментом сбоя, а не расписанием ночного дампа.
Чем PITR отличается от pg_dump и репликации
pg_dump снимает логический снимок на момент запуска. Восстановиться из него можно только на этот момент, всё, что произошло позже, потеряно. Дамп базы в 500 ГБ идёт часами и сильно нагружает диск.
Физическая реплика защищает от отказа железа: узел умер, второй подхватил нагрузку. Логическая ошибка уезжает на реплику за миллисекунды. DELETE FROM orders без WHERE выполнится и на primary, и на standby, и откатить его на реплике нечем.
PITR закрывает обе дыры: даёт непрерывность в секундном масштабе и позволяет встать на момент до ошибочного запроса.
| Метод | От чего защищает | RPO | Восстановление на произвольный момент |
|---|---|---|---|
| pg_dump или pg_dumpall | Логические ошибки, перенос между версиями | Часы или сутки, зависит от расписания | Нет, только момент дампа |
| Физическая реплика | Отказ сервера, диска, сети | Секунды при синхронной репликации | Нет, реплика повторяет все ошибки primary |
| Base backup и архивация WAL | Отказ сервера и логические ошибки | Секунды, ограничено archive_timeout | Да, на время, LSN, XID или именованную точку |
Что нужно подготовить до настройки
Первым делом проверьте версию. До PostgreSQL 12 восстановление настраивалось файлом recovery.conf, с 12 он заменён на recovery.signal, а параметры переехали в postgresql.conf. Инструкция для старой версии на новом сервере не сработает.
- Доступ к postgresql.conf и возможность перезапустить кластер: archive_mode меняется только рестартом.
- Хранилище под архив WAL. Считайте объём: 16 МБ на сегмент, при записи 50 ГБ в сутки архив растёт примерно на 50 ГБ в сутки плюс запас под пики.
- Пользователь postgres должен иметь права записи в директорию архива и право запускать команду архивации.
- Место под base backup: полный размер data directory плюс запас на pg_wal и на два бэкапа, если планируете ротацию.
- Отдельный инстанс или виртуальная машина для тестовых восстановлений. Разворачивать проверку поверх продакшена никто не станет.
Настройка archive_command: как заставить PostgreSQL архивировать WAL
Архивацию включают три параметра. Минимальный рабочий набор выглядит так:
wal_level = replica archive_mode = on archive_command = 'test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f' archive_timeout = 300
Плейсхолдеры %p и %f подставляет сервер. %p это полный путь к сегменту внутри pg_wal, например /var/lib/postgresql/16/main/pg_wal/0000000100000000000000A5, %f это только имя файла. Команда выполняется от пользователя postgres с его окружением, поэтому не рассчитывайте на PATH из своей сессии: прописывайте полные пути к бинарникам.
Проверка test ! -f нужна, чтобы не перезаписать сегмент, который уже лежит в архиве. PostgreSQL может вызвать archive_command повторно для того же файла, и запись поверх готовой копии иногда даёт битый файл.
Код возврата решает всё. Команда вернула ноль: сегмент считается заархивированным, в pg_wal/archive_status появляется метка .done. Вернула что-то другое: сервер оставит метку .ready, повторит вызов и не станет удалять сегмент из pg_wal. Опасность голого cp в том, что при отвалившемся и некорректно размонтированном NFS команда пишет в локальную директорию точки монтирования и возвращает ноль. Файл формально скопирован, в архиве его нет, а узнаете вы об этом при восстановлении.
| Параметр | Значение | Что делает |
|---|---|---|
| wal_level | replica или logical | Определяет объём данных в WAL; для PITR достаточно replica |
| archive_mode | on | Включает архивацию; смена значения требует рестарта кластера |
| archive_command | Команда с %p и %f | Копирует сегмент в архив; вызывается для каждого закрытого сегмента |
| archive_timeout | 60 или 300 секунд | Принудительно закрывает сегмент по таймеру, ограничивает RPO на тихих базах |
| wal_keep_size | По нагрузке реплик | Держит сегменты в pg_wal для реплик; PITR не заменяет |
| max_wal_size | По объёму записи | Управляет частотой checkpoint; косвенно влияет на объём pg_wal |
| restore_command | Команда с %f и %p | Достаёт сегмент из архива при восстановлении |
Рабочие примеры archive_command
Локальная или сетевая директория через rsync. Флаг --ignore-existing защищает уже лежащий в архиве файл, а ненулевой код возврата rsync гарантирует, что PostgreSQL повторит попытку:
archive_command = 'test ! -f /mnt/wal_archive/%f && rsync -a --ignore-existing %p /mnt/wal_archive/%f'
Копирование с журналом для разбора инцидентов. Цепочка через && сохраняет код возврата cp, поэтому ошибка не спрячется за успешным echo:
archive_command = 'cp --preserve=timestamps %p /mnt/wal_archive/%f && echo "$(date -Is) archived %f" >> /var/log/postgresql/wal_archive.log'
Отправка в объектное хранилище через WAL-G. Ошибка сети даёт ненулевой код, сегмент остаётся в pg_wal и уезжает в архив при следующей попытке:
archive_command = 'wal-g wal-push %p --config /etc/wal-g/config.yaml'
Тот же принцип у pgBackRest, но архивацией управляет его собственная подкоманда:
archive_command = 'pgbackrest --stanza=main archive-push %p'
Проверка, что сегменты действительно архивируются
Не ждите следующего checkpoint, переключите сегмент вручную и посмотрите статистику:
SELECT pg_switch_wal(); SELECT archived_count, failed_count, last_archived_wal, last_failed_wal, last_failed_time FROM pg_stat_archiver;
После переключения archived_count вырастает на единицу, а last_archived_wal показывает имя сегмента, уехавшего в архив. Сверьтесь с содержимым директории: ls -l /mnt/wal_archive | tail -5. Файл должен появиться в течение нескольких секунд. В PostgreSQL 9.6 и старше функция переключения называется pg_switch_xlog().
На базе с низкой активностью сегмент закрывается только по archive_timeout или по checkpoint. Если архив пуст, выполните CHECKPOINT и повторите pg_switch_wal().
Правки archive_mode требуют рестарта, изменение archive_command подхватывается через SELECT pg_reload_conf(). После этого обязательно проверьте лог PostgreSQL: ошибки прав и недоступного монтирования видны там сразу.
Base backup: снимаем основу для восстановления
pg_basebackup копирует файлы кластера по протоколу репликации и отдаёт каталог, с которого кластер стартует. Запускайте его только при уже работающей архивации WAL: если архивация выключена, цепочка после бэкапа не начнётся и PITR невозможен.
pg_basebackup -h 10.0.0.5 -U replicator -D /backup/base/2026-09-11 -Ft -z -Xs -P
| Ключ | Что делает | Зачем нужен |
|---|---|---|
| -D /path | Каталог для бэкапа | Целевая директория должна быть пустой |
| -Ft | Формат tar | Удобно переносить и распаковывать на другом сервере |
| -z | Сжатие gzip | Экономит место, на выходе base.tar.gz |
| -Xs | Потоковая передача WAL | В бэкап попадут сегменты, нужные для старта восстановления |
| -P | Прогресс | Видно, что процесс идёт, а не завис |
| -R | Запись restore_command | Полезно, когда бэкап сразу превращают в реплику |
Для нестандартных схем есть ручной путь: SELECT pg_backup_start('manual_label', true); далее rsync или tar файлов данных, затем SELECT pg_backup_stop(); . В PostgreSQL 15 и новее функции называются pg_backup_start() и pg_backup_stop(), в 14 и старше это pg_start_backup() и pg_stop_backup(). Второй аргумент true включает быстрый режим, при котором checkpoint не ждёт завершения всех активных транзакций.
Бэкап связан с WAL через файл backup_label в корне снимка. В нём записаны стартовый адрес WAL (START WAL LOCATION), метка времени и timeline. PostgreSQL читает этот файл при восстановлении и понимает, с какого сегмента начинать проигрывание. Удалите backup_label, и кластер либо не стартует, либо начнёт с неверной точки.
Проверка целостности base backup
pg_basebackup создаёт файл backup_manifest с контрольными суммами. Проверка выполняется одной командой:
pg_verifybackup /backup/base/2026-09-11
Утилита сверяет каждый файл с манифестом и сообщает о расхождениях, лишних или недостающих файлах. Проверка не прошла: бэкап не используйте, снимайте заново. Частые причины сбоя это оборванное соединение, переполнение диска и ошибки сетевого хранилища.
Дополнительно убедитесь, что в бэкапе есть backup_label и, при -Xs, сегменты WAL, записанные во время снятия. Их отсутствие означает разрыв цепочки на самом старте, и такой снимок годится только для запуска кластера без отката на момент времени.
Восстановление PITR: restore_command и recovery target
Порядок действий на отдельном инстансе:
- Остановите кластер: pg_ctl stop -m fast или systemctl stop postgresql@16-main.
- Переименуйте текущий data directory, не удаляйте его. Если новый инстанс не поднимется, вернётесь к исходному состоянию.
- Распакуйте base backup в чистый каталог: mkdir -p /var/lib/postgresql/16/main и tar -xzf /backup/base/2026-09-11/base.tar.gz -C /var/lib/postgresql/16/main.
- Настройте restore_command в postgresql.conf. Здесь %f это имя нужного сегмента, %p это путь, куда его положить.
- Создайте файл recovery.signal в data directory. В PostgreSQL 11 и старше вместо него нужен recovery.conf с параметрами standby_mode = 'off' и restore_command.
- Задайте цель восстановления и поведение по её достижении.
- Запустите кластер и следите за логом.
restore_command = 'cp /mnt/wal_archive/%f %p' recovery_target_time = '2026-09-11 14:31:00+03' recovery_target_action = 'pause'
Значение pause удобно для разбора инцидента: инстанс доходит до цели и замирает в режиме recovery. Вы проверяете данные обычными SELECT, затем снимаете паузу командой SELECT pg_wal_replay_resume(); или переводите кластер на запись через SELECT pg_promote(); В PostgreSQL 11 и старше перевод выполняют через pg_ctl promote.
| Параметр | Что задаёт | Когда использовать |
|---|---|---|
| recovery_target_time | Метку времени | Откат перед ошибочным запросом: recovery_target_time = '2026-09-11 14:31:00+03' |
| recovery_target_lsn | Адрес в WAL | Самая точная цель, когда LSN известен из логов или из плана миграции |
| recovery_target_xid | Номер транзакции | Когда XID проблемной транзакции виден в логах приложения или в pg_stat_activity |
| recovery_target_name | Именованную точку | Плановые операции: точку создаёт pg_create_restore_point('before_migration') |
| recovery_target_action | Поведение при достижении цели | pause для проверки данных, promote для автоматической выдачи нагрузки, shutdown для остановки |
| recovery_target_inclusive | Включать ли целевую транзакцию | on по умолчанию; off, когда нужно встать строго перед транзакцией |
Как выбрать recovery target
Ошибочный DELETE в 14:32 это классический сценарий метки времени. Ставьте recovery_target_time на 14:31 и указывайте смещение часового пояса, иначе сервер применит TimeZone из postgresql.conf и промахнётся на несколько часов.
Когда известен номер проблемной транзакции, работает recovery_target_xid. Восстановление остановится на коммите этой транзакции, независимо от времени. Номер обычно есть в логах приложения с log_min_duration_statement или в истории pg_stat_activity.
Для плановых работ создавайте именованную точку заранее: SELECT pg_create_restore_point('before_migration'); Имя попадает в WAL и переживёт любые операции, пока архив не почистили. Восстановление задаётся как recovery_target_name = 'before_migration'.
Самая предсказуемая цель это recovery_target_lsn: точный байтовый адрес, который вы записали перед рискованной операцией. Восстановление на LSN исключает путаницу с часовыми поясами и округлением времени.
Не проскочить точку помогает recovery_target_inclusive. По умолчанию параметр включён, и транзакции, зафиксированные ровно в целевую метку, будут применены. Поставьте off, если нужно встать строго перед ними.
Если WAL до цели не хватает, инстанс завершит проигрывание раньше и напишет об этом в лог. Проверьте наличие сегментов: ls /mnt/wal_archive | tail -20. Сдвиньте цель на более ранний момент и перезапустите восстановление: оно продолжится с последней применённой позиции, а не с нуля.
Проверка результата восстановления
SELECT pg_is_in_recovery(); SELECT pg_last_wal_replay_lsn(), pg_last_xact_replay_timestamp();
pg_is_in_recovery() возвращает false после promote, значит инстанс готов принимать запись. Пока идёт проигрывание, функция вернёт true, и для промежуточной проверки это нормально.
Дальше проверяйте данные: SELECT count(*) FROM orders WHERE created_at >= '2026-09-11 14:30'; Ожидаемый ноль подтверждает, что удаление откатили. Сверьте суммы и количество строк в ключевых таблицах с эталонными значениями, снятыми заранее. Без эталона проверить корректность восстановления нечем.
Отдельно посмотрите timeline. После PITR сервер создаёт новую линию, например с ID 2. Реплики, оставшиеся на старой, автоматически не переключатся, их нужно пересобирать с нового primary. Общий сценарий аварийного восстановления с репликами разобран в материале Восстановление PostgreSQL после сбоя в продакшне: полное руководство по WAL, PITR и автоматизации.
Контроль архива WAL: мониторинг, очистка, защита от разрастания
Архив без контроля превращается в источник проблем: он либо забивает диск, либо молча теряет сегменты. Обе ситуации ловятся по статистике pg_stat_archiver.
| Столбец pg_stat_archiver | Что показывает | Порог для алерта |
|---|---|---|
| archived_count | Число успешно заархивированных сегментов | Рост должен совпадать с частотой pg_switch_wal |
| failed_count | Число неудачных попыток архивации | Больше нуля: критичный алерт |
| last_archived_wal | Имя последнего сегмента в архиве | Отставание от текущего WAL больше 10 сегментов: предупреждение |
| last_failed_wal | Сегмент, на котором упала команда | Любое значение: разбирать причину |
| last_failed_time | Время последнего сбоя архивации | Свежее время при ненулевом failed_count: авария прямо сейчас |
| stats_reset | Когда статистика обнулялась | Не порог, используется при разборе инцидента |
Что мониторить и какие пороги ставить
Базовый запрос сравнивает последний архивный сегмент с текущим:
SELECT archived_count, failed_count, last_archived_wal,
pg_walfile_name(pg_current_wal_lsn()) AS current_wal
FROM pg_stat_archiver;
Разница между этими двумя именами показывает отставание. Десять сегментов и больше означают, что архив не успевает или команда падает. Учтите, что отставание растёт при большой нагрузке на запись, поэтому порог подбирайте под свою базу.
Размер pg_wal проверяйте напрямую: du -sh /var/lib/postgresql/16/main/pg_wal. Устойчивый рост выше max_wal_size в два или три раза говорит о том, что сегменты копятся вместо отправки в архив, и диск скоро кончится.
archive_timeout закрывает открытый сегмент по таймеру. Без него база, пишущая 100 МБ в сутки, держит сегмент открытым около 160 минут, и RPO вырастает до этих же минут. Значение 60 или 300 секунд ограничивает потерю данных сверху, ценой роста числа сегментов и объёма архива.
wal_keep_size оставляет сегменты в pg_wal для реплик. К PITR он отношения не имеет: если сегмент не ушёл в архив, после перезаписи в pg_wal восстановить его нечем.
Очистка архива без потери нужных сегментов
Чистить архив вручную через rm нельзя: легко снести сегмент, без которого цепочка разорвётся. Штатный инструмент это pg_archivecleanup:
pg_archivecleanup /mnt/wal_archive 0000000100000000000000A5
Аргумент это имя самого старого сегмента, который ещё нужен. Утилита удалит всё, что младше него по порядку нумерации. Имя берётся из backup_label самого старого base backup, который вы планируете сохранить.
Правило без исключений: удаляйте WAL только старше самого старого нужного base backup. Если чистить по дате файла, есть шанс снести сегмент, нужный для восстановления, потому что сегменты пишутся заранее и время изменения файла не связано с содержимым.
Ретеншен держите с запасом: минимум два base backup. Когда последний окажется битым, у вас останется предыдущий вместе с полной цепочкой WAL. Автоматизируйте очистку через cron, но перед удалением скрипт должен убедиться, что старый base backup прошёл pg_verifybackup и восстановление из него действительно возможно.
Регулярная проверка восстановления: регламент и чек-лист
Бэкап, который никогда не разворачивали, рабочим считать нельзя. Раз в квартал это минимум для продакшена, при частых изменениях схемы проверку проводят раз в месяц.
Считайте два показателя. RPO показывает, сколько данных вы теряете между последней транзакцией и последним заархивированным WAL. RTO показывает, сколько времени проходит от команды восстановления до готовности принимать нагрузку. Для базы в 500 ГБ с архивом на сетевом хранилище RTO часто измеряется часами, и это число нужно знать заранее, а не во время аварии. Общую логику проверок и оформление отчёта разбирает статья Как проверить резервную копию: тестовое восстановление файлов, баз данных и настроек.
Чек-лист тестового восстановления
- Поднимите чистый инстанс PostgreSQL той же мажорной версии, что и продакшен.
- Распакуйте свежий base backup и проверьте его через pg_verifybackup.
- Настройте restore_command и создайте recovery.signal.
- Задайте recovery_target_action = 'promote', чтобы восстановление завершилось без ручного вмешательства.
- Дождитесь выхода из recovery и проверьте лог на строки об остановке проигрывания.
- Сверьте контрольные данные: количество строк, суммы, последние записи в журнальных таблицах.
- Зафиксируйте RTO и фактический RPO, сравните с целевыми значениями.
- Запишите результат в журнал проверок: дата, версия, длительность, выявленные проблемы.
Такой прогон вскрывает то, что не видно в мониторинге: неполную цепочку WAL, неверный restore_command, забытые tablespace, расхождение контрольных сумм.
Диагностика ошибок архивации и восстановления
Большинство сбоев PITR сводится к шести причинам. Таблица связывает сообщение в логе с действием.
| Симптом | Причина | Решение |
|---|---|---|
| could not archive WAL file ... No such file or directory | Команда архивации не находит директорию: её не создали или отвалилось монтирование | Проверьте mount, создайте каталог и выдайте права: chown postgres:postgres /mnt/wal_archive |
| archive command failed with exit code 1 | archive_command вернул ненулевой код: сеть, права, переполненный диск | Запустите ту же команду руками от пользователя postgres и посмотрите вывод |
| requested WAL segment 0000000100000000000000A5 has already been removed | Цепочка разорвана: архив почистили слишком рано или restore_command смотрит в другую директорию | Восстанавливайтесь из более старого base backup, проверьте полноту архива |
| recovery ended before configured recovery target | Не хватает WAL до заданной цели | Сдвиньте recovery_target_time назад и проверьте наличие сегментов в архиве |
| permission denied для pg_wal или директории архива | Процесс запущен не от postgres или права сменились после копирования | Верните владельца: chown -R postgres:postgres, права 0700 на data directory |
| FATAL: could not open file pg_wal/0000000100000000000000A5 | Сегмента нет в архиве или restore_command подставляет не тот путь | Проверьте шаблоны %f и %p, вручную достаньте сегмент из архива |
Как читать логи PostgreSQL при восстановлении
Ключевых строк немного, и по ним восстанавливается вся картина:
- starting point-in-time recovery to 2026-09-11 14:31:00+03: цель принята, проигрывание началось.
- restored log file "0000000100000000000000A5": сегмент найден и применён; отсутствие таких строк указывает на проблему с restore_command.
- recovery stopping before commit of transaction 1042, time 2026-09-11 14:32:07+03: инстанс встал перед проблемной транзакцией, это ожидаемый результат.
- recovery stopping after commit of transaction 1042: цель включила транзакцию, иногда это не то, чего вы хотели.
- selected new timeline ID: 2: создана новая линия, старая осталась в прошлом.
- archive recovery complete: проигрывание закончено, инстанс готов к promote.
Стратегия хранения WAL: локально, NFS, S3, pgBackRest и WAL-G
Архив WAL критичен так же, как сами данные: его потеря обнуляет возможность PITR. Выбор места хранения определяет, от каких аварий вы защищены.
| Вариант | Плюсы | Минусы |
|---|---|---|
| Локальная директория на том же сервере | Быстро, просто, нет сети | Отказ сервера уносит и данные, и архив |
| Отдельный диск или NAS | Архив переживает отказ основного тома | Один физический отказ диска убивает PITR; нужен RAID и мониторинг |
| NFS или SMB-шара | Сетевое хранилище с общим доступом | Разрыв сети ломает archive_command, нужны надёжный сервер и таймауты |
| S3 или объектное хранилище | Защита от отказа площадки, версионирование | Нужен WAL-G или pgBackRest, на каждый сегмент добавляется задержка |
Рабочая схема для продакшена: архив пишется в две точки, локально для быстрого восстановления и в объектное хранилище для защиты от отказа площадки. Принципы 3-2-1 и 4-3-2 разобраны в статье Резервное копирование в стратегии отказоустойчивости: лучшие практики 2026. Для тестовых восстановлений удобно держать отдельный инстанс в облаке: его проще поднять и погасить после проверки, чем искать свободное железо в стойке. Подойдет, например, Timeweb Cloud с виртуальными серверами и объектным хранилищем.
Когда переходить на pgBackRest или WAL-G
archive_command с rsync закрывает задачу для одного инстанса с небольшим архивом. Признаки, что пора брать специализированный инструмент: несколько кластеров, нужен автоматический ретеншен, шифрование, дедупликация, параллельная архивация и единая точка управления бэкапами.
pgBackRest и WAL-G работают поверх того же механизма, подставляясь в archive_command, и дают сверх базовой настройки проверку бэкапов, инкрементальные копии, сжатие и отправку сразу в несколько репозиториев. Пошаговая настройка pgBackRest с полными и дифференциальными бэкапами и восстановлением до точки времени описана в отдельном руководстве: Резервное копирование PostgreSQL с pgBackRest: полные и дифференциальные бэкапы, WAL и PITR.
Что бы вы ни выбрали, порядок работ одинаков: включить архивацию WAL, проверить pg_stat_archiver, снять base backup, протестировать восстановление на отдельном инстансе и только потом считать систему защищённой.