# Отказоустойчивость и восстановление (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` в Infisical (окружение `prod` — не в `.env` на сервере напрямую, его перезапишет следующий деплой, см. ARCHITECTURE.md §10) и передеплой/рестарт сервисов (или автоматизация через 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 не критично) | ## 6. Известный операционный риск — RabbitMQ `consumer_timeout` vs долгие таски Обнаружено на практике (2026-08-26): RabbitMQ по умолчанию рвёт канал, если consumer не сделал ack за 1800с. Любой Celery-таск с retry-логикой (rate-limit backoff у парсеров, ожидание зависшего внешнего сервиса вроде Ollama), суммарно занимающий дольше — брокер обрывает соединение раньше, чем таск успевает сдаться и подтвердиться, весь Celery-процесс падает (`exitCode=1`), Docker (`restart: unless-stopped`) поднимает его заново, недоставленное сообщение передоставляется — и цикл повторяется бесконечно, монополизируя весь пул воркера (было и на `worker-indexer` из-за backoff `scripts/parsers/openalex.py`, и на `worker-gpu` из-за зависшей Ollama на embedding-gpu). Митигации (2026-08-27, сделано для `worker-indexer`): 1. **Бюджет времени прогона** — `PARSER_TIME_BUDGET_S` (1500с) в `worker-indexer/app/config.py`: `index.run_parser` сам останавливается раньше дедлайна и помечает прогон `partial` вместо того, чтобы довести воркер до падения. Остаток дозаливается повторным запуском источника. 2. **`worker_prefetch_multiplier=1`** (`worker-indexer/app/celery_app.py`) — ключевое при массовой заливке. Таймаут отсчитывается от **доставки** сообщения, а не от начала выполнения: с дефолтным префетчем (4×concurrency) сотни поставленных в очередь долгих `run_parser` висят unacked и убивают канал на задачах, которые ещё даже не начинались. 3. Retry-бэкоффы держим заведомо короче 1800с (`MAX_RATE_LIMIT_RETRIES` в `openalex.py`). `worker-gpu` этих защит пока не имеет (там нет своих долгих retry-циклов, но есть зависание внешней Ollama — см. выше). Более глубокий общий фикс — поднять `consumer_timeout` на самом RabbitMQ (доступа для этого пока не заводили).