ГлавнаяБлогПрометеус молчит: как я поймал скрытый баг в алертинге
Карьера

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

Разбор бага, когда Prometheus молчал 6 дней из-за дедупликации алертов. Узнай, как исправить и не наступить на грабли.

Al
Редакция Algolitalgolit.ru
7 мин чтения20 августа 2026 г.

Почему зелёные дашборды — это не доказательство здоровья системы

Представь: сервис работает, метрики рисуются, статус-страница зелёная. А база данных тем временем не пишет на диск уже несколько часов. Именно так я потерял шесть дней, пока мой личный observability-стек на Railway тихо умирал. В этой статье разберу, как дедупликация алертов превратила постоянную ошибку в «разовый сбой», и как я это починил.

Как я обнаружил, что Prometheus не пишет данные

13 августа Prometheus перестал сохранять блоки на диск. Каждую минуту TSDB-компакция падала с ошибкой no space left on device. Шестьдесят ошибок в час — не деградация, а полный отказ. Но внешне всё выглядело нормально: статус-страница отдавала актуальные числа, HTTP-проверки зелёные, Grafana рисовала графики. Проблема была невидима для всех инструментов мониторинга, потому что Prometheus отвечает на запросы из head-block в памяти. Я нашёл проблему, читая логи контейнеров руками — это был единственный способ.

И вот что оказалось стыдно: метрики prometheus_tsdb_* никто не собирал. Prometheus был единственным сервисом в стеке, за которым никто не следил. Не было рядов, чтобы написать правило алерта. Я исправил: настроил сбор метрик Prometheus самим Prometheus, ограничил хранение по размеру, а не только по времени, и добавил watchdog, который шлёт события в Sentry.

Второй баг: алерт сработал один раз и замолчал навсегда

Sentry получил одно событие. Затем — тишина на шесть дней. Когда я зашёл 19 августа, там было написано «last seen five days ago». Выглядело как разовый сбой, который сам разрешился. Я чуть не закрыл issue.

Дедупликация в моём watchdog была модульной переменной-множеством: одно уведомление за время жизни процесса. Watchdog сработал один раз, добавил ключ и больше не говорил. Четыре события, которые я получил, были не четырьмя срабатываниями, а четырьмя перезапусками. Между ними компакция падала 60 раз в час, а алертинг молчал.

Тишина была бы честной. Но алерт активно сообщал форму — одно событие и тишина, — которую человек читает как «временный сбой». Баг не в самой дедупликации: она нужна, чтобы не слать 1440 событий в день. Проблема в окне дедупликации «навсегда», которое не отличает «случилось один раз» от «продолжает происходить».

В том же коде был и баг стоимости: watchdog выполнялся на каждом запросе, а не раз в минуту. После перехода на тариф с оплатой за использование это грозило лишними расходами.

Как я исправил дедупликацию: TTL вместо вечного окна

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

INFRA_ALERT_INTERVAL_S = 3600.0
_INFRA_ALERTS_SENT: dict[str, float] = {}

async def _report_infra_throttled(key: str, exc: Exception) -> None:
    last_sent = _INFRA_ALERTS_SENT.get(key)
    now = time.monotonic()
    if last_sent is not None and now - last_sent < INFRA_ALERT_INTERVAL_S:
        return
    _INFRA_ALERTS_SENT[key] = now  # помечаем ДО отправки, осознанно
    await capture_exception(exc, tags={"endpoint": "status"})

Ключевая строка — _INFRA_ALERTS_SENT[key] = now до вызова capture_exception. Почему? capture_exception проглатывает ошибки доставки: если Sentry недоступен, час тишины мы платим в любом случае. Но если пометить после отправки, то 5xx со стороны Sentry превратится в лавину ретраев на каждом публичном запросе. Лучше потерять одно событие, чем усилить чужой сбой. Это fail-open подход, как и во всём проекте.

Ещё я заменил time.time() на time.monotonic() — чтобы перевод системных часов не заморозил алерт. И вынес watchdog из горячего пути: теперь он запускается раз в минуту, а не на каждый запрос.

Настройка хранения и watchdog для watchdog

Retention пересчитан под реальный диск: было 7d/300MB для 500MB тома, стало 30d/3GB для 5GB. Иначе история обрезалась бы до 6% уже оплаченного диска. Теперь время истекает первым, а размер — страховка от повтора исходной ошибки.

Из этого опыта родился четвёртый тип исключения — PrometheusWatchdogBlind. Он срабатывает, когда входные данные самого watchdog отсутствуют. Если запрос watchdog возвращает пустой ряд, он вечно пишет «всё в порядке». Я только что неделю учился, сколько это стоит.

Главный вывод: зелёные дашборды — это отсутствие доказательств

Каждый слой был слеп по-своему, поэтому проблема жила так долго. Сервис, который никто не скрейпил, — не могло быть правила. Watchdog, который говорил один раз за процесс, — постоянная ошибка выглядела временной. Статус-API читал RAM, пока диск умирал, — публичный сигнал оставался правдивым и бесполезным.

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

Что делать прямо сейчас: проверь, собираешь ли ты метрики со своих сборщиков метрик. Убедись, что дедупликация алертов имеет TTL, а не вечное окно. И добавь watchdog для watchdog — чтобы тишина не выглядела как «всё ок».

#prometheus#алертинг#дедупликация#sentry#мониторинг
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →