From d32a07d0a89c0919362fdd5f157856d06e017d70 Mon Sep 17 00:00:00 2001 From: jze9 Date: Tue, 11 Aug 2026 20:40:03 +0500 Subject: [PATCH] =?UTF-8?q?feat(ops):=20DR-=D1=82=D0=B5=D1=81=D1=82=20?= =?UTF-8?q?=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B1=D1=8D=D0=BA=D0=B0=D0=BF=D0=B0?= =?UTF-8?q?=20PG=20+=20runbook=20HA/DR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закрыта слепая зона «бэкапы есть, но восстановление не проверялось». - 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 --- docs/DR-HA.md | 55 +++++++++++++++++++++++++ scripts/ops/pg_restore_verify.sh | 71 ++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 docs/DR-HA.md create mode 100755 scripts/ops/pg_restore_verify.sh diff --git a/docs/DR-HA.md b/docs/DR-HA.md new file mode 100644 index 0000000..30fcb08 --- /dev/null +++ b/docs/DR-HA.md @@ -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 не критично) | diff --git a/scripts/ops/pg_restore_verify.sh b/scripts/ops/pg_restore_verify.sh new file mode 100755 index 0000000..16ea770 --- /dev/null +++ b/scripts/ops/pg_restore_verify.sh @@ -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