Перейти к основному содержимому

Практикум DR — катастрофа и restore

Учебное упражнение имитирует полную потерю данных PostgreSQL и проверяет, что offsite-бэкап реально возвращает сервис. Это restore drill — запланированное учение, результат которого (фактический RTO, наблюдаемый RPO gap) заносят в runbook. Без такого учения DR остаётся теорией.

Только lab
Не выполняйте на production. Нужен свежий dump в 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.

ПолеПример значения
Дата drill2025-06-15
Фактический RTO3 мин 40 сек
Целевой RTO1 час
Фактический RPO (наблюдённый)6 h (интервал cron)
Целевой RPO6 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

СценарийЧто видитеДействие
Битый dumparchive is corruptedВзять предыдущий файл из retention
Неверная версия PGошибки extensionRestore на ту же major версию 16
Пустой ordersrestore в wrong DB-d shop, проверить --create
Долгий S3 downloadRTO > целиДержать hot copy local + cold S3

Каждый сценарий отрабатывают на отдельном учении — таблица пополняется реальными строками из вашего lab.


10. Метрики для runbook и Grafana

Запишите в runbook таблицу последних трёх drill:

ДатаRTO фактRPO gapDump fileПримечание
2025-06-153m 40s6 hshop-20250615-0300.dumpOK

При подключении мониторинга экспортируйте "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 — чеклист

РаундЦельОжидание
1Baseline RTOВсе строки до dump на месте
2RPO gaporder-after-backup отсутствует

Между раундами обновите runbook фактическим RTO раунда 1. Раунд 2 эмоционально ближе к реальному инциденту — пользователи "теряют" свежие данные.


15. Эскалация в production (шаблон)

УровеньКтоДействие
L1on-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

ШагТемаРаздел
PITRWAL archive8.11/10
HAPatroni / replica8.11/9
CostOffsite tierFinOps
MonitorBackup age alertPrometheus practicum

Приложение — полный timeline учения (пример)

ВремяСобытие
T0 14:00:00Объявлен drill, зафиксирован timestamp
T0+1mdocker stop/rm, volume rm
T0+2mdocker run new container
T0+3mpg_restore start
T0+5mpg_restore end, SELECT ok
T1 14:05:30RTO = 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 archivepg_restore --listolder dump
wrong PG versiondocker image tagpostgres:16-alpine
empty orderswrong -d database-d shop
port in usess -tlnp | grep 5433stop old container
slow S3 downloadtime aws s3 cphot copy local + cold S3

Чек-лист завершения шага 3

#КритерийОжидание
1T0/T1 записаныISO timestamps
2RTO < targetнапример 5m < 1h
3RPO gap shownorder-after-backup lost
4Runbook v1.0last test restore date
5Post-incident tableзаполнена
6Quarterly eventв календаре

Итоги · Чек-лист


Предупреждения безопасности

  1. Drill только на lab контейнерах.
  2. Production restore — change ticket + approval.
  3. Dump с PII — шифрование offsite.
  4. Не публикуйте dump в public bucket.
  5. После drill проверьте, что prod connection string не переключился случайно.