gendesign/docker-compose.metrics-agent.yml
bot-backend 62a560387c
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 15s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
feat(ops): измеряем очередь Celery и Redis, до сих пор слепая зона
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
2026-09-12 14:16:24 +03:00

339 lines
19 KiB
YAML
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.

# Стек наблюдаемости — АГЕНТСКАЯ сторона. Поднимается на КАЖДОМ хосте: и на Beget
# (рядом с сервером), и на Poincare (рядом с продуктом).
#
# docker compose -p gendesign-metrics-agent -f docker-compose.metrics-agent.yml up -d
#
# Что делает: собирает метрики хоста (node-exporter), контейнеров (cAdvisor), баз
# (postgres-exporter) и логи journald, после чего Alloy отправляет всё на приёмник.
#
# ПОЧЕМУ АГЕНТ, А НЕ СКРЕЙП ИЗ ЦЕНТРА. Prometheus на Beget не может дотянуться до
# экспортеров на Poincare, не открыв там входящие порты — а ufw на Poincare намеренно
# держит только 22/80/443. Агент решает это push'ем: исходящий HTTPS уже разрешён.
# Побочный выигрыш — при обрыве канала Alloy копит в WAL и досылает, тогда как
# pull-скрейп просто теряет точки.
#
# КОНФИГУРАЦИЯ ЗАВИСИТ ОТ ХОСТА — задаётся переменными окружения, файл один:
#
# На Beget (инфраструктура, приёмник рядом):
# METRICS_ROLE=infra
# METRICS_ALLOY_CONFIG=alloy-infra.alloy # пишет напрямую в prometheus:9090 / loki:3100
# COMPOSE_PROFILES=infra
#
# На Poincare (продукт, приёмник за интернетом):
# METRICS_ROLE=apps
# METRICS_ALLOY_CONFIG=alloy-apps.alloy # пишет в https://metrics.gendsgn.ru под basic_auth
# COMPOSE_PROFILES=apps
# METRICS_INGEST_USER / METRICS_INGEST_PASSWORD — учётка приёмника
#
# Профили решают, какие postgres-exporter'ы поднимать: на Poincare две базы
# (Птица + МЕРА), на Beget одна (infra-postgres с forgejo и glitchtip).
x-logging: &default-logging
driver: journald
services:
# ── Alloy: единый агент метрик и логов ───────────────────────────────────────
# Почему Alloy, а не Promtail: у Promtail EOL 2 марта 2026. Alloy читает journald
# нативно и умеет одновременно скрейпить Prometheus-эндпоинты.
alloy:
image: grafana/alloy:v1.6.1
container_name: gendesign-alloy
restart: unless-stopped
# root нужен для чтения /var/log/journal. Штатный пользователь `alloy` требует
# членства в группах adm и systemd-journal — внутри контейнера этих групп с
# правильными gid хоста нет, и агент молча читает НОЛЬ записей. Проверено как
# известные грабли: отказ выглядит как «логов просто нет».
user: root
command:
- "run"
- "--server.http.listen-addr=0.0.0.0:12345"
- "--storage.path=/var/lib/alloy/data"
- "/etc/alloy/config.alloy"
env_file:
- path: ./backend/.env.runtime
required: false
- path: ./backend/.env
required: false
environment:
# Метка хоста попадает во все ряды и логи — без неё графики двух машин
# сливаются в один и разобрать, где что, невозможно.
METRICS_HOST_LABEL: ${METRICS_ROLE:?zadaj METRICS_ROLE=infra ili apps}
METRICS_INGEST_USER: ${METRICS_INGEST_USER:-}
METRICS_INGEST_PASSWORD: ${METRICS_INGEST_PASSWORD:-}
volumes:
- ./ops/metrics/alloy/${METRICS_ALLOY_CONFIG:?zadaj METRICS_ALLOY_CONFIG}:/etc/alloy/config.alloy:ro
# Явный путь к журналу обязателен. Без него libsystemd применяет
# SD_JOURNAL_LOCAL_ONLY и хостовые логи из контейнера не видны — при этом
# ошибки нет, просто пустой поток.
- /var/log/journal:/var/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- alloy_data:/var/lib/alloy/data
expose:
- "12345"
networks:
- shared
mem_limit: 512m
logging: *default-logging
healthcheck:
# В образе grafana/alloy нет wget/curl/nc, только bash — шлём
# сырой HTTP-запрос через /dev/tcp и проверяем код 200 в ответе.
test:
[
"CMD-SHELL",
"bash -c 'exec 3<>/dev/tcp/localhost/12345 && printf \"GET /-/ready HTTP/1.0\r\n\r\n\" >&3 && head -1 <&3 | grep -q 200' || exit 1",
]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
# ── node-exporter: CPU, память, диск, сеть, IO хоста ─────────────────────────
node-exporter:
image: prom/node-exporter:v1.8.2
container_name: gendesign-node-exporter
restart: unless-stopped
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
- "--path.rootfs=/host/root"
# Без исключения оверлеев метрика файловой системы засоряется десятками
# слоёв docker, и «свободное место на диске» перестаёт читаться глазами.
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc|var/lib/docker/.+)($$|/)"
- "--collector.filesystem.fs-types-exclude=^(autofs|binfmt_misc|cgroup2?|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$$"
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/host/root:ro,rslave
expose:
- "9100"
networks:
- shared
mem_limit: 128m
logging: *default-logging
# ── cAdvisor: память и CPU по контейнерам ────────────────────────────────────
# Читает докер-сокет, поэтому видит ВСЕ контейнеры хоста независимо от сетей —
# отдельно подключать его к gendesign_default не нужно.
cadvisor:
# v0.55.1, не v0.52.x: Docker 29 работает через containerd-snapshotter
# (Storage Driver = overlayfs), а 0.52 искал метаданные слоя в легаси
# /var/lib/docker/image/<driver>/layerdb, которого при снапшоттере
# просто нет. Итог был — ни одной метрики по контейнерам.
image: gcr.io/cadvisor/cadvisor:v0.55.1
container_name: gendesign-cadvisor
restart: unless-stopped
privileged: true
devices:
- /dev/kmsg:/dev/kmsg
command:
# Урезаем набор метрик: полный cAdvisor выдаёт тысячи рядов на контейнер и
# раздувает TSDB на порядок ради данных, которые никто не смотрит.
- "--docker_only=true"
- "--housekeeping_interval=30s"
# disk/diskIO добавлены 27.08 по замеру. Именно они держат fsHandler,
# который в логе честно отчитывается: «disk usage and inodes count on
# following dirs took 1.31.8s» — обход overlay-слоёв на каждом цикле.
# Память растёт вместе с числом слоёв, а слои копятся от сборок.
#
# Прод: на продуктовом хосте cadvisor доходил до 359 МиБ из 384 (93 %
# лимита), на инфраструктурном за два часа поднялся со 105 до 291 МиБ.
# До OOM не дошло, но запас был 25 МиБ — то есть вопрос времени.
#
# Теряем метрики использования диска ПО КОНТЕЙНЕРАМ. Проверено, что их
# никто не использует: ни правила Prometheus, ни дашборды. Алерты по
# диску построены на node_exporter (`node_filesystem_*`), то есть на
# уровне файловой системы хоста — а это как раз то измерение, которое
# и нужно, чтобы вовремя увидеть кончающееся место.
- "--disable_metrics=disk,diskIO,percpu,sched,tcp,udp,advtcp,process,hugetlb,referenced_memory,cpu_topology,resctrl"
- "--store_container_labels=false"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
expose:
- "8080"
networks:
- shared
mem_limit: 384m
logging: *default-logging
# ── postgres-exporter: Птица (только на Poincare) ────────────────────────────
# WAL/сутки, n_tup_upd против n_tup_hot_upd, рост TOAST, коннекты, горизонт
# vacuum — то, из-за отсутствия чего раздутие копилось 91 день незамеченным.
# v0.18.0, а не v0.16.0: та печатала ПОЛНЫЙ DSN вместе с паролем в каждую
# ошибку сбора (#3114). Строка вида
# msg="error scraping dsn" err="queryNamespaceMappings returned 1 errors"
# dsn="postgresql://<роль>:<ПАРОЛЬ>@<хост>:5432/<база>?sslmode=disable"
# уходила в Loki при каждом скрейпе — около 210 строк в сутки с двух хостов,
# при ретенции 30 дней это тысячи паролей в хранилище логов.
#
# Воспроизведено на скретч-контейнерах 27.08 (постгрес + роль без прав +
# этот же queries.yml): v0.16.0 печатает пароль, v0.18.0 — нет.
#
# NB: форма DATA_SOURCE_URI + DATA_SOURCE_USER + DATA_SOURCE_PASS, которую
# предлагали как обходной путь, НЕ помогает — проверено тем же опытом:
# экспортер собирает DSN внутри и печатает его целиком точно так же.
# Утечку закрывает только версия образа.
#
# Паритет метрик проверен на том же стенде: все пять пользовательских
# запросов из queries.yml отдаются обеими версиями одинаково
# (PG_EXPORTER_EXTEND_QUERY_PATH в v0.18 работает), v0.18 добавляет три
# встроенные метрики и не теряет ни одной.
postgres-exporter-gendesign:
image: quay.io/prometheuscommunity/postgres-exporter:v0.18.0
container_name: gendesign-pg-exporter-gendesign
restart: unless-stopped
profiles: ["apps"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
# `:-`, а НЕ `:?`. Compose интерполирует ВЕСЬ файл до фильтрации по
# профилям, поэтому `:?` роняет команду из-за переменной сервиса, который
# на этом хосте вообще не поднимается: продуктовый агент падал на
# INFRA_EXPORTER_DSN (профиль infra), инфраструктурный упал бы на двух
# продуктовых. Громкость не потеряна — перенесена в deploy-metrics.yml,
# где роль ИЗВЕСТНА и проверяется ровно тот DSN, который нужен здесь.
DATA_SOURCE_NAME: ${GENDESIGN_EXPORTER_DSN:-}
PG_EXPORTER_EXTEND_QUERY_PATH: /etc/pg-queries.yml
volumes:
- ./ops/metrics/postgres/queries.yml:/etc/pg-queries.yml:ro
expose:
- "9187"
networks:
- shared
- product
mem_limit: 128m
logging: *default-logging
# ── postgres-exporter: МЕРА (только на Poincare) ─────────────────────────────
postgres-exporter-tradein:
image: quay.io/prometheuscommunity/postgres-exporter:v0.18.0
container_name: gendesign-pg-exporter-tradein
restart: unless-stopped
profiles: ["apps"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
# `:-`, а не `:?` — см. пояснение у экспортера Птицы выше.
DATA_SOURCE_NAME: ${TRADEIN_EXPORTER_DSN:-}
PG_EXPORTER_EXTEND_QUERY_PATH: /etc/pg-queries.yml
volumes:
- ./ops/metrics/postgres/queries.yml:/etc/pg-queries.yml:ro
expose:
- "9187"
networks:
- shared
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 нет вообще.
postgres-exporter-infra:
image: quay.io/prometheuscommunity/postgres-exporter:v0.18.0
container_name: gendesign-pg-exporter-infra
restart: unless-stopped
profiles: ["infra"]
env_file:
- path: ./backend/.env.runtime
required: false
environment:
# `:-`, а не `:?` — см. пояснение у экспортера Птицы выше.
DATA_SOURCE_NAME: ${INFRA_EXPORTER_DSN:-}
PG_EXPORTER_EXTEND_QUERY_PATH: /etc/pg-queries.yml
volumes:
- ./ops/metrics/postgres/queries.yml:/etc/pg-queries.yml:ro
expose:
- "9187"
networks:
- shared
- infra
mem_limit: 128m
logging: *default-logging
volumes:
alloy_data:
networks:
shared:
external: true
name: gendesign_shared
# Сеть продуктового стека Птицы — в ней gendesign-postgres-1 на Poincare.
product:
external: true
name: gendesign_default
# На Beget это та же по имени сеть, но с инфраструктурными контейнерами.
# Разные алиасы в файле нужны, чтобы профили не тянули лишнюю сеть на чужом хосте.
infra:
external: true
name: gendesign_default