Практикум DR — катастрофа и restore
Учебное упражнение имитирует полную потерю данных PostgreSQL и проверяет, что offsite-бэкап реально возвращает сервис. Это restore drill — запланированное учение, результат которого (фактический RTO, наблюдаемый RPO gap) заносят в runbook. Без такого учения DR остаётся теорией.
backup-offsite или в облачном bucket. Сохраните копию runbook и dump перед началом.1. Зафиксируйте время начала (T0)
RTO отсчитывают от момента, когда команда начала восстановление (или от официального объявления инцидента — в lab достаточно T0 в момент "катастрофы"). Запишите время в ISO формате.
date -Iseconds # T0
Сохраните T0 в блокнот, runbook или ticket "DR drill 2025-06-15". Если между T0 и завершением restore вы отвлекались на чай — для честного RTO в enterprise фиксируют только "hands-on keyboard" время; в pet-lab можно считать wall-clock от T0 до T1.
2. Учебная катастрофа
Остановите контейнер, удалите его и уничтожьте volume с данными. Это аналог "диск сгорел" или "VM удалена без snapshot".
docker stop pg-dr-lab && docker rm pg-dr-lab
docker volume rm pgdata
Проверьте, что volume исчез и порт 5433 свободен:
docker volume ls | grep pgdata
ss -tlnp | grep 5433 || true
На этом этапе база shop полностью потеряна на этой машине. Единственный путь назад — offsite dump из шага 2.
3. Новый инстанс и restore
Поднимите новый контейнер с новым volume (pgdata-new), чтобы не смешать пустой каталог с остатками старых файлов.
docker run -d --name pg-dr-lab-new -e POSTGRES_PASSWORD=lab -e POSTGRES_DB=shop \
-v pgdata-new:/var/lib/postgresql/data -p 5433:5432 postgres:16-alpine
sleep 5
Выберите последний dump в offsite (по времени модификации):
DUMP=$(ls -t ./backup-offsite/*.dump | head -1)
echo "Restoring from $DUMP"
Если offsite в S3, сначала скачайте файл на хост:
# aws s3 cp s3://my-dr-bucket/shop/latest.dump ./backup-offsite/
pg_restore с --clean --if-exists удаляет существующие объекты перед созданием — удобно при restore в непустую базу shop, которую Postgres создал при старте контейнера.
docker exec -i pg-dr-lab-new pg_restore -U postgres -d shop --clean --if-exists < "$DUMP"
Предупреждения pg_restore о правах или extensions в lab часто можно игнорировать, если SELECT возвращает строки. Ошибки FATAL или could not read — признак битого dump; учение считается проваленным, нужен более старый файл из retention.
Проверка данных и фиксация T1:
docker exec pg-dr-lab-new psql -U postgres -d shop -c "SELECT * FROM orders;"
date -Iseconds # T1 — RTO ≈ T1 - T0
Вычислите разницу T1 − T0 в минутах. Сравните с целевым RTO из шага 1. Если restore занял 4 минуты при цели "1 час" — запас есть; если 90 минут — runbook или инфраструктура требуют доработки (pre-pull образа, automation, smaller dump).
4. Упражнение на RPO gap
RPO на практике виден как разрыв данных между последним dump и моментом "катастрофы". Проведите второй раунд drill.
Снова поднимите рабочий контейнер (можно переименовать для ясности), добавьте заказ после timestamp последнего dump:
docker exec pg-dr-lab-new psql -U postgres -d shop -c \
"INSERT INTO orders(item) VALUES ('order-after-backup');"
docker exec pg-dr-lab-new psql -U postgres -d shop -c "SELECT * FROM orders;"
Не делайте новый dump. Повторите катастрофу (stop, rm, volume rm) и restore из того же offsite dump, что и в первом раунде.
После restore выполните SELECT * FROM orders. Строка order-after-backup исчезнет — это и есть RPO gap. Пользователь, оформивший заказ после бэкапа, потеряет его при восстановлении. Закрыть gap можно более частым pg_dump, incremental backup или WAL archiving (8.11/10).
Запишите в runbook наблюдение: "При RPO 6h заказ в 14:00 при dump в 03:00 не восстанавливается".
5. Post-incident и обновление runbook
После учения оформите короткий post-incident (даже для lab) — таблица или три абзаца в wiki.
| Поле | Пример значения |
|---|---|
| Дата drill | 2025-06-15 |
| Фактический RTO | 3 мин 40 сек |
| Целевой RTO | 1 час |
| Фактический RPO (наблюдённый) | 6 h (интервал cron) |
| Целевой RPO | 6 h |
| Проблемы | sleep 5 мало на медленном диске — увеличить до 10 |
| Действия | Quarterly restore test в календаре |
Заведите повторяющееся событие "quarterly restore test" — раз в квартал повторяйте шаги 1–3 с новым dump. Обновите в runbook строку last successful test restore и фактический RTO.
Если RTO или RPO не укладываются в цели — меняют расписание бэкапа, добавляют automation (Terraform, Ansible), pre-baked образ Docker или managed Postgres с автоматическим PITR вместо одного cron без проверки restore.
6. Скрипт полного restore drill
Сохраните restore-drill.sh рядом с runbook. На учении запускайте его целиком — меньше ручных опечаток.
#!/bin/bash
set -euo pipefail
echo "T0=$(date -Iseconds)"
docker stop pg-dr-lab 2>/dev/null || true
docker rm pg-dr-lab 2>/dev/null || true
docker volume rm pgdata 2>/dev/null || true
docker run -d --name pg-dr-lab-new -e POSTGRES_PASSWORD=lab -e POSTGRES_DB=shop \
-v pgdata-new:/var/lib/postgresql/data -p 5433:5432 postgres:16-alpine
sleep 10
DUMP=$(ls -t ./backup-offsite/*.dump | head -1)
docker exec -i pg-dr-lab-new pg_restore -U postgres -d shop --clean --if-exists < "$DUMP"
docker exec pg-dr-lab-new psql -U postgres -d shop -c "SELECT count(*) FROM orders;"
echo "T1=$(date -Iseconds)"
Сравните вывод count(*) с числом строк до катастрофы (без учёта order-after-backup в первом раунде).
7. Коммуникация во время учения
В production во время DR фиксируют статус для пользователей. В lab имитируйте короткое сообщение:
[DR drill] База shop восстанавливается из бэкапа. Ожидаемое окно 30 min.
Обновление: restore завершён, проверяем данные.
Шаблон снижает панику в реальном инциденте. В runbook добавьте ссылку на status page или Telegram-канал pet-проекта.
8. Возврат к production-именам
После drill контейнер называется pg-dr-lab-new. Для постоянного lab переименуйте или обновите runbook под фактическое имя. В production после restore меняют DNS или connection string приложения на новый инстанс — в Docker lab достаточно порта 5433 и того же пароля.
docker stop pg-dr-lab-new
docker rename pg-dr-lab-new pg-dr-lab
9. Сценарии сбоя restore
| Сценарий | Что видите | Действие |
|---|---|---|
| Битый dump | archive is corrupted | Взять предыдущий файл из retention |
| Неверная версия PG | ошибки extension | Restore на ту же major версию 16 |
| Пустой orders | restore в wrong DB | -d shop, проверить --create |
| Долгий S3 download | RTO > цели | Держать hot copy local + cold S3 |
Каждый сценарий отрабатывают на отдельном учении — таблица пополняется реальными строками из вашего lab.
10. Метрики для runbook и Grafana
Запишите в runbook таблицу последних трёх drill:
| Дата | RTO факт | RPO gap | Dump file | Примечание |
|---|---|---|---|---|
| 2025-06-15 | 3m 40s | 6 h | shop-20250615-0300.dump | OK |
При подключении мониторинга экспортируйте "hours since last successful backup" — см. 8.11/11.
12. Smoke test приложения после restore
SQL SELECT count(*) — минимум. Добавьте проверку, которую делает app:
curl -sf http://localhost:8080/health | jq .
# ожидаем {"db":"ok"}
Если API в другом compose, перезапустите app после Postgres — connection pool может держать мёртвые соединения. Restart app входит в runbook как шаг после pg_restore.
13. Параллельный pg_restore на больших dump
Флаг -j 4 ускоряет restore custom dump:
docker exec pg-dr-lab-new pg_restore -U postgres -d shop -j 4 --clean --if-exists < "$DUMP"
На маленьком учебном dump выигрыш секунды; на гигабайтах — минуты RTO. Runbook для production указывает -j по числу vCPU.
14. Два раунда drill — чеклист
| Раунд | Цель | Ожидание |
|---|---|---|
| 1 | Baseline RTO | Все строки до dump на месте |
| 2 | RPO gap | order-after-backup отсутствует |
Между раундами обновите runbook фактическим RTO раунда 1. Раунд 2 эмоционально ближе к реальному инциденту — пользователи "теряют" свежие данные.
15. Эскалация в production (шаблон)
| Уровень | Кто | Действие |
|---|---|---|
| L1 | on-call | Запуск runbook restore |
| L2 | владелец продукта | Решение о принятии потери данных или ожидании WAL |
| L3 | провайдер | Ticket при physical DC failure |
В pet-проекте все три роли — вы; шаблон готовит к команде из двух человек.
11. Что записать в runbook после drill
Дополните runbook фактическими командами, которые сработали у вас (имена volume, путь к DUMP, версия Postgres). Укажите контакт on-call и ссылку на bucket. Приложите скрин или вывод SELECT после успешного restore — доказательство для будущего себя.
16. Lessons learned — шаблон wiki
# DR drill YYYY-MM-DD
## Summary
Full restore from offsite dump after volume delete.
## Metrics
RTO: Xm Ys | Target: 1h | RPO gap demonstrated: yes
## What went well
- ...
## What failed
- ...
## Action items
- [ ] Update runbook section ...
Копируйте шаблон после каждого учения — через год будет история улучшений RTO.
17. Следующий уровень после practicum
| Шаг | Тема | Раздел |
|---|---|---|
| PITR | WAL archive | 8.11/10 |
| HA | Patroni / replica | 8.11/9 |
| Cost | Offsite tier | FinOps |
| Monitor | Backup age alert | Prometheus practicum |
Приложение — полный timeline учения (пример)
| Время | Событие |
|---|---|
| T0 14:00:00 | Объявлен drill, зафиксирован timestamp |
| T0+1m | docker stop/rm, volume rm |
| T0+2m | docker run new container |
| T0+3m | pg_restore start |
| T0+5m | pg_restore end, SELECT ok |
| T1 14:05:30 | RTO = 5m 30s |
Сохраните свою таблицу в post-incident — baseline для сравнения через квартал.
Приложение — интеграция с приложением
После restore connection string app указывает на localhost:5433. При смене пароля обновите .env и перезапустите app. Runbook app-level: migrate если schema изменилась между dump и кодом — редкий edge case при lab без deploy между backup и drill.
Полный restore drill — одна команда
cd ~/dr-lab
chmod +x restore-drill.sh 2>/dev/null || true
./restore-drill.sh
Если скрипта нет — создайте из раздела 6 и запустите. Ожидаемый финал:
T0=2026-06-15T14:00:00+03:00
count
-------
2
T1=2026-06-15T14:05:30+03:00
Runbook v1.0 — полный текст после drill
# DR Runbook shop v1.0
Last successful test restore: 2026-06-15
Measured RTO: 5m 30s (target 1h)
Measured RPO gap: demonstrated yes (order-after-backup lost)
## Commands (verified)
T0=$(date -Iseconds)
docker stop pg-dr-lab; docker rm pg-dr-lab; docker volume rm pgdata
docker run -d --name pg-dr-lab-new -e POSTGRES_PASSWORD=lab -e POSTGRES_DB=shop \
-v pgdata-new:/var/lib/postgresql/data -p 5433:5432 postgres:16-alpine
sleep 10
DUMP=$(ls -t ./backup-offsite/*.dump | head -1)
docker exec -i pg-dr-lab-new pg_restore -U postgres -d shop --clean --if-exists < "$DUMP"
docker exec pg-dr-lab-new psql -U postgres -d shop -c "SELECT count(*) FROM orders;"
T1=$(date -Iseconds)
## Issues found
- sleep 5 → 10 for slow disk
## Next drill
- Quarterly calendar event
RPO gap exercise — пошагово
# После успешного restore раунда 1
docker exec pg-dr-lab-new psql -U postgres -d shop -c \
"INSERT INTO orders(item) VALUES ('order-after-backup');"
docker exec pg-dr-lab-new psql -U postgres -d shop -c "SELECT * FROM orders;"
# 3 строки — НЕ делайте новый dump
# Катастрофа снова
docker stop pg-dr-lab-new && docker rm pg-dr-lab-new && docker volume rm pgdata-new
# restore из ТОГО ЖЕ dump
DUMP=$(ls -t ./backup-offsite/*.dump | head -1)
docker run -d --name pg-dr-lab -e POSTGRES_PASSWORD=lab -e POSTGRES_DB=shop \
-v pgdata:/var/lib/postgresql/data -p 5433:5432 postgres:16-alpine
sleep 10
docker exec -i pg-dr-lab pg_restore -U postgres -d shop --clean --if-exists < "$DUMP"
docker exec pg-dr-lab psql -U postgres -d shop -c "SELECT * FROM orders;"
Ожидаемо — 2 строки, order-after-backup отсутствует. Это RPO gap.
Ожидаемый вывод pg_restore
Успех — stderr с WARNING, без FATAL:
pg_restore: warning: errors ignored on restore: N
Провал — искать:
pg_restore: error: could not read from input file: end of file
pg_restore: error: archive is corrupted
При провале возьмите предыдущий dump из retention.
Расширенное устранение неполадок restore
| Симптом | Диагностика | Решение |
|---|---|---|
| corrupted archive | pg_restore --list | older dump |
| wrong PG version | docker image tag | postgres:16-alpine |
| empty orders | wrong -d database | -d shop |
| port in use | ss -tlnp | grep 5433 | stop old container |
| slow S3 download | time aws s3 cp | hot copy local + cold S3 |
Чек-лист завершения шага 3
| # | Критерий | Ожидание |
|---|---|---|
| 1 | T0/T1 записаны | ISO timestamps |
| 2 | RTO < target | например 5m < 1h |
| 3 | RPO gap shown | order-after-backup lost |
| 4 | Runbook v1.0 | last test restore date |
| 5 | Post-incident table | заполнена |
| 6 | Quarterly event | в календаре |
Предупреждения безопасности
- Drill только на lab контейнерах.
- Production restore — change ticket + approval.
- Dump с PII — шифрование offsite.
- Не публикуйте dump в public bucket.
- После drill проверьте, что prod connection string не переключился случайно.