
Статья подготовлена вместе с экспертом
Елизавета Белоконова, менеджер продукта

Резервное копирование PostgreSQL в проде часто держится исключительно на cron: pg_dump настроили один раз, дамп уходит по расписанию, и полгода в лог никто не заглядывает. В одной команде дамп две недели подряд падал с ошибкой нехватки места на диске, а узнали об этом только в момент аварии — последний рабочий файл оказался двухнедельной давности. Многие организации настолько уверены в своей устойчивости к шифровальщикам, что вообще не тестировали восстановление после кибератаки. А ведь может быть так, что экапы были исправны, но восстановление заняло четыре часа вместо расчётных тридцати минут, потому что база выросла втрое, а саму процедуру ни разу не прогоняли на тестовом стенде.
Для небольшой базы и разовой миграции достаточно pg_dump: инструмент прост в использовании, кроссверсионен и восстанавливает отдельную таблицу без поднятия всего кластера. Но у него нет Point-in-Time Recovery — он откатывает базу только к моменту снятия дампа, а всё, что случилось между дампами, теряется. Для продакшена со строгим RPO (Recovery Point Objective — допустимый объём потери данных при аварии) нужен другой инструмент: WAL-G делает физический бэкап кластера и добавляет к нему непрерывный поток WAL-архивов.
Дальше собираем рабочую связку: WAL-G складывает бэкапы и WAL в VK Object Storage, lifecycle-правила S3 сами вырезают лишнее, а мониторинг и тестовое восстановление превращают формальное «бэкап есть» в проверенное «бэкап восстанавливается». Разберём по шагам: от ограничений pg_dump до восстановления на точку времени с WAL-G.

Елизавета Белоконова, менеджер продукта
Утилита pg_dump снимает логический дамп одной базы: SQL-скрипт или custom-формат через флаг -Fc, который умеет параллельное восстановление и выборочный откат объектов. pg_dumpall дополнительно выгружает роли и глобальные объекты кластера, которых в дампе отдельной базы нет — без них восстановленная на чистом сервере база останется без пользователей и прав. Восстановление идёт через pg_restore (для custom-формата) или psql (для SQL-дампа).
Главное практическое преимущество pg_dump — кроссверсионность. Дамп, снятый с PostgreSQL 14, восстанавливается на PostgreSQL 16 без промежуточных миграций. Обратное с WAL-G невозможно: формат бэкапа там привязан к мажорной версии кластера. Второе преимущество — pg_dump восстанавливает отдельную таблицу или схему, а не весь кластер целиком, что удобно для миграций и точечного отката ошибочного DELETE.
Главное ограничение — отсутствие Point-in-Time Recovery: pg_dump восстанавливает базу ровно на момент снятия дампа, а транзакции, прошедшие между дампами, теряются безвозвратно. Если дамп снят в 2:00, а авария случилась в 14:00, двенадцать часов данных не существуют ни в каком виде.
Второе ограничение — линейный рост времени восстановления с объёмом базы: чем больше данных, тем дольше идёт логическое воссоздание таблиц и индексов, точное время зависит от конфигурации диска и CPU конкретного сервера. Третье: pg_dump не умеет инкрементальные бэкапы, каждый дамп — это полная копия базы независимо от того, сколько строк изменилось со вчера. И четвёртое: при сбое дампа, например при нехватке места на диске или обрыве соединения, файл повреждается частично, и такой дамп часто невозможно восстановить вообще — отсюда риск того самого «дамп падал две недели, а бэкапа не было».
Резервное копирование PostgreSQL средствами одного pg_dump закрывает точечные задачи, но не годится как единственная стратегия для боевого кластера.
Команды pg_dump, которые нужны в проде bash
# custom-формат со сжатием (уровень 6) — снятие дампа одной базы pg_dump -Fc -Z 6 -d production -f /backups/production.dump # параллельное восстановление в 4 потока (работает только с custom/directory форматом) pg_restore -j 4 -d production_restored /backups/production.dump # роли и глобальные объекты кластера — отдельно от дампа конкретной базы pg_dumpall --globals-only -f /backups/globals.sql # восстановление одной таблицы без поднятия всего кластера pg_restore -t orders -d production_restored /backups/production.dump # потоковая заливка дампа в Object Storage без промежуточного файла на диске pg_dump -Fc -Z 6 -d production | aws s3 cp - s3://pg-backups/production.dump \ --endpoint-url https://hb.ru-msk.vkcloud-storage.ru
Промежуточный файл на диске — источник тех самых обрывов дампа: если место заканчивается в процессе снятия, pg_dump останавливается на середине, а файл остаётся частично записанным и часто непригоден для восстановления. Потоковая заливка через aws s3 cp - снимает эту зависимость целиком: дамп не касается локального диска, узкое место переносится на пропускную способность сети до хранилища.
Флаг -j работает только с custom (-Fc) или directory (-Fd) форматом: pg_restore разбирает архив на независимые объекты и грузит их параллельно. Обычный SQL-дамп (-Fp) — последовательный поток команд для psql, никакого параллелизма в нём структурно нет, поэтому -j с текстовым форматом просто игнорируется.
Резервное копирование PostgreSQL для продакшена со строгим RPO строится вокруг WAL-G — инструмента, который добавляет к физическому бэкапу непрерывный поток WAL и Point-in-Time Recovery.
WAL-G снимает физическую копию кластера, как pg_basebackup, и параллельно непрерывно архивирует WAL-сегменты в объектное хранилище через archive_command. Восстановление строится из двух частей: базовый бэкап поднимает кластер на состояние на момент старта копирования, а накат WAL-сегментов доигрывает транзакции до нужной секунды. Это и есть Point-in-Time Recovery — восстановление с точностью до конкретной транзакции, а не до последнего ночного дампа.
Актуальный релиз — WAL-G 3.0.8. Между веткой 2.x и текущей 3.x переменные окружения для S3 не менялись, так что переход на новую версию не требует переписывать конфигурацию хранилища.
Резервное копирование PostgreSQL через WAL-G начинается с подключения к VK Object Storage: шести переменных окружения достаточно для рабочей связки. Основная переменная для адреса хранилища — WALG_S3_PREFIX. Старая форма WALE_S3_PREFIX тоже поддерживается, но это легаси-имя из более ранних версий инструмента, и в новых конфигурациях лучше сразу писать WALG_S3_PREFIX.
bash
export WALG_S3_PREFIX="s3://pg-backups/prod-cluster" export AWS_ACCESS_KEY_ID="ваш_ключ_доступа" export AWS_SECRET_ACCESS_KEY="ваш_секретный_ключ" export AWS_ENDPOINT="https://hb.ru-msk.vkcloud-storage.ru" export AWS_S3_FORCE_PATH_STYLE=true export AWS_REGION="ru-msk" export WALG_COMPRESSION_METHOD="lz4"
Эндпоинт https://hb.ru-msk.vkcloud-storage.ru соответствует региону ru-msk . AWS_S3_FORCE_PATH_STYLE=true — обязательное условие для любого S3-совместимого хранилища, отличного от AWS. Без неё WAL-G обращается к бакету по virtual-hosted-style адресу вида bucket.hb.ru-msk.vkcloud-storage.ru, а не по path-style hb.ru-msk.vkcloud-storage.ru/bucket. Для VK Object Storage такой адрес не резолвится, и WAL-G возвращает ошибку, будто бакета не существует — это самая частая ошибка первой настройки, и лечится она одной этой переменной.
WALG_COMPRESSION_METHOD по умолчанию — lz4: самый быстрый алгоритм из поддерживаемых, но с наименьшим коэффициентом сжатия. Альтернативы: lzma (медленнее, сжимает заметно лучше) и zstd.
Архивирование WAL включается в postgresql.conf:
text
archive_mode = on wal_level = replica archive_command = 'wal-g wal-push %p'
Параметр archive_mode требует перезапуска сервера, а не только reload. Проверить, что архивирование идёт, можно запросом:
sql
SELECT pg_walfile_name(pg_current_wal_lsn());
Имя текущего WAL-файла из результата должно появляться среди архивных объектов бакета. Если wal-g wal-push отрабатывает без ошибок, но новых файлов в хранилище нет, проблема в правах доступа или в самом archive_command.
Первый базовый бэкап запускается вручную:
bash
wal-g backup-push $PGDATA
Дальше — по расписанию, обычно раз в сутки в период низкой нагрузки:
bash
# crontab: ежедневный полный бэкап в 02:00 0 2 * * * wal-g backup-push $PGDATA >> /var/log/wal-g-backup.log 2>&1
WAL-сегменты между полными бэкапами архивируются непрерывно через archive_command, отдельного расписания для них не нужно: PostgreSQL сам вызывает wal-g wal-push при каждом переключении сегмента.
Процедура PITR — четыре шага. Сначала разворачивается ближайший базовый бэкап:
bash
wal-g backup-fetch $PGDATA LATEST
Затем создаётся файл-триггер восстановления вместо упразднённого с PostgreSQL 12 recovery.conf:
bash
touch $PGDATA/recovery.signal
Параметры восстановления задаются в postgresql.conf (или postgresql.auto.conf):
text
restore_command = 'wal-g wal-fetch %f %p' recovery_target_time = '2026-07-22 14:36:59'
restore_command копирует нужные WAL-сегменты из хранилища по мере накатки, а recovery_target_time останавливает восстановление, как только PostgreSQL встречает транзакцию с более поздней временной меткой, чем указано. После этого PostgreSQL запускается обычной командой старта сервиса: наличие recovery.signal переводит его в режим восстановления автоматически.
Убедиться, что PITR отработал, можно по двум признакам. В логе PostgreSQL должна появиться запись вида «recovery stopping before commit» с указанием целевого времени — она подтверждает, что накат остановился именно там, где задано, а не доехал до конца доступных WAL. Второй признак — запрос SELECT pg_is_in_recovery(); должен вернуть false после завершения восстановления: пока идёт накат WAL, он возвращает true, и только переход в false означает, что кластер поднялся в рабочем режиме на нужный момент времени.
WAL-архивы растут непрерывно, пока работает archive_command: каждый переключённый сегмент WAL становится новым объектом в бакете и без правил ротации остаётся там навсегда. За месяц активной OLTP-нагрузки (интенсивная запись транзакций) объём WAL-сегментов легко превышает размер базы в несколько раз. Ежедневные базовые бэкапы добавляют то же самое: без ограничения retention в бакете копится десяток полных копий, а для восстановления нужны последние две-три.
Ручное удаление не масштабируется: администратор не успевает вычищать бакет вручную каждую неделю, а ошибка (удалил не тот префикс) обходится дороже сэкономленного места. Lifecycle Policy в Object Storage переносит эту рутину на сторону хранилища: правило настроено один раз и работает без участия человека.
В панели управления VK Cloud правило задаётся через раздел Object Storage → бакет → Lifecycle → «Добавить правило»: префикс объектов, действие (удаление или перевод в другой класс хранения) и срок в днях. Тот же результат даёт AWS CLI: конфиг в JSON-файле.
json
{ "Rules": [ { "ID": "basebackups-expiration", "Filter": { "Prefix": "basebackups/" }, "Status": "Enabled", "Expiration": { "Days": 30 } }, { "ID": "wal-expiration", "Filter": { "Prefix": "wal/" }, "Status": "Enabled", "Expiration": { "Days": 7 } } ] }
bash
aws s3api put-bucket-lifecycle-configuration \ --bucket pg-backups \ --lifecycle-configuration file://lifecycle.json \ --endpoint-url https://hb.ru-msk.vkcloud-storage.ru
Полный бэкап хранится 30 дней, WAL — 7. Отправная точка расчёта: срок хранения WAL привязывается к самому старому нужному базовому бэкапу.
Отдельный вопрос — класс хранения. Bucket Lifecycle умеет и удалять объекты, и переводить их между классами: правило Transition вместо Expiration переносит старые базовые копии из Hotbox в Icebox. Держать весь архив в горячем классе не нужно: восстановление обращается к последним копиям, а старые версии лежат мёртвым грузом. Перевод холодных копий в Icebox выгоднее постоянного хранения в Hotbox при той же надёжности — разница в стоимости, не в доступности данных.
WAL-G и S3-lifecycle решают одну задачу с двух концов. wal-g delete удаляет бэкапы логически: разбирает манифесты и понимает, какие базовые бэкапы и WAL-сегменты ещё нужны для восстановления, а какие нет. Lifecycle в бакете подчищает физически: убирает объекты, которые WAL-G уже пометил ненужными, но которые остались лежать в хранилище.
bash
wal-g delete retain FULL 7 --confirm
Команда оставляет 4 последних полных бэкапа и удаляет всё, что для них не нужно. Флаг --confirm обязателен: без него wal-g delete по умолчанию работает в режиме dry-run, печатает список объектов на удаление, но ничего не стирает. Частая ошибка: команду кладут в cron без --confirm и получают бесконечно растущий бакет — задача выполняется, реального удаления не происходит.
Обратная ошибка опаснее: удалять WAL-сегменты вручную в обход wal-g delete, полагаясь на собственную оценку возраста. Если стереть WAL, нужный ещё не устаревшему по мнению WAL-G базовому бэкапу, PITR сломается: восстановление упрётся в пробел в цепочке WAL. Ротацию доверяют одной команде (wal-g delete) плюс lifecycle для уже разрешённых к удалению объектов, а не ручному rm или S3-правилам с более коротким сроком, чем retention WAL-G.
Резервный репозиторий — приоритетная цель шифровальщика: скомпрометировав административный доступ, атака переходит от продовой базы к бэкапам, чтобы лишить жертву пути отката без выкупа. Готовность к такому сценарию в российских компаниях низкая. По опросу Linx Cloud реальный план аварийного восстановления есть только у 20% организаций . По отдельному исследованию «Кросс технолоджис» около 40% компаний ни разу не проверяли уже внедрённые планы восстановления . Большинство узнаёт, что бэкап стал целью атаки, уже в момент самой аварии.
Первый рубеж защиты — версионирование бакета Object Storage. Каждая перезапись объекта создаёт новую версию, а удаление не стирает данные физически, а ставит delete-marker: предыдущее состояние остаётся доступным. Без ротации версий бакет растёт бесконечно, поэтому версии подчиняются тем же lifecycle-правилам, что и сами бэкапы: NoncurrentVersionExpiration удаляет старые версии по возрасту, NewerNoncurrentVersions ограничивает их количество на объект.
Второй рубеж — Object Lock в режиме WORM (write once, read many): объект нельзя удалить или перезаписать до истечения заданного срока, даже обладая правами администратора бакета. Доступны режимы Compliance (запрет без исключений) и Governance (снимается специальным правом), плюс Legal Hold — блокировка без срока, снимаемая вручную. Обязательная оговорка: Object Lock требует заранее включённого версионирования бакета, дооснастить защиту постфактум, если версионирование не было включено с самого начала, нельзя.
Третий рубеж — организационный: отдельная учётная запись S3 для задач бэкапирования с минимальным набором прав. Ключи, которые лежат на сервере с базой данных, там же, где потенциальный шифровальщик получит первую точку опоры, не должны иметь прав на удаление бакета целиком, только на запись новых объектов. Права на удаление и на изменение lifecycle-правил остаются у отдельной учётки.
Режимы Object Lock (Compliance, Governance, Legal Hold) и требование заранее включённого версионирования подтверждены продуктовой документацией VK Object Storage.
Голый cron решает задачу «запустить команду по расписанию» и на этом останавливается. Контроля параллельных запусков нет: если вчерашний backup-push завис на сетевой заминке и ещё не завершился, cron всё равно поднимет новый процесс в 02:00, и на кластер придутся два конкурирующих снятия сразу. Логирование ограничено перенаправлением в файл, без ротации и структуры: причину сбоя ищут вручную по растущему логу. Автоматического ретрая нет: упавшая задача просто не выполнится до следующего цикла расписания. И статус выполнения не виден в общей системе мониторинга хоста: cron не публикует состояние юнита, только пишет или не пишет в лог.
text
# /etc/systemd/system/wal-g-backup.service [Unit] Description=WAL-G full backup of PostgreSQL cluster After=postgresql.service [Service] Type=oneshot User=postgres EnvironmentFile=/etc/wal-g/env ExecStart=/usr/local/bin/wal-g backup-push /var/lib/postgresql/16/main
text
# /etc/systemd/system/wal-g-backup.timer [Unit] Description=Ежедневный запуск wal-g-backup.service [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true RandomizedDelaySec=600 [Install] WantedBy=timers.target
bash
systemctl enable --now wal-g-backup.timer systemctl list-timers wal-g-backup.timer journalctl -u wal-g-backup.service --since today
Persistent=true — то, чего cron не умеет вовсе: если сервер был выключен или перезагружался в 02:00, таймер запустит пропущенный прогон сразу после старта systemd, а не будет молча ждать следующего календарного окна.
Учётные данные для доступа к Object Storage хранятся в отдельном файле (/etc/wal-g/env) с правами 600 и владельцем postgres, не в самом crontab и не в общем /etc/profile, куда заглядывает любой пользователь системы.
journalctl -u wal-g-backup.service заменяет растущий лог-файл единым структурированным журналом с уровнями и таймстампами. systemctl list-timers отвечает на вопрос, ради которого обычно лезут в старые логи: когда был последний запуск и когда будет следующий, без парсинга дат из текста. Юниты systemd умеют зависимости (After=postgresql.service): бэкап не стартует раньше, чем поднялась сама база, чего голый crontab не гарантирует никак.
Резервное копирование PostgreSQL не заканчивается на успешном wal-push: автоматизации нужен отдельный слой проверки и мониторинга.
Проверка валидности: wal-g backup-list
bash
wal-g backup-list --detail
Команда выводит все хранимые бэкапы с временем создания, размером и статусом WAL-сегментов. Место команды — в cron сразу после ночного backup-push, с результатом в системе алертинга: свежая запись в списке подтверждает, что бэкап запустился и завершился без ошибок.
Проверка валидности бэкапа отвечает только на вопрос «файл записан», не на вопрос «база восстановится». Ответ даёт только реальное восстановление на тестовой ВМ: backup-fetch, накат WAL, запуск PostgreSQL, сверка схемы через pg_dump --schema-only и diff между дампами.
bash
pg_dump --schema-only -d production > schema_prod.sql pg_dump --schema-only -d restored_test > schema_restored.sql diff schema_prod.sql schema_restored.sql
Drill фиксирует фактический RTO (Recovery Time Objective — время на восстановление): не расчётное «должно занять полчаса», а реальное время от команды backup-fetch до рабочей базы на тестовой ВМ. Именно этот замер стабильно расходится с ожиданиями. По данным CNews, 76,8% российских компаний закладывают на восстановление всей ИТ-среды из бэкапов больше четырёх часов, и лишь 17% укладываются в этот срок . Проверку восстановления из бэкапов за последние 12 месяцев проводили 50,8% организаций, а сценарий восстановления именно после кибератаки тестировали только 34,6% . Ещё нагляднее выглядит планирование: по данным Linx Cloud реальный план аварийного восстановления есть только у 20% российских компаний , а по отдельному опросу «Кросс технолоджис» около 40% компаний не проверяли уже внедрённые планы . Первое реальное восстановление у многих происходит уже во время аварии, не на плановом drill.
| Метрика | Порог | Уровень |
| Возраст последнего успешного бэкапа | больше 25 часов | Critical |
| Задержка WAL-архивации (age(pg_current_wal_lsn())) | больше 5 минут | Warning |
| Размер бэкапа относительно предыдущего | меньше 50% | Warning |
Первая метрика ловит остановку бэкапирования: backup-push упал, а cron не сообщил. Вторая — разрыв в потоке WAL: archive_command не успевает или сеть до Object Storage деградировала, PITR теряет точность. Третья — косвенный признак повреждения дампа: резкое падение объёма редко бывает нормой для production-базы.
Типичные ошибки первого месяца повторяются от команды к команде: wal-g delete в cron без флага --confirm (задача выполняется, реального удаления не происходит), отсутствие алерта на возраст бэкапа (остановка backup-push обнаруживается только в момент аварии), пропущенный тест восстановления (валидность дампа проверена, а факт восстановления — нет) и ключи доступа с правами на удаление всего бакета, лежащие на том же сервере, что и база данных.
pg_dump и WAL-G — два разных архитектурных подхода к тому, как устроено резервное копирование PostgreSQL под конкретную нагрузку, а не вопрос вкуса.
| Параметр | pg_dump | WAL-G |
| Тип бэкапа | Логический (SQL/custom) | Физический (бинарный) |
| PITR | Нет | Да, до транзакции |
| Скорость восстановления | Медленно, растёт с объёмом | Быстро, параллельное чтение |
| Инкрементальность | Нет, всегда полный дамп | Да, WAL — инкременты |
| Объём хранения | 100% БД на каждый дамп | Базовый бэкап + WAL, обычно меньше |
| Подходит для | до 10 ГБ, разовая миграция | Production, от 10 ГБ, строгий RPO |
| Кроссверсионность | Да, PG 14 → PG 16 | Нет, только та же мажорная версия |
| Восстановление одной таблицы | Да | Нет, только весь кластер |
Граница по объёму условна: пока дамп восстанавливается за минуты, накладные расходы pg_dump терпимы. Когда восстановление растягивается на часы, а RPO измеряется минутами, логический дамп уступает место WAL-G с непрерывной архивацией.
Кроссверсионность делает pg_dump незаменимым ровно в одном сценарии — мажорном апгрейде. WAL-G восстанавливает кластер строго в той же мажорной версии, в которой снят бэкап: перейти с PostgreSQL 14 на 16 физическим восстановлением нельзя. Логический дамп, наоборот, читает и пишет через SQL и переживает смену версии.
Поэтому в production-конфигурации оба инструмента не конкурируют, а закрывают разные задачи: WAL-G — основной механизм ежедневного бэкапа и PITR, pg_dump — резервный путь для миграций, разовых выгрузок и восстановления отдельной таблицы без поднятия всего кластера.
Резервное копирование PostgreSQL — это цепочка из трёх звеньев: инструмент бэкапирования, автоматическая ротация в Object Storage и регулярная проверка восстановлением. Если выпадает любое звено, пропадает и смысл всей конструкции: невалидный дамп, бесконечно растущий бакет и непроверенное восстановление одинаково заканчиваются тем, что данные не восстанавливаются, когда это нужно. WAL-G с lifecycle rules закрывают первые два звена автоматически, ежемесячный drill с замером RTO закрывает третье, и только все вместе они складываются в рабочее резервное копирование PostgreSQL.

S3-хранилище с Object Lock и версионированием для бэкапов
WAL-G делает физические бэкапы с поддержкой Point-in-Time Recovery: базу можно восстановить на конкретный момент времени, а не только на момент последнего дампа. pg_dump создаёт логический дамп без PITR и без инкрементальных бэкапов. Для production с объёмом от нескольких гигабайт и жёстким RPO WAL-G — основной инструмент, pg_dump остаётся резервным для миграций и точечного восстановления.
Включить archive_mode = on и archive_command = 'wal-g wal-push %p' в postgresql.conf, снять первый wal-g backup-push. При восстановлении: wal-g backup-fetch нужного базового бэкапа, файл-триггер recovery.signal в PGDATA, параметр recovery_target_time в postgresql.conf, запуск PostgreSQL.
Два независимых механизма: wal-g delete retain FULL N --confirm со стороны WAL-G и lifecycle rules со стороны Object Storage. Первый решает, какие бэкапы логически больше не нужны, второй физически освобождает место в бакете. Рабочая связка — оба механизма вместе, а не один вместо другого.
Точная доля зависит от компрессора (WALG_COMPRESSION_METHOD: lz4 по умолчанию, доступны lzma и zstd) и характера данных: lz4 быстрее, но сжимает слабее, lzma сжимает плотнее, но медленнее. Универсальную цифру «X% от размера БД» дать нельзя: объём базового бэкапа измеряют на своей базе после первого прогона. WAL-архивы зависят от write activity: чем интенсивнее нагрузка на запись, тем быстрее растёт объём между полными бэкапами.
Наши специалисты свяжутся с вами в ближайшее время и ответят на все вопросы.

Будем держать в курсе новостей и облачных трендов




