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:
jze9
2026-08-11 20:40:03 +05:00
parent d2d99d8231
commit d32a07d0a8
2 changed files with 126 additions and 0 deletions

55
docs/DR-HA.md Normal file
View 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 не критично) |

View 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