Метрики: экспортёр Celery наконец поднимается, тревоги про воркеров и память перестают врать #3547
4 changed files with 190 additions and 20 deletions
|
|
@ -433,8 +433,18 @@ jobs:
|
||||||
#
|
#
|
||||||
# promtool проверяет ОБА файла ДО reload: битый конфиг не должен
|
# promtool проверяет ОБА файла ДО reload: битый конфиг не должен
|
||||||
# положить работающий Prometheus молчаливым откатом на дефолты.
|
# положить работающий Prometheus молчаливым откатом на дефолты.
|
||||||
|
#
|
||||||
|
# Плюс юнит-тесты правил (#3493): синтаксически верное правило может
|
||||||
|
# врать по смыслу — `count(x == 1) == 0` от пустого вектора не
|
||||||
|
# срабатывает никогда, а под текстом «воркер мёртв» горел не поднятый
|
||||||
|
# экспортёр. Каталог tests/ в контейнер не смонтирован, поэтому
|
||||||
|
# одноразовый контейнер ТОГО ЖЕ образа поверх файлов с диска.
|
||||||
if docker exec gendesign-prometheus promtool check config /etc/prometheus/prometheus.yml \
|
if docker exec gendesign-prometheus promtool check config /etc/prometheus/prometheus.yml \
|
||||||
&& docker exec gendesign-prometheus sh -c 'promtool check rules /etc/prometheus/rules/*.yml'; then
|
&& docker exec gendesign-prometheus sh -c 'promtool check rules /etc/prometheus/rules/*.yml' \
|
||||||
|
&& docker run --rm --entrypoint promtool \
|
||||||
|
-v /opt/gendesign/ops/metrics/prometheus:/work:ro \
|
||||||
|
"$(docker inspect -f '{{.Config.Image}}' gendesign-prometheus)" \
|
||||||
|
test rules /work/tests/infra_test.yml; then
|
||||||
LAST_CONFIG_BEFORE="$(docker exec gendesign-prometheus wget -qO- http://localhost:9090/api/v1/status/runtimeinfo | grep -oE '"lastConfigTime":"[^"]*"')"
|
LAST_CONFIG_BEFORE="$(docker exec gendesign-prometheus wget -qO- http://localhost:9090/api/v1/status/runtimeinfo | grep -oE '"lastConfigTime":"[^"]*"')"
|
||||||
|
|
||||||
docker exec gendesign-prometheus wget -q -O /dev/null --post-data='' http://localhost:9090/-/reload
|
docker exec gendesign-prometheus wget -q -O /dev/null --post-data='' http://localhost:9090/-/reload
|
||||||
|
|
@ -456,7 +466,7 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "Prometheus: конфиг и правила проверены, reload подтверждён ($LAST_CONFIG_BEFORE -> $LAST_CONFIG_AFTER)."
|
echo "Prometheus: конфиг и правила проверены, reload подтверждён ($LAST_CONFIG_BEFORE -> $LAST_CONFIG_AFTER)."
|
||||||
else
|
else
|
||||||
echo "ОШИБКА: конфиг/правила Prometheus не проходят promtool — reload НЕ выполнен, работающий Prometheus остаётся на прежнем конфиге."
|
echo "ОШИБКА: конфиг/правила Prometheus не проходят promtool (проверка конфига, правил или их юнит-тестов — смотри вывод выше) — reload НЕ выполнен, работающий Prometheus остаётся на прежнем конфиге."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -275,20 +275,26 @@ services:
|
||||||
# брокер и даёт все три метрики разом, поэтому выбран он, а не комбинация
|
# брокер и даёт все три метрики разом, поэтому выбран он, а не комбинация
|
||||||
# check-keys + что-то ещё для остальных двух чисел.
|
# check-keys + что-то ещё для остальных двух чисел.
|
||||||
#
|
#
|
||||||
# ⚠️ ИМЕНА МЕТРИК НИЖЕ (celery_queue_length, celery_worker_up,
|
# ТЕГ И ФЛАГИ СВЕРЕНЫ С ИСХОДНИКОМ ОБРАЗА, а не с памятью (#3493). Прежняя
|
||||||
# celery_task_failed_total) — по документации проекта на момент правки, БЕЗ
|
# запись `0.13.0` + `--queue` не существовала вовсе: такого тега нет на Docker
|
||||||
# прогона на реальном брокере (агент писал этот файл без доступа к проду).
|
# Hub (`pull` → `not found`), а в `src/cli.py` версии 0.12.2 опция зовётся
|
||||||
# Сверить с `curl http://gendesign-celery-exporter:9808/metrics` на хосте
|
# `--queues` (список через запятую), `--queue` click отвергает при старте.
|
||||||
# после первого деплоя и поправить `ops/metrics/prometheus/rules/infra.yml`
|
# Под `set -e` упавший `pull` обрывал ВСЮ джобу `agent-apps` деплоя метрик:
|
||||||
# при расхождении — иначе алерты будут молча ничего не ловить.
|
# с 12.09 до этой правки на Poincare не доезжало ничего из агента — ни этот
|
||||||
|
# экспортер, ни redis-exporter, ни новый конфиг Alloy, а NoActiveCeleryWorkers
|
||||||
|
# горел по ветке absent() при живом воркере.
|
||||||
|
#
|
||||||
|
# Имена метрик 0.12.2 (src/exporter.py, metric_prefix="celery_"):
|
||||||
|
# celery_worker_up{hostname}, celery_queue_length{queue_name},
|
||||||
|
# celery_task_failed_total — совпадают с ops/metrics/prometheus/rules/infra.yml.
|
||||||
celery-exporter:
|
celery-exporter:
|
||||||
image: danihodovic/celery-exporter:0.13.0
|
image: danihodovic/celery-exporter:0.12.2
|
||||||
container_name: gendesign-celery-exporter
|
container_name: gendesign-celery-exporter
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles: ["apps"]
|
profiles: ["apps"]
|
||||||
command:
|
command:
|
||||||
- "--broker-url=${METRICS_CELERY_BROKER_URL:-redis://gendesign-redis:6379/0}"
|
- "--broker-url=${METRICS_CELERY_BROKER_URL:-redis://gendesign-redis:6379/0}"
|
||||||
- "--queue=celery"
|
- "--queues=celery"
|
||||||
expose:
|
expose:
|
||||||
- "9808"
|
- "9808"
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -149,16 +149,30 @@ groups:
|
||||||
# при этом срабатывало верно, врал только текст. Поэтому отношение стоит
|
# при этом срабатывало верно, врал только текст. Поэтому отношение стоит
|
||||||
# СЛЕВА, а отсев нулевого лимита убран внутрь знаменателя: `(X > 0)`
|
# СЛЕВА, а отсев нулевого лимита убран внутрь знаменателя: `(X > 0)`
|
||||||
# выбрасывает серии без лимита ДО деления.
|
# выбрасывает серии без лимита ДО деления.
|
||||||
|
#
|
||||||
|
# ВТОРОЕ УСЛОВИЕ — АНОНИМНАЯ ПАМЯТЬ (#3493). working_set = usage − inactive_file,
|
||||||
|
# то есть в нём остаётся АКТИВНЫЙ кэш страниц, а его ядро вытесняет само и
|
||||||
|
# OOM из-за него не наступает. У контейнера с базой ночные сканы поднимают
|
||||||
|
# активный кэш к потолку каждую ночь: tradein-postgres за 14 суток дал 5
|
||||||
|
# эпизодов (все 01:09–03:44 UTC) при анонимной памяти не выше 11.4 % лимита и
|
||||||
|
# oom_kill = 0 — текст «дальше OOM-kill» был ложью. Калибровка этим самым
|
||||||
|
# выражением по истории 14 суток: tradein-postgres 5 → 0, tradein-browser
|
||||||
|
# 2 → 2 (06.09 и 12.09, rss до 95 % — настоящие), остальные 0 → 0.
|
||||||
|
# `and` без `on()`: у working_set и rss один и тот же набор меток cAdvisor,
|
||||||
|
# совпадение проверено на проде (913 из 915 серий); слева по-прежнему доля.
|
||||||
- alert: ContainerNearMemoryLimit
|
- alert: ContainerNearMemoryLimit
|
||||||
expr: |
|
expr: |
|
||||||
container_memory_working_set_bytes{name!=""}
|
container_memory_working_set_bytes{name!=""}
|
||||||
/ (container_spec_memory_limit_bytes{name!=""} > 0) > 0.90
|
/ (container_spec_memory_limit_bytes{name!=""} > 0) > 0.90
|
||||||
|
and
|
||||||
|
(container_memory_rss{name!=""}
|
||||||
|
/ (container_spec_memory_limit_bytes{name!=""} > 0) > 0.50)
|
||||||
for: 15m
|
for: 15m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Контейнер у своего потолка памяти"
|
summary: "Контейнер у своего потолка памяти"
|
||||||
description: "{{ $labels.host }} / {{ $labels.name }}: {{ $value | humanizePercentage }} от mem_limit. Дальше OOM-kill."
|
description: "{{ $labels.host }} / {{ $labels.name }}: {{ $value | humanizePercentage }} от mem_limit, и больше половины лимита — анонимная память процессов, которую ядро не вытеснит как кэш. Дальше OOM-kill."
|
||||||
|
|
||||||
# tradein-tgbot и tradein-scraper не HTTP-сервисы — у них нет `up{}`
|
# tradein-tgbot и tradein-scraper не HTTP-сервисы — у них нет `up{}`
|
||||||
# вообще, поэтому крэш-без-рестарта или удаление контейнера иначе не
|
# вообще, поэтому крэш-без-рестарта или удаление контейнера иначе не
|
||||||
|
|
@ -240,11 +254,9 @@ groups:
|
||||||
# приходит только с продуктового alloy (alloy-apps.alloy), host в неё
|
# приходит только с продуктового alloy (alloy-apps.alloy), host в неё
|
||||||
# проставляется через external_labels уже на месте.
|
# проставляется через external_labels уже на месте.
|
||||||
#
|
#
|
||||||
# ⚠️ Имена метрик celery_queue_length / celery_worker_up /
|
# Имена метрик celery_queue_length / celery_worker_up / celery_task_failed_total
|
||||||
# celery_task_failed_total — по документации celery-exporter на момент
|
# сверены с исходником celery-exporter 0.12.2 (src/exporter.py) — см.
|
||||||
# написания правил, без проверки на реальном брокере (см. комментарий у
|
# комментарий у сервиса в docker-compose.metrics-agent.yml (#3493).
|
||||||
# сервиса celery-exporter в docker-compose.metrics-agent.yml). Сверить после
|
|
||||||
# первого деплоя.
|
|
||||||
- name: redis-celery
|
- name: redis-celery
|
||||||
interval: 60s
|
interval: 60s
|
||||||
rules:
|
rules:
|
||||||
|
|
@ -257,16 +269,41 @@ groups:
|
||||||
summary: "Redis недоступен"
|
summary: "Redis недоступен"
|
||||||
description: "redis_exporter не может достучаться до Redis (или сам процесс лёг). Разом теряют связь celery-брокер Site Finder, SearchCache trade-in и glitchtip."
|
description: "redis_exporter не может достучаться до Redis (или сам процесс лёг). Разом теряют связь celery-брокер Site Finder, SearchCache trade-in и glitchtip."
|
||||||
|
|
||||||
# `absent()` — как у CadvisorDown: если сам celery-exporter не поднялся,
|
# ДВЕ РАЗНЫЕ НОВОСТИ — ДВА ПРАВИЛА (#3493). Прежнее
|
||||||
# серии celery_worker_up не будет вообще, а не будет со значением 0.
|
# `count(celery_worker_up == 1) == 0 or absent(celery_worker_up)` смешивало
|
||||||
|
# «воркер мёртв» с «экспортёр не поднят» под текстом первой: с 12.09 по 17.09
|
||||||
|
# оно горело без перерыва по ветке absent() при живом воркере (экспортёр не
|
||||||
|
# стартовал), повторяясь каждые ~3 часа. А первая ветка не сработала бы
|
||||||
|
# НИКОГДА: `count()` от пустого вектора возвращает пустой вектор, а не 0,
|
||||||
|
# поэтому `== 0` сравнивать не с чем — мёртвый воркер она бы пропустила.
|
||||||
|
#
|
||||||
|
# Здесь — только когда экспортёр ЖИВ (`up == 1`), но живых воркеров нет:
|
||||||
|
# либо все серии в 0, либо серий нет (экспортёр вычищает отвалившихся).
|
||||||
|
# Отсутствие самого экспортёра — QueueExporterDown ниже, со своим текстом.
|
||||||
- alert: NoActiveCeleryWorkers
|
- alert: NoActiveCeleryWorkers
|
||||||
expr: count(celery_worker_up == 1) == 0 or absent(celery_worker_up)
|
expr: |
|
||||||
|
(sum(celery_worker_up) == 0 or absent(celery_worker_up))
|
||||||
|
and on() (up{job="celery"} == 1)
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Ни одного живого воркера Celery"
|
summary: "Ни одного живого воркера Celery"
|
||||||
description: "celery-exporter не видит ни одного heartbeat от воркера Site Finder. Все periodic-таски (парсинг, аналитика, синк слоёв) встали."
|
description: "celery-exporter работает, но не видит ни одного heartbeat от воркера Site Finder. Все periodic-таски (парсинг, аналитика, синк слоёв) встали."
|
||||||
|
|
||||||
|
# Экспортёр очереди не отдаёт метрики. `absent(up{job="X"})` переносит
|
||||||
|
# job в метки результата, поэтому текст знает, какой из двух.
|
||||||
|
- alert: QueueExporterDown
|
||||||
|
expr: |
|
||||||
|
up{job=~"redis|celery"} == 0
|
||||||
|
or absent(up{job="redis"})
|
||||||
|
or absent(up{job="celery"})
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Метрики очереди не собираются"
|
||||||
|
description: "{{ $labels.job }}-экспортёр на Poincare не отдаёт метрики (up=0 или серии нет вовсе). Это авария наблюдаемости, а не продукта: Redis и воркеры могут быть живы — проверь `docker ps`. Пока она горит, RedisDown и NoActiveCeleryWorkers молчат по построению."
|
||||||
|
|
||||||
# Порог 150 ПРЕДВАРИТЕЛЬНЫЙ: реальных данных по глубине очереди нет (до
|
# Порог 150 ПРЕДВАРИТЕЛЬНЫЙ: реальных данных по глубине очереди нет (до
|
||||||
# этой правки метрика не собиралась). beat_schedule.py на момент правки
|
# этой правки метрика не собиралась). beat_schedule.py на момент правки
|
||||||
|
|
|
||||||
117
ops/metrics/prometheus/tests/infra_test.yml
Normal file
117
ops/metrics/prometheus/tests/infra_test.yml
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Юнит-тесты правил `promtool test rules` (#3493). Исполняются деплоем метрик
|
||||||
|
# перед reload Prometheus (.forgejo/workflows/deploy-metrics.yml): упавший тест
|
||||||
|
# оставляет работающий Prometheus на прежних правилах.
|
||||||
|
#
|
||||||
|
# Каждый случай — ровно тот, что уже случился на проде, а не придуманный.
|
||||||
|
rule_files:
|
||||||
|
- ../rules/infra.yml
|
||||||
|
|
||||||
|
evaluation_interval: 1m
|
||||||
|
|
||||||
|
tests:
|
||||||
|
# tradein-postgres, ночь 17.09: рабочий набор 98 % от лимита, но почти весь —
|
||||||
|
# кэш страниц; анонимная память ~1 %. OOM не грозит — тревоги быть не должно.
|
||||||
|
- interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: 'container_memory_working_set_bytes{host="apps",name="tradein-postgres"}'
|
||||||
|
values: '2940x40'
|
||||||
|
- series: 'container_memory_rss{host="apps",name="tradein-postgres"}'
|
||||||
|
values: '40x40'
|
||||||
|
- series: 'container_spec_memory_limit_bytes{host="apps",name="tradein-postgres"}'
|
||||||
|
values: '3000x40'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: ContainerNearMemoryLimit
|
||||||
|
exp_alerts: []
|
||||||
|
|
||||||
|
# tradein-browser, 12.09: у потолка, и это анонимная память незакрытых
|
||||||
|
# инстансов браузера. Тревога обязана прийти, и в тексте — доля, а не байты.
|
||||||
|
- interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: 'container_memory_working_set_bytes{host="apps",name="tradein-browser"}'
|
||||||
|
values: '1960x40'
|
||||||
|
- series: 'container_memory_rss{host="apps",name="tradein-browser"}'
|
||||||
|
values: '1900x40'
|
||||||
|
- series: 'container_spec_memory_limit_bytes{host="apps",name="tradein-browser"}'
|
||||||
|
values: '2000x40'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: ContainerNearMemoryLimit
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
severity: warning
|
||||||
|
host: apps
|
||||||
|
name: tradein-browser
|
||||||
|
exp_annotations:
|
||||||
|
summary: "Контейнер у своего потолка памяти"
|
||||||
|
description: "apps / tradein-browser: 98% от mem_limit, и больше половины лимита — анонимная память процессов, которую ядро не вытеснит как кэш. Дальше OOM-kill."
|
||||||
|
|
||||||
|
# 12.09–17.09: экспортёр Celery не поднят, серий нет вовсе, воркер при этом жив.
|
||||||
|
# Должна гореть QueueExporterDown про celery — и НЕ должна NoActiveCeleryWorkers.
|
||||||
|
- interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: 'up{job="redis",host="apps"}'
|
||||||
|
values: '1x40'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: NoActiveCeleryWorkers
|
||||||
|
exp_alerts: []
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: QueueExporterDown
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
severity: warning
|
||||||
|
job: celery
|
||||||
|
exp_annotations:
|
||||||
|
summary: "Метрики очереди не собираются"
|
||||||
|
description: "celery-экспортёр на Poincare не отдаёт метрики (up=0 или серии нет вовсе). Это авария наблюдаемости, а не продукта: Redis и воркеры могут быть живы — проверь `docker ps`. Пока она горит, RedisDown и NoActiveCeleryWorkers молчат по построению."
|
||||||
|
|
||||||
|
# Нормальная работа: экспортёр жив, воркер шлёт heartbeat — тишина.
|
||||||
|
- interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: 'up{job="celery",host="apps"}'
|
||||||
|
values: '1x40'
|
||||||
|
- series: 'up{job="redis",host="apps"}'
|
||||||
|
values: '1x40'
|
||||||
|
- series: 'celery_worker_up{hostname="celery@worker",host="apps"}'
|
||||||
|
values: '1x40'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: NoActiveCeleryWorkers
|
||||||
|
exp_alerts: []
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: QueueExporterDown
|
||||||
|
exp_alerts: []
|
||||||
|
|
||||||
|
# Воркер умер: экспортёр жив, серия воркера в 0. Прежнее правило
|
||||||
|
# (`count(x == 1) == 0`) этот случай пропускало — count() от пустого вектора пуст.
|
||||||
|
- interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: 'up{job="celery",host="apps"}'
|
||||||
|
values: '1x40'
|
||||||
|
- series: 'celery_worker_up{hostname="celery@worker",host="apps"}'
|
||||||
|
values: '0x40'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: NoActiveCeleryWorkers
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
severity: critical
|
||||||
|
exp_annotations:
|
||||||
|
summary: "Ни одного живого воркера Celery"
|
||||||
|
description: "celery-exporter работает, но не видит ни одного heartbeat от воркера Site Finder. Все periodic-таски (парсинг, аналитика, синк слоёв) встали."
|
||||||
|
|
||||||
|
# Воркер умер давно: экспортёр вычистил его серию, осталось только `up`.
|
||||||
|
- interval: 1m
|
||||||
|
input_series:
|
||||||
|
- series: 'up{job="celery",host="apps"}'
|
||||||
|
values: '1x40'
|
||||||
|
alert_rule_test:
|
||||||
|
- eval_time: 30m
|
||||||
|
alertname: NoActiveCeleryWorkers
|
||||||
|
exp_alerts:
|
||||||
|
- exp_labels:
|
||||||
|
severity: critical
|
||||||
|
exp_annotations:
|
||||||
|
summary: "Ни одного живого воркера Celery"
|
||||||
|
description: "celery-exporter работает, но не видит ни одного heartbeat от воркера Site Finder. Все periodic-таски (парсинг, аналитика, синк слоёв) встали."
|
||||||
Loading…
Add table
Reference in a new issue