feat(ops): DR-тест восстановления бэкапа PG + runbook HA/DR
Закрыта слепая зона «бэкапы есть, но восстановление не проверялось». - scripts/ops/pg_restore_verify.sh — берёт последний дамп из MinIO backups/pg/, restore в ЭФЕМЕРНЫЙ postgres:16, проверяет ключевые таблицы (users/documents/ tasks). Прод не трогает, всё в одноразовом контейнере. Под cron раз в неделю. - docs/DR-HA.md — runbook: бэкапы, проверка восстановления, потоковая репликация PG, Redis-реплика+Sentinel, таблица SPOF со статусом митигаций. Провижн реплик PG/Redis — на Proxmox (нужен новый LXC), это работа на железе, не в репозитории; runbook даёт конкретные шаги. Векторный SPOF уже снимается Qdrant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
55
docs/DR-HA.md
Normal file
55
docs/DR-HA.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Отказоустойчивость и восстановление (HA / DR)
|
||||||
|
|
||||||
|
Актуальная топология: PostgreSQL на выделенном LXC **1.38**, Redis на **1.35**,
|
||||||
|
RabbitMQ на **192.168.20.82**, app+ES на **1.32**. Это не docker-compose, поэтому
|
||||||
|
HA для БД/кэша делается на уровне Proxmox/LXC, а не в этом репозитории.
|
||||||
|
|
||||||
|
## 1. Бэкапы (сделано)
|
||||||
|
|
||||||
|
`pg_dump → gzip → MinIO backups/pg/`, cron ежедневно 03:00 на 1.38, ротация 14 дней.
|
||||||
|
|
||||||
|
## 2. Проверка восстановимости (сделано — было слепой зоной)
|
||||||
|
|
||||||
|
Бэкап без проверенного восстановления = отсутствие бэкапа. Скрипт
|
||||||
|
[`scripts/ops/pg_restore_verify.sh`](../scripts/ops/pg_restore_verify.sh) берёт
|
||||||
|
последний дамп из MinIO, restore в **эфемерный** `postgres:16` и проверяет ключевые
|
||||||
|
таблицы. Прод не трогает.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/ops/pg_restore_verify.sh # креды из .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Рекомендация: cron раз в неделю на 1.32, алерт (как в antiplag_monitor) при ненулевом коде.
|
||||||
|
|
||||||
|
## 3. HA PostgreSQL — потоковая репликация (требует новый LXC)
|
||||||
|
|
||||||
|
Провижн реплики — задача на Proxmox (нужен ещё один LXC, напр. 1.39). Шаги:
|
||||||
|
|
||||||
|
1. **Primary (1.38)** `postgresql.conf`: `wal_level=replica`, `max_wal_senders=5`,
|
||||||
|
`wal_keep_size=1GB`; `pg_hba.conf`: строка `replication` для IP реплики.
|
||||||
|
2. **Replica**: `pg_basebackup -h 1.38 -U replicator -D $PGDATA -R` (создаёт
|
||||||
|
`standby.signal` + `primary_conninfo`), затем старт — догоняет primary по WAL.
|
||||||
|
3. **Failover**: ручной `pg_ctl promote` на реплике + переключение `POSTGRES_HOST`
|
||||||
|
в `.env` (или автоматизация через Patroni + etcd — если нужен авто-failover).
|
||||||
|
4. Проверка лага: `SELECT * FROM pg_stat_replication` на primary.
|
||||||
|
|
||||||
|
## 4. HA Redis — реплика + Sentinel (требует новый LXC)
|
||||||
|
|
||||||
|
Redis у нас — кэш/rate-limits/LSH-индекс (префикс `antiplag_lsh`). Потеря = деградация,
|
||||||
|
не потеря данных задач (они в PG). Если нужна устойчивость:
|
||||||
|
|
||||||
|
1. Второй Redis (реплика): `replicaof 1.35 6379` + тот же `requirepass`.
|
||||||
|
2. **3× Sentinel** (на app-хостах): `sentinel monitor antiplag 1.35 6379 2`,
|
||||||
|
авто-переключение мастера.
|
||||||
|
3. Клиенты (Celery/кэш) → на Sentinel-aware подключение (`redis.sentinel`),
|
||||||
|
либо оставить прямое подключение + ручной перевод `REDIS_URL` при аварии.
|
||||||
|
|
||||||
|
## 5. Единые точки отказа — статус
|
||||||
|
|
||||||
|
| Компонент | SPOF | Митигация |
|
||||||
|
|-----------|:----:|-----------|
|
||||||
|
| PostgreSQL 1.38 | да | бэкап+restore-тест ✅; реплика — §3 (нужен LXC) |
|
||||||
|
| Redis 1.35 | да | graceful-фолбэк LSH в память ✅; реплика+Sentinel — §4 |
|
||||||
|
| Векторный индекс | было | `VECTOR_BACKEND=qdrant` снимает (см. README) ✅ |
|
||||||
|
| RabbitMQ .82 | да | мониторинг ловит падение ✅; кластер — по потребности |
|
||||||
|
| app/ES 1.32 | да | воркеры горизонтальны; ES single-node (для BM25 не критично) |
|
||||||
71
scripts/ops/pg_restore_verify.sh
Executable file
71
scripts/ops/pg_restore_verify.sh
Executable file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# DR-тест: проверка ВОССТАНОВИМОСТИ бэкапа PostgreSQL.
|
||||||
|
#
|
||||||
|
# Берёт последний дамп из MinIO (backups/pg/), поднимает ЭФЕМЕРНЫЙ контейнер
|
||||||
|
# postgres:16, восстанавливает в него дамп и проверяет, что ключевые таблицы не
|
||||||
|
# пусты. Прод не трогается — всё в одноразовом контейнере/сети, удаляются на выходе.
|
||||||
|
#
|
||||||
|
# Зачем: бэкапы делаются (cron на 1.38), но восстановление раньше никто не проверял —
|
||||||
|
# «непроверенный бэкап = отсутствие бэкапа». Запускать по cron раз в неделю.
|
||||||
|
#
|
||||||
|
# Требуется docker + доступ до MinIO. Креды берутся из .env (или окружения):
|
||||||
|
# MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY,
|
||||||
|
# MINIO_BUCKET_BACKUPS (=backups), POSTGRES_DB, POSTGRES_USER.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
if [ -f "$ROOT/.env" ]; then set -a; . "$ROOT/.env"; set +a; fi
|
||||||
|
|
||||||
|
: "${MINIO_ENDPOINT:?нужен MINIO_ENDPOINT}"
|
||||||
|
: "${MINIO_ACCESS_KEY:?нужен MINIO_ACCESS_KEY}"
|
||||||
|
: "${MINIO_SECRET_KEY:?нужен MINIO_SECRET_KEY}"
|
||||||
|
BUCKET="${MINIO_BUCKET_BACKUPS:-backups}"
|
||||||
|
PREFIX="${PG_BACKUP_PREFIX:-pg/}"
|
||||||
|
PGDB="${POSTGRES_DB:-antiplagiator}"
|
||||||
|
PGUSER="${POSTGRES_USER:-antiplagiator}"
|
||||||
|
|
||||||
|
SUFFIX="$$-$RANDOM"
|
||||||
|
PG="drtest-pg-$SUFFIX"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
MC_HOST="http://${MINIO_ACCESS_KEY}:${MINIO_SECRET_KEY}@${MINIO_ENDPOINT}"
|
||||||
|
|
||||||
|
cleanup() { docker rm -f "$PG" >/dev/null 2>&1 || true; rm -rf "$WORK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "▶ Поиск последнего дампа в minio://$BUCKET/$PREFIX"
|
||||||
|
LATEST="$(docker run --rm -e MC_HOST_dr="$MC_HOST" minio/mc:latest \
|
||||||
|
sh -c "mc ls --recursive dr/$BUCKET/$PREFIX | awk '{print \$NF}' | grep -E '\.(sql\.)?gz$' | sort | tail -1")"
|
||||||
|
[ -n "$LATEST" ] || { echo "❌ дампы не найдены"; exit 1; }
|
||||||
|
FNAME="$(basename "$LATEST")"
|
||||||
|
echo " последний: $FNAME"
|
||||||
|
|
||||||
|
echo "▶ Скачивание дампа"
|
||||||
|
docker run --rm -e MC_HOST_dr="$MC_HOST" -v "$WORK":/w minio/mc:latest \
|
||||||
|
sh -c "mc cp 'dr/$BUCKET/$PREFIX$FNAME' /w/dump.gz"
|
||||||
|
|
||||||
|
echo "▶ Эфемерный postgres:16"
|
||||||
|
docker run -d --name "$PG" \
|
||||||
|
-e POSTGRES_PASSWORD=drtest -e POSTGRES_DB="$PGDB" -e POSTGRES_USER="$PGUSER" \
|
||||||
|
postgres:16 >/dev/null
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
docker exec "$PG" pg_isready -U "$PGUSER" -d "$PGDB" >/dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "▶ Восстановление"
|
||||||
|
gunzip -c "$WORK/dump.gz" | docker exec -i "$PG" psql -v ON_ERROR_STOP=0 -U "$PGUSER" -d "$PGDB" >/dev/null 2>&1
|
||||||
|
|
||||||
|
echo "▶ Проверка ключевых таблиц"
|
||||||
|
rc=0
|
||||||
|
for tbl in users documents tasks; do
|
||||||
|
n="$(docker exec "$PG" psql -U "$PGUSER" -d "$PGDB" -tAc "SELECT count(*) FROM $tbl" 2>/dev/null | tr -d '[:space:]' || echo ERR)"
|
||||||
|
echo " $tbl: ${n:-ERR}"
|
||||||
|
{ [ -z "$n" ] || [ "$n" = "ERR" ]; } && rc=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
echo "✅ Бэкап восстанавливается, ключевые таблицы на месте ($FNAME)"
|
||||||
|
else
|
||||||
|
echo "❌ Восстановление не прошло проверку — разобраться СРОЧНО"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user