Разбор итогов массовой заливки показал в отладке «среднюю длительность прогона» в 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>
99 lines
7.6 KiB
Markdown
99 lines
7.6 KiB
Markdown
# Отказоустойчивость и восстановление (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 (доступа для этого пока не заводили).
|