From 62a560387c9e6d8189349967e168f5177bd5e2cb Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 12 Sep 2026 14:16:24 +0300 Subject: [PATCH] =?UTF-8?q?feat(ops):=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4=D1=8C?= =?UTF-8?q?=20Celery=20=D0=B8=20Redis,=20=D0=B4=D0=BE=20=D1=81=D0=B8=D1=85?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=80=20=D1=81=D0=BB=D0=B5=D0=BF=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prometheus не видел ни одной серии celery_*/redis_* — переполнение очереди Site Finder и залипший воркер снаружи выглядели одинаково, тишиной (issue #3471). Добавлено (только на продуктовом хосте, профиль apps): - redis-exporter (oliver006/redis_exporter) — здоровье общего Redis (db0 celery-брокер Site Finder, db1 SearchCache trade-in, db2 glitchtip), адрес через alias gendesign-redis на сети shared, без нового сетевого доступа. - celery-exporter (danihodovic/celery-exporter) — глубина очереди, число живых воркеров, счётчик неуспешных задач. Выбран вместо redis-exporter --check-keys, потому что дефолтная очередь "celery" дала бы только глубину, но не воркеров и не failures. - Скрейп обоих в alloy-apps.alloy. - Алерты в infra.yml: RedisDown, NoActiveCeleryWorkers, CeleryQueueGrowing (порог 150 предварительный — реальных данных по глубине очереди ещё нет, пересмотр через неделю наблюдений). Имена метрик celery-exporter (celery_queue_length, celery_worker_up, celery_task_failed_total) — по документации проекта, без прогона на реальном брокере; сверить после первого деплоя, см. комментарий у сервиса. promtool check rules — 23 правила, SUCCESS. Refs #3471 --- docker-compose.metrics-agent.yml | 65 ++++++++++++++++++++++++++ ops/metrics/alloy/alloy-apps.alloy | 21 +++++++++ ops/metrics/prometheus/rules/infra.yml | 57 ++++++++++++++++++++++ 3 files changed, 143 insertions(+) diff --git a/docker-compose.metrics-agent.yml b/docker-compose.metrics-agent.yml index 506be509..7755e2cd 100644 --- a/docker-compose.metrics-agent.yml +++ b/docker-compose.metrics-agent.yml @@ -231,6 +231,71 @@ services: mem_limit: 128m logging: *default-logging + # ── redis-exporter: здоровье общего Redis (только на Poincare) ─────────────── + # #3471. Redis — один инстанс на три потребителя: db0 celery-брокер Site + # Finder, db1 SearchCache trade-in, db2 glitchtip (см. комментарий у сервиса + # `redis` в docker-compose.prod.yml). Один `redis_up` покрывает риск для всех + # трёх разом — до этой правки Redis не измерялся вообще, переполнение + # брокера и обычная недоступность снаружи выглядели одинаково — тишиной. + # + # Адрес — через alias `gendesign-redis`, который `redis` регистрирует на + # сети `shared` (см. #2709 в docker-compose.prod.yml) — джойнить ещё и + # `product` не нужно, тем же путём уже идёт postgres-exporter-tradein. + # + # Пароль — из окружения, не хардкод: сегодня на Redis нет requirepass (нет + # переменной ни в docker-compose.prod.yml, ни здесь), но если он появится, + # значение подставляется через METRICS_REDIS_PASSWORD в /opt/gendesign/.env + # на хосте, а не в этот файл. + redis-exporter: + image: oliver006/redis_exporter:v1.65.0 + container_name: gendesign-redis-exporter + restart: unless-stopped + profiles: ["apps"] + environment: + REDIS_ADDR: ${METRICS_REDIS_ADDR:-redis://gendesign-redis:6379} + REDIS_PASSWORD: ${METRICS_REDIS_PASSWORD:-} + expose: + - "9121" + networks: + - shared + mem_limit: 64m + logging: *default-logging + + # ── celery-exporter: очередь Site Finder (только на Poincare) ──────────────── + # #3471. Слепая зона: глубина очереди, число живых воркеров и счётчик + # неуспешных задач нигде не измерялись — залипший воркер и переполненная + # очередь снаружи неотличимы от тишины. + # + # ПОЧЕМУ ОТДЕЛЬНЫЙ ОБРАЗ, А НЕ redis-exporter --check-keys. check-keys дал + # бы LLEN дефолтной очереди "celery" (в backend/app/workers/celery_app.py + # НЕТ task_routes — все таски идут в один дефолтный queue, имя буквально + # "celery") без нового образа вообще. Но он НЕ умеет считать живых + # воркеров и неуспешные таски — то есть закрыл бы только треть минимума + # из задачи. celery-exporter слушает событийную шину Celery через тот же + # брокер и даёт все три метрики разом, поэтому выбран он, а не комбинация + # check-keys + что-то ещё для остальных двух чисел. + # + # ⚠️ ИМЕНА МЕТРИК НИЖЕ (celery_queue_length, celery_worker_up, + # celery_task_failed_total) — по документации проекта на момент правки, БЕЗ + # прогона на реальном брокере (агент писал этот файл без доступа к проду). + # Сверить с `curl http://gendesign-celery-exporter:9808/metrics` на хосте + # после первого деплоя и поправить `ops/metrics/prometheus/rules/infra.yml` + # при расхождении — иначе алерты будут молча ничего не ловить. + celery-exporter: + image: danihodovic/celery-exporter:0.13.0 + container_name: gendesign-celery-exporter + restart: unless-stopped + profiles: ["apps"] + command: + - "--broker-url=${METRICS_CELERY_BROKER_URL:-redis://gendesign-redis:6379/0}" + - "--queue=celery" + expose: + - "9808" + networks: + - shared + mem_limit: 128m + logging: *default-logging + # ── postgres-exporter: инфраструктурная БД (только на Beget) ───────────────── # forgejo + glitchtip. Нужен и сам по себе, и как страховка: рост базы glitchtip # ничем не ограничен — политики ретенции у GlitchTip нет вообще. diff --git a/ops/metrics/alloy/alloy-apps.alloy b/ops/metrics/alloy/alloy-apps.alloy index 709d621f..5b66559a 100644 --- a/ops/metrics/alloy/alloy-apps.alloy +++ b/ops/metrics/alloy/alloy-apps.alloy @@ -119,6 +119,27 @@ prometheus.scrape "postgres" { scrape_interval = "60s" } +// ═══ REDIS И ОЧЕРЕДЬ CELERY (#3471) ═════════════════════════════════════════════ +// До этой правки ни одной серии redis_* / celery_* в Prometheus не было: глубина +// очереди, число живых воркеров и потеря соединения с брокером были невидимы — +// переполнение очереди и залипший воркер снаружи выглядели одинаково, тишиной. + +prometheus.scrape "redis" { + targets = [ + { __address__ = "gendesign-redis-exporter:9121", job = "redis" }, + ] + forward_to = [prometheus.remote_write.central.receiver] + scrape_interval = "30s" +} + +prometheus.scrape "celery" { + targets = [ + { __address__ = "gendesign-celery-exporter:9808", job = "celery" }, + ] + forward_to = [prometheus.remote_write.central.receiver] + scrape_interval = "30s" +} + // ═══ МЕТРИКИ ПРИЛОЖЕНИЙ ════════════════════════════════════════════════════════ // Эндпоинты появляются в части 3. До этого скрейп просто отдаёт `up 0` — и это // правильно: цель видна как недоступная, а не отсутствует молча. diff --git a/ops/metrics/prometheus/rules/infra.yml b/ops/metrics/prometheus/rules/infra.yml index ee906111..c064cd4a 100644 --- a/ops/metrics/prometheus/rules/infra.yml +++ b/ops/metrics/prometheus/rules/infra.yml @@ -232,6 +232,63 @@ groups: summary: "p95 задержки ответа выше 5 секунд" description: "{{ $labels.app }}: p95 за 10 минут — {{ $value | humanizeDuration }}." + # ── Redis и очередь Celery (#3471) ─────────────────────────────────────────── + # Слепая зона: до этих правил ни redis_*, ни celery_* не собирались вовсе. + # Redis — общий инстанс на три потребителя (celery-брокер Site Finder, кэш + # trade-in, glitchtip — см. docker-compose.prod.yml), поэтому его смерть + # клиентская, отсюда severity: critical без явного host: apps — серия + # приходит только с продуктового alloy (alloy-apps.alloy), host в неё + # проставляется через external_labels уже на месте. + # + # ⚠️ Имена метрик celery_queue_length / celery_worker_up / + # celery_task_failed_total — по документации celery-exporter на момент + # написания правил, без проверки на реальном брокере (см. комментарий у + # сервиса celery-exporter в docker-compose.metrics-agent.yml). Сверить после + # первого деплоя. + - name: redis-celery + interval: 60s + rules: + - alert: RedisDown + expr: up{job="redis"} == 0 or redis_up == 0 + for: 5m + labels: + severity: critical + annotations: + summary: "Redis недоступен" + description: "redis_exporter не может достучаться до Redis (или сам процесс лёг). Разом теряют связь celery-брокер Site Finder, SearchCache trade-in и glitchtip." + + # `absent()` — как у CadvisorDown: если сам celery-exporter не поднялся, + # серии celery_worker_up не будет вообще, а не будет со значением 0. + - alert: NoActiveCeleryWorkers + expr: count(celery_worker_up == 1) == 0 or absent(celery_worker_up) + for: 5m + labels: + severity: critical + annotations: + summary: "Ни одного живого воркера Celery" + description: "celery-exporter не видит ни одного heartbeat от воркера Site Finder. Все periodic-таски (парсинг, аналитика, синк слоёв) встали." + + # Порог 150 ПРЕДВАРИТЕЛЬНЫЙ: реальных данных по глубине очереди нет (до + # этой правки метрика не собиралась). beat_schedule.py на момент правки + # содержит 44 periodic-задачи с разным временем срабатывания — даже + # маловероятный залп всех разом даёт кратно меньше 150. Порог взят с + # запасом сознательно и требует пересмотра через неделю наблюдений по + # факту `celery_queue_length`. + # + # `delta(...) >= 0` — очередь не УМЕНЬШАЕТСЯ за 15 минут (тот же приём, + # что и "растёт и не разгребается" в тексте задачи): просто высокое + # значение без этого условия поймало бы и здоровый кратковременный всплеск. + - alert: CeleryQueueGrowing + expr: | + celery_queue_length{queue_name="celery"} > 150 + and delta(celery_queue_length{queue_name="celery"}[15m]) >= 0 + for: 15m + labels: + severity: warning + annotations: + summary: "Очередь Celery растёт и не разгребается" + description: "В очереди {{ $value }} задач, за 15 минут меньше не стало. Похоже на залипший воркер или устойчивый рост нагрузки." + # ── Postgres ──────────────────────────────────────────────────────────────── - name: postgres interval: 60s -- 2.45.3