Большинство команд делают бэкапы. Но мало кто их проверяет. Именно поэтому в новостях периодически появляются истории: «компания потеряла данные, хотя бэкапы были». Были-то были — просто не восстанавливались.

Эта статья о том, как выстроить резервное копирование так, чтобы в час X вы не обнаружили пустой архив или несовместимую версию дампа.

Почему бэкап без теста — это не бэкап

Есть классическая DevOps-шутка: «бэкап существует только тогда, когда ты его восстановил». За этим стоит реальная проблема.

Типичные причины, по которым бэкап не восстанавливается:

  • Архив создавался, но диск был заполнен — последние N файлов пустые или обрезанные
  • Версия PostgreSQL на сервере восстановления отличается от версии на источнике
  • Дамп был сделан с флагом --no-owner, и при восстановлении падает на правах доступа
  • S3-бакет с бэкапами удалили «случайно» — или доступ отозвали
  • Скрипт бэкапа упал с ошибкой, но никто не настроил алертинг

Всё это реальные сценарии. И все они обнаруживаются в момент, когда времени уже нет.

Стратегия 3-2-1

Перед тем как писать скрипты, определитесь со стратегией. Классика — правило 3-2-1:

  • 3 копии данных
  • 2 разных носителя или хранилища
  • 1 копия за пределами основной инфраструктуры (offsite)

Для небольшого проекта это может выглядеть так: дамп на том же сервере + синхронизация в S3 + репликация S3-бакета в другой регион. Уже три копии, два хранилища, одна из них географически удалена.

Для баз данных важно также понять два параметра:

  • RPO (Recovery Point Objective) — сколько данных допустимо потерять. Если RPO = 1 час, бэкапы нужны каждый час.
  • RTO (Recovery Time Objective) — за сколько нужно восстановиться. Если RTO = 2 часа, процедура восстановления должна укладываться в это время.

Без этих цифр непонятно, как часто делать бэкапы и что вообще считать приемлемым.

Автоматизация для PostgreSQL

PostgreSQL — наиболее распространённая база в современных проектах, поэтому начнём с неё.

Простой дамп через pg_dump

Самый базовый вариант — pg_dump по крону:

#!/bin/bash
DATE=$(date +%Y-%m-%d_%H-%M-%S)
DB_NAME="myapp"
BACKUP_DIR="/var/backups/postgres"

mkdir -p "$BACKUP_DIR"

pg_dump -U postgres -Fc "$DB_NAME" > "$BACKUP_DIR/${DB_NAME}_${DATE}.dump"

# Удаляем бэкапы старше 7 дней
find "$BACKUP_DIR" -name "*.dump" -mtime +7 -delete

Флаг -Fc создаёт бинарный формат — он компактнее SQL-дампа и позволяет восстанавливать отдельные таблицы через pg_restore.

Добавляем в crontab:

0 3 * * * /usr/local/bin/backup_postgres.sh >> /var/log/postgres_backup.log 2>&1

Это работает, но есть проблема: если скрипт упал, вы узнаете об этом только когда откроете лог вручную. Или не узнаете вообще.

Уведомления об ошибках

Минимальный вариант — отправка в Telegram при сбое:

#!/bin/bash
TELEGRAM_BOT_TOKEN="ваш_токен"
TELEGRAM_CHAT_ID="ваш_chat_id"

send_alert() {
  curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
    -d chat_id="$TELEGRAM_CHAT_ID" \
    -d text="$1"
}

pg_dump -U postgres -Fc myapp > /var/backups/myapp_$(date +%Y%m%d).dump

if [ $? -ne 0 ]; then
  send_alert "ALERT: Бэкап базы myapp упал! Проверь сервер."
else
  send_alert "OK: Бэкап myapp успешно создан — $(date)"
fi

Можно усложнить: проверять размер файла (пустой дамп — красный флаг), сравнивать с предыдущим (если размер упал на 50% — тоже алерт).

WAL-архивирование и Point-in-Time Recovery

Для продакшн-баз pg_dump раз в сутки может быть недостаточно. Если RPO = 15 минут, нужен другой подход — WAL-архивирование.

PostgreSQL пишет все изменения в Write-Ahead Log (WAL). Если архивировать эти файлы непрерывно, можно восстановить базу на любой момент времени.

Настройка в postgresql.conf:

archive_mode = on
archive_command = 'aws s3 cp %p s3://my-bucket/wal/%f'
wal_level = replica

Для управления этим удобнее использовать готовые инструменты: pgBackRest или Barman. Они берут на себя ротацию, сжатие, параллельную загрузку и верификацию.

Пример конфига pgBackRest:

[global]
repo1-path=/var/lib/pgbackrest
repo1-s3-bucket=my-backup-bucket
repo1-s3-region=eu-central-1
repo1-type=s3
retention-full=4

[myapp]
pg1-path=/var/lib/postgresql/15/main

Полный бэкап раз в неделю + WAL непрерывно = можно восстановиться на любую минуту за последние 4 недели.

Автоматизация для MySQL / MariaDB

Для MySQL базовый инструмент — mysqldump. Схема та же, но есть нюансы.

mysqldump \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  -u root -p myapp | gzip > /var/backups/myapp_$(date +%Y%m%d).sql.gz

Флаг --single-transaction критичен для InnoDB: он делает консистентный снапшот без блокировки таблиц. Без него на нагруженной базе дамп может содержать противоречивые данные.

Для MySQL аналог WAL-архивирования — бинарные логи (binary logs) и инструмент Percona XtraBackup, который делает физические бэкапы без остановки сервера.

Отправка бэкапов в облако

Хранить бэкапы только локально — плохая идея. Сервер упал, диск сгорел — и бэкапы вместе с данными.

Минимальный вариант загрузки в S3 (или совместимое хранилище — Selectel, Yandex Cloud, Timeweb):

DATE=$(date +%Y%m%d_%H%M%S)
FILE="/var/backups/myapp_${DATE}.dump"

pg_dump -U postgres -Fc myapp > "$FILE"

aws s3 cp "$FILE" s3://my-bucket/postgres/$(basename $FILE) \
  --storage-class STANDARD_IA

rm "$FILE"

STANDARD_IA (Infrequent Access) — дешевле стандартного хранилища, подходит для бэкапов, которые редко читаются.

Настройте lifecycle policy в S3: например, удалять файлы старше 30 дней автоматически. Иначе через год обнаружите несколько сотен гигабайт архивов и счёт за хранение.

Шифрование бэкапов

Если в базе есть персональные данные, платёжная информация или коммерческая тайна — бэкапы нужно шифровать. Особенно если они хранятся у третьей стороны.

Простой вариант с gpg:

pg_dump -U postgres -Fc myapp | \
  gpg --encrypt --recipient backup@company.com | \
  aws s3 cp - s3://my-bucket/postgres/myapp_$(date +%Y%m%d).dump.gpg

Ключ шифрования храните отдельно от бэкапов — иначе смысл теряется.

Проверка восстановления — самая важная часть

Вот к чему всё вело. Сделать бэкап — это половина работы. Вторая половина — убедиться, что он восстанавливается.

Ручная проверка раз в месяц

Минимум — раз в месяц поднимать базу из бэкапа на тестовом сервере и проверять:

  1. Дамп разворачивается без ошибок
  2. Количество записей в ключевых таблицах совпадает с ожидаемым
  3. Приложение поднимается и работает корректно

Это занимает 30-60 минут и даёт уверенность, что в реальной аварии всё пройдёт так же.

Автоматическая верификация

Лучше — автоматизировать проверку. Раз в неделю скрипт берёт последний бэкап, разворачивает его в изолированном окружении и запускает проверочные запросы.

#!/bin/bash
# Скрипт верификации бэкапа

TEST_DB="myapp_restore_test"
BACKUP_FILE=$(aws s3 ls s3://my-bucket/postgres/ | sort | tail -1 | awk '{print $4}')

# Скачиваем последний бэкап
aws s3 cp "s3://my-bucket/postgres/$BACKUP_FILE" /tmp/restore_test.dump

# Удаляем тестовую базу если есть
dropdb --if-exists -U postgres $TEST_DB
createdb -U postgres $TEST_DB

# Восстанавливаем
pg_restore -U postgres -d $TEST_DB /tmp/restore_test.dump

if [ $? -ne 0 ]; then
  send_alert "КРИТИЧНО: Восстановление бэкапа провалилось!"
  exit 1
fi

# Проверяем количество записей
USER_COUNT=$(psql -U postgres -d $TEST_DB -t -c "SELECT COUNT(*) FROM users")

if [ "$USER_COUNT" -lt 100 ]; then
  send_alert "ПРЕДУПРЕЖДЕНИЕ: В восстановленной базе только $USER_COUNT пользователей — это подозрительно мало"
fi

# Чистим
dropdb -U postgres $TEST_DB
rm /tmp/restore_test.dump

send_alert "Верификация бэкапа прошла успешно. Пользователей в базе: $USER_COUNT"

Docker для изолированного тестирования

Ещё удобнее разворачивать тестовую базу в Docker — чисто, изолированно, не трогает продакшн-окружение:

docker run -d \
  --name pg_restore_test \
  -e POSTGRES_PASSWORD=test \
  -p 5433:5432 \
  postgres:15

sleep 5

docker exec -i pg_restore_test \
  pg_restore -U postgres -d postgres < /tmp/backup.dump

# Запускаем проверки...

docker rm -f pg_restore_test

После проверки контейнер удаляется — никаких следов.

Мониторинг и алертинг

Автоматизация без мониторинга — вещь в себе. Нужно знать:

  • Когда последний раз успешно прошёл бэкап
  • Какой размер последнего архива
  • Прошла ли последняя верификация

Простейший подход — «dead man's switch»: скрипт бэкапа после успешного выполнения пингует URL (например, Healthchecks.io или аналог). Если пинга не было дольше 25 часов — алерт.

# В конце скрипта бэкапа, после успешного выполнения:
curl -fsS --retry 3 https://hc-ping.com/ваш-uuid > /dev/null

Такой подход ловит даже ситуацию, когда скрипт просто перестал запускаться — крон сломался, сервер завис, задача удалена.

Сколько хранить и что это стоит

Типичная схема ротации:

  • Ежедневные бэкапы — 7 дней
  • Еженедельные — 4 недели
  • Ежемесячные — 12 месяцев
  • Ежегодные — 3 года (если есть регуляторные требования)

Стоимость хранения в S3-совместимых хранилищах российских провайдеров — около 1-2 ₽ за ГБ в месяц. База 10 ГБ с историей на месяц при ежедневных бэкапах — примерно 300-600 ₽/мес. Вполне подъёмно.

Что делать прямо сейчас

Если у вас нет автоматических бэкапов — это первое, что нужно исправить. Даже простой pg_dump по крону с отправкой в облако лучше, чем ничего.

Если бэкапы есть, но не проверялись — возьмите последний архив и разверните его прямо сейчас. Это займёт час, но покажет реальную картину.

Если всё настроено, но нет алертинга — добавьте хотя бы уведомления в Telegram об успехе и провале каждого бэкапа.

Команда REEXY регулярно помогает клиентам выстраивать подобную инфраструктуру в рамках скриптов автоматизации — от простых cron-задач до полноценных pipeline с верификацией и ротацией. Это один из тех случаев, когда час работы сейчас может сэкономить несколько суток нервов потом.

Чек-лист

  • Бэкапы создаются автоматически по расписанию
  • Архивы хранятся минимум в двух местах
  • Одна из копий — за пределами основного сервера
  • Настроены алерты при сбое бэкапа
  • Проверяется размер файла (не пустой)
  • Восстановление тестируется минимум раз в месяц
  • Время восстановления укладывается в RTO
  • Бэкапы зашифрованы, если содержат чувствительные данные
  • Ключи шифрования хранятся отдельно
  • Настроен мониторинг «последнего успешного бэкапа»

Бэкап, который не тестировался — это вера, а не защита. Тестируйте.