Files
anti-plagiarism/docs/DR-HA.md
jze9 78e27806b9
All checks were successful
Deploy / test (push) Successful in 3m56s
Deploy / deploy (push) Successful in 31s
feat(admin): шкала загрузки источников, отладка и загрузка работ в корпус
Заливка корпуса была чёрным ящиком: у источника только last_status
(idle/running/done/error), без «сколько из скольки», без причины падения и без
способа остановить начатое. Теперь каждый запуск создаёт строку parse_runs,
куда воркер раз в ~2с пишет стадию, счётчики и журнал событий.

Админка:
- шкала загрузки у каждого источника (0→50% выборка, 50→100% индексация),
  раскрытая строка — журнал прогона по шагам с таймингами;
- «Запустить всё» / «Остановить всё» и остановка по одному источнику
  (кооперативная отмена: воркер останавливается сам, не рвя запись в базу);
- пакетное добавление источников (тип + список тем), тип pmc в форме;
- загрузка PDF/DOCX/TXT прямо в базу сравнения (index.ingest_upload);
- страница «Отладка»: воркеры Celery и их текущие таски, очереди RabbitMQ,
  покрытие корпуса эмбеддингами, зависшие и упавшие прогоны, конфиг бэкендов.

Защита от краш-лупа по consumer_timeout RabbitMQ (docs/DR-HA.md §6), без неё
массовый запуск 170+ источников гарантированно ронял воркер:
- PARSER_TIME_BUDGET_S (1500с) — прогон закругляется сам и помечается partial;
- worker_prefetch_multiplier=1 — таймаут считается от ДОСТАВКИ сообщения, и с
  дефолтным префетчем очередь долгих run_parser убивала канал на задачах,
  которые ещё не начинались.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:40:23 +05:00

88 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Отказоустойчивость и восстановление (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 (доступа для этого пока не заводили).