Legan Studio
Все статьи
~ 7 мин чтения

Резервное копирование данных бота MAX: PostgreSQL, Redis, файлы

Как организовать backup бота в MAX: pg_basebackup, WAL archiving, Redis RDB, S3-совместимые хранилища, шифрование, тестирование восстановления и RTO/RPO.

  • MAX
  • DevOps
  • надёжность

Если у вас нет тестируемой стратегии бэкапа — у вас нет бэкапа. У бота в MAX это особенно болезненно: один уронил миграцию — потерял историю диалогов 50 000 пользователей; забыл продлить S3-ключ — три недели нет копий; не проверил восстановление — обнаружил, что pg_dump делался без --clean и при ресторе всё конфликтует. В этой статье разберём прикладную стратегию backup для бота MAX: что и как часто бэкапить (PostgreSQL, Redis, медиа), куда складывать (Yandex Object Storage, MinIO, Selectel), как шифровать, как тестировать восстановление, и какие RTO/RPO выбирать под реальный SLA.

Что бэкапить в боте MAX

Минимальный список:

ИсточникЧто внутриКритичностьRPOМетод
PostgreSQLпользователи, FSM-state, заказы, платежи, логи диалоговКритично5–15 минWAL archiving + base backup
Redisкеши, rate limit, сессии, FSM (если в Redis)Среднее1 часRDB snapshot + AOF
Object storage медиаприсланные пользователями фото, документыСреднее1 суткирепликация бакета
Конфиги (.env, secrets)bot_token, API-ключи, secret_tokenКритичнопри измененииVault snapshot
Кодgitgit remote
Метрики (Prometheus)history алертовНизкое1 неделяsnapshot

RPO — recovery point objective, сколько данных мы готовы потерять. RTO — recovery time objective, как быстро поднимаемся.

PostgreSQL: pg_basebackup + WAL archiving

pg_dump хорош для миграций и dev, но для прода — только pg_basebackup + непрерывный архив WAL. Это даёт PITR (point-in-time recovery): восстановление на любую секунду.

postgresql.conf:

wal_level = replica
archive_mode = on
archive_command = 'aws --endpoint-url=https://storage.yandexcloud.net s3 cp %p s3://botmax-wal/%f'
archive_timeout = 300        # принудительный switch WAL раз в 5 минут
max_wal_senders = 3
wal_keep_size = 2GB

База бэкапа — раз в сутки:

#!/bin/bash
set -euo pipefail
DATE=$(date +%F)
DEST="/var/backups/pg/base-$DATE"

pg_basebackup -h localhost -U replicator -D "$DEST" -Ft -z -Xs -P
aws --endpoint-url=https://storage.yandexcloud.net s3 sync \
    "$DEST" "s3://botmax-pg-base/$DATE/" \
    --storage-class COLD \
    --sse AES256
rm -rf "$DEST"

Lifecycle policy в Object Storage: STANDARD 7 дней → COLD 30 дней → удаление через 90 дней. Это закрывает ретроспективу 3 месяца.

Recovery: разворот PostgreSQL из base + WAL

Сценарий: вчера вечером кто-то выполнил DELETE FROM users WHERE created_at < '2026-01-01'. Хотим откатить на 18:30.

# 1. Стоп Postgres
systemctl stop postgresql

# 2. Сохранить текущее состояние (для разбора инцидента)
mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main.broken

# 3. Скачать base backup (последний до 18:30)
mkdir -p /var/lib/postgresql/16/main
aws s3 sync s3://botmax-pg-base/2026-01-19/ /var/lib/postgresql/16/main/
tar -xzf /var/lib/postgresql/16/main/base.tar.gz -C /var/lib/postgresql/16/main/

# 4. recovery.signal + restore_command
cat > /var/lib/postgresql/16/main/postgresql.auto.conf <<EOF
restore_command = 'aws s3 cp s3://botmax-wal/%f %p'
recovery_target_time = '2026-01-19 18:30:00 MSK'
recovery_target_action = 'promote'
EOF
touch /var/lib/postgresql/16/main/recovery.signal

# 5. Старт
chown -R postgres:postgres /var/lib/postgresql/16/main
systemctl start postgresql

Проверка: SELECT pg_is_in_recovery();f после promote.

Redis: RDB + AOF

Redis в боте обычно хранит rate limit, временные сессии, FSM-state. Что критично — переезжает в PostgreSQL. Что не критично — RDB-снапшот раз в час хватит.

redis.conf:

save 900 1
save 300 10
save 60  10000
dir /var/lib/redis
dbfilename dump.rdb
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

Скрипт бэкапа Redis в S3:

#!/bin/bash
set -e
TS=$(date +%F-%H%M)
redis-cli BGSAVE
sleep 5
while [ "$(redis-cli LASTSAVE)" -lt "$(date +%s -d '1 minute ago')" ]; do
    sleep 1
done
gpg --batch --yes --passphrase-file /etc/backup/gpg.pass --symmetric --cipher-algo AES256 \
    -o "/tmp/dump-$TS.rdb.gpg" /var/lib/redis/dump.rdb
aws --endpoint-url=https://storage.yandexcloud.net s3 cp \
    "/tmp/dump-$TS.rdb.gpg" "s3://botmax-redis/dump-$TS.rdb.gpg"
rm "/tmp/dump-$TS.rdb.gpg"

Шифрование на стороне клиента

Хранение чувствительных бэкапов в S3 безопасно «по умолчанию», но для ПДн (152-ФЗ) нужно шифрование на стороне клиента — ключ известен только вам:

# Шифрование AES-256 через gpg
gpg --batch --yes --passphrase-file /etc/backup/gpg.pass \
    --symmetric --cipher-algo AES256 \
    -o backup.tar.gz.gpg backup.tar.gz

# Расшифровка
gpg --batch --yes --passphrase-file /etc/backup/gpg.pass \
    -o backup.tar.gz -d backup.tar.gz.gpg

GPG-пароль храните в Vault / KMS / SOPS. Не в том же S3, где бэкапы. Иначе теряет смысл.

Тестирование восстановления

Самая частая ошибка — backup делается, но восстановление никто не пробовал. К моменту реального инцидента выясняется, что pg_basebackup за полгода сменил формат, S3-ключ протух, или на recovery-сервере не хватает места.

Минимум — раз в месяц, автоматизированный «restore drill»:

#!/bin/bash
# Раз в месяц cron поднимает временный контейнер,
# восстанавливает последний бэкап, прогоняет smoke test
docker run -d --name pg-restore-test \
    -e POSTGRES_PASSWORD=test postgres:16

# Скачать последний base + WAL
LATEST=$(aws s3 ls s3://botmax-pg-base/ | tail -1 | awk '{print $4}')
aws s3 sync "s3://botmax-pg-base/$LATEST" /tmp/restore/

docker exec pg-restore-test bash -c "
    pg_restore -d postgres -h localhost -U postgres /tmp/restore/base.tar
    psql -c 'SELECT count(*) FROM users' | grep -E '^[0-9]+$'
"

# Сравнить количество строк с ожидаемым
RESTORED=$(docker exec pg-restore-test psql -t -c "SELECT count(*) FROM users")
EXPECTED=$(psql -h prod -t -c "SELECT count(*) FROM users")
DIFF=$((EXPECTED - RESTORED))
if [ "$DIFF" -gt 100 ]; then
    /usr/local/bin/alert "Restore drill failed: diff=$DIFF rows"
fi
docker rm -f pg-restore-test

Бэкап медиа в Object Storage

Если бот хранит присланные пользователями фото/документы в S3-совместимом хранилище — настройте репликацию бакетов между регионами:

  • Источник: botmax-media в Yandex Cloud, регион ru-central1
  • Назначение: botmax-media-dr в ru-central1-b (другая зона)

Для критичных данных (медицинский бот, юридические документы) — реплика в другой провайдер: например, основной — Yandex Object Storage, реплика — Selectel S3. Это страхует от инцидентов на уровне облака.

Структура бэкапа

s3://botmax-backups/
├── pg/
│   ├── base/
│   │   ├── 2026-01-15/  
│   │   ├── 2026-01-16/  
│   │   └── ...
│   └── wal/
│       ├── 000000010000000000000001
│       └── ...
├── redis/
│   └── dump-2026-01-19-1200.rdb.gpg
├── vault/
│   └── snapshot-2026-01-19.snap.gpg
└── config/
    └── ansible-2026-01-19.tar.gz.gpg

Ретеншн:

  • WAL — 14 дней (для PITR на 2 недели);
  • base — ежедневный 30 дней + еженедельный 12 недель + ежемесячный 12 месяцев;
  • Redis RDB — 7 дней;
  • Vault snapshot — 90 дней.

Мониторинг бэкапа

Без алертов «бэкап не сделался» легко прожить полгода без копий. Минимум:

  • Prometheus exporter, который раз в час проверяет timestamp последнего файла в S3;
  • Алерт «backup older than 25 hours» (для дневных) и «WAL not archived 30 min» (для непрерывных);
  • Дашборд: размер последнего бэкапа vs средний (резкий рост — заполнили всё, резкое падение — что-то отвалилось);
  • Smoke test после каждого восстановления.
# Prometheus alert
- alert: BackupTooOld
  expr: time() - max(s3_object_last_modified_timestamp{bucket="botmax-pg-base"}) > 90000
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "PostgreSQL base backup older than 25 hours"

RPO/RTO — реалистичные цифры

Для среднего коммерческого бота в MAX (выручка через бота 100K–10M ₽/мес):

СценарийRPORTOСтоимость инфраструктуры
Корпоративный/HR бот24 ч4 ч500–2 000 ₽/мес
Магазин, запись на услуги1 ч1 ч2 000–5 000 ₽/мес
Платежи, медкабинет5 мин30 мин5 000–15 000 ₽/мес
Финтех, биржа< 1 мин< 5 мин30 000+ ₽/мес

Снижение RPO с 1 ч до 5 мин требует WAL archiving + быстрый network до S3 (часто канал в Object Storage в том же регионе).

Disaster recovery: full failover

Если упал весь регион (редко, но бывает): runbook должен быть написан и прорепетирован.

  1. Поднимаем PostgreSQL на standby в другом регионе (Patroni / managed service).
  2. Расшифровываем последний base + WAL до точки сбоя.
  3. Поднимаем Redis из RDB.
  4. Меняем DNS A-запись бэкенда (TTL 60 секунд!).
  5. Перерегистрируем webhook на новый URL через setWebhook.
  6. Smoke test: тестовое сообщение боту → проверка ответа.

Цель — RTO < 1 час. Достигается только тренировкой команды раз в квартал.

Защита от ransomware и compromised credentials

Объёмный риск 2026 года — атакующий получает доступ к S3 и удаляет все бэкапы. Защита:

  • Object Lock / WORM на бакете бэкапов: файлы нельзя удалить или перезаписать в течение N дней.
  • Версионирование бакета: даже delete оставляет старую версию.
  • Отдельный read-only ключ для бэкап-сервера → запись разрешена, удаление запрещено через bucket policy.
  • Air-gapped копия раз в неделю — на оффлайн-носитель / в другой облачный аккаунт.
  • MFA на удаление для админ-консоли.

Итого

Backup бота MAX строится на стеке: PostgreSQL с WAL archiving + ежедневным pg_basebackup, Redis RDB + AOF раз в час, медиа через репликацию S3-бакетов в другую зону, Vault snapshot для секретов. Всё шифруется на стороне клиента (gpg AES-256), складывается в S3-совместимое хранилище с lifecycle policy и Object Lock. Восстановление тестируется раз в месяц автоматически. Алерты на «backup older than 25h» и «WAL not archived 30 min» в Prometheus. RPO 5–15 минут и RTO 30 минут — реальная цель для коммерческого бота за 5–15 тыс. ₽/мес. Главное правило: незатестированный бэкап не считается бэкапом.

Частые вопросы

Чем pg_basebackup лучше pg_dump для бэкапа PostgreSQL?

pg_dump делает логический бэкап в виде SQL-скрипта или бинарного дампа — он удобен для миграций и dev, но восстановление крупной базы занимает часы. pg_basebackup делает физическую копию файлов кластера, поверх неё разворачивается WAL для PITR. Это даёт восстановление на любую секунду, а не только на момент дампа. Для прода с RPO < 1 часа выбор однозначный — pg_basebackup + непрерывный архив WAL. pg_dump оставьте для миграций между версиями и переноса схемы.

Зачем шифровать бэкап на стороне клиента, если S3 уже шифрует?

Server-side encryption (SSE) защищает только от физической кражи дисков провайдера — данные расшифровываются автоматически любым, у кого есть ключ доступа к бакету. Если ключ AWS-API утечёт через скомпрометированный CI или сотрудника — атакующий скачает все бэкапы в plain. Client-side шифрование (gpg AES-256 с паролем в Vault) добавляет второй слой: даже с доступом к бакету без passphrase ничего не получить. Для ПДн по 152-ФЗ это фактически обязательно.

Как часто тестировать восстановление?

Минимум — раз в месяц автоматический restore drill: cron поднимает временный контейнер, скачивает последний бэкап, восстанавливает, прогоняет smoke test (сравнение количества строк в ключевых таблицах с продом). Раз в квартал — ручной DR-учения с командой: имитируем падение региона, поднимаем всё в DR-окружении, замеряем фактический RTO. Без этого реальное восстановление в кризисе превращается в гадание — формат бэкапа изменился, ключ протух, на сервере не хватает места.

Какой RPO/RTO выбрать для бота с записью на услуги?

Для коммерческого бота в MAX с записью на услуги (салон, клиника, фитнес) реалистичные цели — RPO 1 час и RTO 1 час. Это означает WAL archiving раз в час, ежедневный base backup, отдельный standby Postgres в горячем резерве. Стоимость инфраструктуры 2–5 тыс. ₽/мес. Если в боте есть платежи и хранение медданных — RPO 5–15 минут и RTO 30 минут с непрерывным WAL archiving в S3 и автоматическим failover через Patroni.

Как защитить бэкапы от удаления при компрометации?

Включайте Object Lock (WORM-режим) на бакете бэкапов — файлы нельзя удалить N дней. Добавьте версионирование, чтобы даже delete оставлял предыдущую версию. Доступ для бэкап-сервера выдавайте отдельным IAM-ключом с правами put-object и list, без delete-object. Раз в неделю делайте air-gapped копию в отдельный облачный аккаунт или физический оффлайн-носитель. Для админ-консоли S3 включайте MFA. Ransomware-сценарий 2026 года стал слишком частым, чтобы игнорировать эту угрозу.

Где хранить ключи шифрования бэкапов?

Не в том же бакете, что бэкапы — это убивает смысл шифрования. Варианты: HashiCorp Vault, Yandex KMS, SOPS-encrypted файл в отдельном приватном git-репозитории, аппаратный YubiKey. Доступ к ключу — только сервис-аккаунту бэкап/восстановления и 1–2 админам через MFA. Дополнительно — Shamir's Secret Sharing: ключ разбит на 5 частей, для восстановления нужно 3 — защищает от потери одного админа и от компрометации одного источника.

Нужно ли бэкапить Redis, если данные в нём временные?

Зависит от того, что вы кладёте в Redis. Если только rate limit, кеш и короткие сессии — потеря не критична, бэкап не нужен. Если в Redis FSM-state открытых диалогов, незавершённые корзины, временные коды двухфакторки — RDB snapshot раз в час + AOF с appendfsync everysec даёт RPO 1 секунду. Лучшая стратегия — критичные данные (заказы, согласия ПДн, платежи) хранить в PostgreSQL, а Redis оставить под действительно временные структуры.