Files
anti-plagiarism/docs/DR-HA.md
jze9 99bf14fe6a feat(admin): честная длительность прогона + добор эмбеддингов
Разбор итогов массовой заливки показал в отладке «среднюю длительность прогона»
в 2.8 часа там, где парсинг занимал 50 секунд: started_at пишется в момент
постановки в очередь, а очередь из 173 источников разбирается часами. Теперь
момент реального старта пишется отдельно (run_started_at, миграция 006), а
схема отдаёт обе величины — сколько ждал очереди и сколько работал.

Плюс scripts/ops/reembed_missing.py: документ попадает в корпус сразу, а вектор
для L3 считает отдельная задача; когда worker-gpu или Ollama недоступны, эти
задачи теряются и документ остаётся невидимым для семантического поиска. Скрипт
находит faiss_id IS NULL и переотправляет пачками (dry-run по умолчанию) —
сейчас таких 23 101 из 177 147.

Документация: актуальные цифры корпуса, дубли при повторном прогоне, лимит
OpenAlex, и главное — гипервизор .254 зафиксирован в DR-HA как самая широкая
единая точка отказа (брокер, эмбеддинги, секреты и прокси на одном железе;
подтверждено аварией 28.08).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:51:25 +05:00

99 lines
7.6 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 не критично) |
| Хост Proxmox .254 | **да, широкий** | митигации нет — см. ниже |
**Гипервизор — самая широкая единая точка отказа.** На хосте `192.168.20.254`
одновременно живут RabbitMQ (.82), embedding-gpu (.109), Infisical (.111) и
CT 102 (.253) — фронтенд и реверс-прокси. Его падение снимает сразу: приём и
обработку задач (нет брокера), эмбеддинги (нет Ollama), деплой (нет Infisical и
Gitea) и весь публичный доступ (нет прокси) — при том что API, PostgreSQL и
MinIO продолжают работать. Проверено на практике 2026-08-28: хост перестал
отвечать даже на ARP, всё перечисленное отвалилось разом, данные не пострадали.
Разнести хотя бы прокси/брокер по разным физическим хостам — самая дешёвая
мера; пока её нет, восстановление требует физического доступа к железу.
## 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 (доступа для этого пока не заводили).