Заливка корпуса была чёрным ящиком: у источника только 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>
6.4 KiB
Отказоустойчивость и восстановление (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 берёт
последний дамп из MinIO, restore в эфемерный postgres:16 и проверяет ключевые
таблицы. Прод не трогает.
bash scripts/ops/pg_restore_verify.sh # креды из .env
Рекомендация: cron раз в неделю на 1.32, алерт (как в antiplag_monitor) при ненулевом коде.
3. HA PostgreSQL — потоковая репликация (требует новый LXC)
Провижн реплики — задача на Proxmox (нужен ещё один LXC, напр. 1.39). Шаги:
- Primary (1.38)
postgresql.conf:wal_level=replica,max_wal_senders=5,wal_keep_size=1GB;pg_hba.conf: строкаreplicationдля IP реплики. - Replica:
pg_basebackup -h 1.38 -U replicator -D $PGDATA -R(создаётstandby.signal+primary_conninfo), затем старт — догоняет primary по WAL. - Failover: ручной
pg_ctl promoteна реплике + переключениеPOSTGRES_HOSTв Infisical (окружениеprod— не в.envна сервере напрямую, его перезапишет следующий деплой, см. ARCHITECTURE.md §10) и передеплой/рестарт сервисов (или автоматизация через Patroni + etcd — если нужен авто-failover). - Проверка лага:
SELECT * FROM pg_stat_replicationна primary.
4. HA Redis — реплика + Sentinel (требует новый LXC)
Redis у нас — кэш/rate-limits/LSH-индекс (префикс antiplag_lsh). Потеря = деградация,
не потеря данных задач (они в PG). Если нужна устойчивость:
- Второй Redis (реплика):
replicaof 1.35 6379+ тот жеrequirepass. - 3× Sentinel (на app-хостах):
sentinel monitor antiplag 1.35 6379 2, авто-переключение мастера. - Клиенты (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):
- Бюджет времени прогона —
PARSER_TIME_BUDGET_S(1500с) вworker-indexer/app/config.py:index.run_parserсам останавливается раньше дедлайна и помечает прогонpartialвместо того, чтобы довести воркер до падения. Остаток дозаливается повторным запуском источника. worker_prefetch_multiplier=1(worker-indexer/app/celery_app.py) — ключевое при массовой заливке. Таймаут отсчитывается от доставки сообщения, а не от начала выполнения: с дефолтным префетчем (4×concurrency) сотни поставленных в очередь долгихrun_parserвисят unacked и убивают канал на задачах, которые ещё даже не начинались.- Retry-бэкоффы держим заведомо короче 1800с (
MAX_RATE_LIMIT_RETRIESвopenalex.py).
worker-gpu этих защит пока не имеет (там нет своих долгих retry-циклов, но
есть зависание внешней Ollama — см. выше). Более глубокий общий фикс — поднять
consumer_timeout на самом RabbitMQ (доступа для этого пока не заводили).