MAATRIX / Блог / Prometheus не собирал половину целей, и график был честно ровным

Prometheus не собирал половину целей, и график был честно ровным

MAATRIX

Самое неприятное в мониторинге — не когда он орёт зря, а когда молчит, хотя должен орать. У нас Prometheus три недели честно не видел половину целей после расширения кластера, а главный дашборд всё это время показывал ровную линию без единой аномалии. Разбираем, как так вышло, какие гипотезы мы проверяли зря и что в итоге сломалось на самом деле — на уровне сети, а не на уровне метрик.

Симптом: /targets показывал down, а дашборд — ровную линию

История началась не с алерта, а со случайности. Инженер разбирался с медленным раскатом канареечной версии и открыл графики CPU по конкретным инстансам нового пула серверов. Часть графиков была пустой — не «упало до нуля», а буквально No data, как будто инстансов не существует. При этом сами серверы работали, отвечали на запросы, systemctl status был зелёным.

Проверка /targets в самом Prometheus расставила всё по местам:

app-07-new    DOWN   context deadline exceeded
app-08-new    DOWN   context deadline exceeded
app-09-new    DOWN   context deadline exceeded
app-01-old    UP
app-02-old    UP
...

Из 14 целей job app шесть были в состоянии down — все из нового пула серверов, добавленного при расширении три недели назад. Старые инстансы скрейпились штатно. То есть проблема жила с момента расширения, но никто её не заметил — ни по алертам, ни по дашбордам.

Дальше стандартный вопрос для любого разбора инцидента: что видели, чего не видели и почему. У нас были логи scrape-ошибок (context deadline exceeded — это таймаут установки соединения, а не ошибка приложения), был список целей в service discovery, и был график, который две недели подряд выглядел абсолютно нормально. Именно последний пункт и увёл расследование в сторону на первом же шаге.

Почему график был честно ровным

Главный дашборд команды строился на агрегирующих запросах вида:

sum(rate(http_requests_total{job="app"}[5m]))
avg(node_cpu_seconds_total{job="app", mode="idle"})

sum() и avg() считают по тем сериям, которые реально есть в TSDB. Если шесть инстансов из четырнадцати вообще не попадают в базу — для агрегата их как будто не существует. Никакого провала графика не происходит: он просто честно показывает сумму или среднее по тому, что видит. А поскольку балансировщик перед приложением ровно распределял трафик по живым (то есть скрейпящимся) инстансам, а автоскейлинг компенсировал недостачу лишними репликами, суммарный RPS и средняя latency оставались в прежнем диапазоне.

Получилась ловушка: дашборд не врал — он действительно показывал ровную картину, потому что сама система осталась работоспособной за счёт избыточности. Но избыточность съедала запас прочности молча, без единого сигнала. Если бы в момент расследования начался реальный пиковый трафик, оставшихся восьми инстансов могло не хватить, а никакого предупреждения об этом дашборд дать не мог — он не показывал факта потери половины scrape-покрытия в принципе.

Отдельный урок: ни на одном дашборде не было панели с количеством целей up по job. Метрика up{job="app"} в Prometheus существует всегда, автоматически, без дополнительной настройки экспортёров — но ей никто не пользовался.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотеза первая и вторая: экспортёр и service discovery — мимо

Первая версия — экспортёр (в нашем случае — сам /metrics эндпоинт приложения плюс node_exporter на порту 9100) не поднялся на новых серверах или упал под нагрузкой. Проверили с самих серверов, локально:

curl -sS http://localhost:9100/metrics | head
curl -sS http://localhost:8080/metrics | head
systemctl status node_exporter

Всё работало. Экспортёры отвечали мгновенно, без ошибок, с полным набором метрик. Гипотеза отпала за пять минут.

Вторая версия — служба обнаружения целей (service discovery) не подхватила новые серверы, и Prometheus в принципе не знает об их существовании. Мы использовали file_sd_config, куда список хостов пишет отдельный скрипт при каждом изменении инвентаря. Проверили конфиг и сам файл:

cat /etc/prometheus/targets/app.json
[
  {
    "targets": ["app-07-new:9100", "app-08-new:9100", "app-09-new:9100"],
    "labels": { "job": "app", "env": "prod" }
  }
]

Новые хосты в файле были, Prometheus эти цели видел (иначе на /targets они не показывались бы вовсе, а не висели бы в состоянии down). Значит, discovery отработал корректно — сервер знал, куда стучаться, но само подключение не проходило. Это отсекло вторую гипотезу и сузило поиск до сетевого уровня.

Гипотеза третья: тайминги скрейпа — тоже мимо

Третья версия была на всякий случай: возможно, scrape_timeout слишком агрессивный и новые серверы просто не успевают ответить под нагрузкой. Проверили конфиг:

scrape_configs:
  - job_name: app
    scrape_interval: 15s
    scrape_timeout: 10s
    file_sd_configs:
      - files: ["/etc/prometheus/targets/app.json"]

10 секунд — щедрый таймаут для локального HTTP-запроса к экспортёру, который сам отвечает за миллисекунды. К тому же ошибка была не «иногда медленно», а стабильно одна и та же на всех шести хостах, без единого успешного скрейпа за три недели. Проблема таймингов даёт совсем другую картину — плавающие успехи вперемешку с отказами. Здесь же был стопроцентный, стабильный, воспроизводимый отказ. Это тоже не билось с гипотезой про нагрузку и таймауты.

К этому моменту стало ясно: приложение живое, экспортёр живой, Prometheus знает о целях — но соединение между Prometheus и новыми серверами не устанавливается вообще. Дальше расследование ушло туда, где и должно было начаться сразу — в сеть.

Настоящая причина: firewall не пустил новый сегмент к порту скрейпа

Проверка с самого сервера Prometheus дала однозначный ответ:

curl -v --max-time 5 http://app-07-new:9100/metrics
# curl: (28) Failed to connect to app-07-new port 9100: Connection timed out

curl -v --max-time 5 http://app-01-old:9100/metrics
# HTTP/1.1 200 OK

А с промежуточного хоста, находящегося в той же подсети, что и новые серверы, тот же запрос к app-07-new:9100 проходил моментально. Значит, дело не в самом сервере и не в приложении — пакеты не долетали до порта 9100 именно с хоста мониторинга, и именно на новую подсеть.

При расширении кластера три недели назад новые серверы подняли в отдельном сегменте сети (новый диапазон адресов, добавленный отдельным провижининг-скриптом). Правило firewall на стороне мониторингового хоста, разрешающее входящие и исходящие соединения на порт 9100/9090, обновлялось не автоматически, а отдельным ansible-плейбуком, который в момент расширения просто не запустили — обновили только правила для веб-порта приложения (80/443), потому что именно их проверяли при приёмке новых серверов. Итоговое правило nftables выглядело так:

table inet filter {
  chain output {
    ip daddr 10.0.1.0/24 tcp dport 9100 accept  # старая подсеть — есть
    ip daddr 10.0.1.0/24 tcp dport 9090 accept  # старая подсеть — есть
    # для 10.0.4.0/24 (новая подсеть) правила для 9100/9090 не завели
  }
}

Правила для порта приложения новую подсеть учитывали, потому что их правил casting шёл через тот же Terraform-модуль, что разворачивал сами серверы. А правила для мониторингового трафика жили в отдельном плейбуке, который применялся вручную и по памяти — и про него в момент расширения забыли. Классическая причина такого рода инцидентов: два источника правды для соседних, но разных конфигураций, и синхронизация между ними держится на человеке, а не на автоматике.

Почему молчал алерт: группировка by job и широкий silence

Отдельный и не менее важный вопрос — почему при таком стабильном отказе не сработал алерт up == 0, который у нас был настроен. Он действительно сработал — в первый же день после расширения. Правило выглядело так:

- alert: TargetDown
  expr: up{job="app"} == 0
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Target down for job {{ $labels.job }}"

А маршрутизация в Alertmanager группировала алерты только по job:

route:
  group_by: ["alertname", "job"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 12h

В день расширения кластера уже случался кратковременный ложный down на паре новых хостов — они на несколько минут дольше обычного проходили health-check при старте, и алерт мигнул. Дежурный расследовал, увидел, что цели быстро поднялись, счёл инцидент решённым и поставил silence на все алерты TargetDown с меткой job="app" на неделю — «чтобы не дёргало во время раскатки». Silence был широким: он матчился по одному лейблу job, без учёта instance. Когда через несколько часов реальная, постоянная проблема с firewall проявилась на тех же шести хостах, новый алерт попал под тот же самый silence и не отправил ни одного уведомления. А через неделю про сам silence благополучно забыли, потому что дашборд выглядел спокойным и повода вспоминать не было.

Получилась связка из двух независимых, но усиливающих друг друга проблем: сетевой доступ был перекрыт по забытому правилу firewall, а сигнал об этом гасился слишком широким и слишком долгим silence. Ни одна из причин по отдельности не объясняет три недели тишины — вместе они объясняют её полностью.

Если разбираетесь, как в принципе устроена связка Prometheus и Grafana с нуля, у нас есть отдельный разбор — настройка Prometheus и Grafana в связке. А про типичные ошибки, из-за которых мониторинг существует, но реальной пользы не приносит, — в статье антипаттерн мониторинга, который никто не смотрит.

Что изменили после разбора

По итогам инцидента список правок получился короче, чем можно было ожидать, но каждая пункт закрывает конкретную дыру, а не «мониторинг в целом»:

  • Firewall для мониторингового трафика перенесли в тот же Terraform-модуль, что разворачивает серверы и настраивает правила для порта приложения. Теперь оба набора правил обновляются одним apply и не могут разъехаться по разным подсетям.
  • Правило алерта переписали с группировкой по instance, а не только по job:
- alert: TargetDown
  expr: up{job="app"} == 0
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Target {{ $labels.instance }} down for job {{ $labels.job }}"
route:
  group_by: ["alertname", "job", "instance"]

Теперь новый упавший инстанс создаёт отдельную группу уведомлений, даже если по этому же job уже висит silence на другой инстанс.

  • Silence обязали снабжать комментарием с причиной и списком конкретных instance, а не общим job — это внесли в чеклист дежурного как обязательный пункт, а не пожелание.
  • Добавили постоянную панель «доля целей up» на главный дашборд:
count(up{job="app"} == 1) / count(up{job="app"})

и алерт на неё отдельно от алерта по каждой цели:

- alert: TargetCoverageLow
  expr: count(up{job="app"} == 1) / count(up{job="app"}) < 0.9
  for: 10m
  labels:
    severity: critical

Такой алерт реагирует на факт «мы видим меньше 90% целей», даже если конкретные TargetDown-алерты по какой-то причине снова окажутся заглушены.

  • В план пост-инцидентного разбора добавили обязательный пункт «что показывал дашборд во время проблемы и почему он не показал провал» — потому что именно ложное спокойствие графика оказалось главной причиной, по которой инцидент тянулся три недели, а не три часа. Если ваша команда ещё не формализовала такой разбор, у нас есть отдельный материал о том, как писать разбор инцидента, который помогает не терять подобные детали.

Отдельно стоит сказать: сам по себе Prometheus в этой истории не виноват — он честно писал down в /targets с первого дня и ни секунды не скрывал проблему. Инцидент случился на стыке инфраструктуры (забытое правило firewall) и процесса (слишком широкий silence, отсутствие панели покрытия целей). Это довольно типичная картина: инструмент работает как задумано, а разрыв — в том, как команда его использует и на что смотрит по умолчанию. Похожий сценарий, где мониторинг молчал не из-за багов, а из-за слепой зоны в самой логике проверки, разобран в статье про пропавший RAID, о котором мониторинг молчал месяц.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро проверить, что Prometheus реально видит все ожидаемые цели, а не только то, что не помечено down?

Сравните число записей в service discovery (файл file_sd, вывод Consul catalog или Kubernetes-эндпоинтов) с результатом count(up{job="..."}). Если числа расходятся — часть целей не долетает до Prometheus вообще, и /targets их даже не покажет.

Почему up == 0 не считается достаточным алертом сам по себе?

Потому что он ничего не говорит о масштабе проблемы и легко тонет в широких silence или в группировке без учёта instance. Держите рядом второй, агрегирующий алерт — на долю доступных целей, как показано выше, — он переживёт неправильно настроенный silence на конкретный инстанс.

Можно ли было заметить проблему раньше без случайного стечения обстоятельств?

Да — если бы на главном дашборде была хотя бы одна панель с абсолютным числом или долей up-целей, провал с 14 до 8 бросился бы в глаза в первый же день. Агрегирующие графики трафика и latency для этого физически не предназначены — они показывают результат работы системы, а не её видимость для мониторинга.

Стоит ли использовать одну и ту же firewall-логику для трафика приложения и трафика мониторинга?

Разумнее держать их в одном источнике правды (один Terraform-модуль, один плейбук), но с раздельными явными правилами, а не полагаться на то, что широкое правило «разрешить всё внутри VPC» когда-нибудь понадобится. Именно раздельные, вручную обновляемые правила стали причиной разъезда конфигураций в этом инциденте.

Как правильно ограничивать silence в Alertmanager, чтобы не повторить эту ошибку?

Всегда указывайте максимально узкий matcher — конкретный instance, а не только job, ставьте реалистичный срок истечения (часы, а не недели) и обязывайте оставлять комментарий с причиной и датой снятия. Широкий и долгий silence — это, по сути, отключение мониторинга для целой группы целей, просто оформленное не так явно.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →