Разбор бага, когда Prometheus молчал 6 дней из-за дедупликации алертов. Узнай, как исправить и не наступить на грабли.
Представь: сервис работает, метрики рисуются, статус-страница зелёная. А база данных тем временем не пишет на диск уже несколько часов. Именно так я потерял шесть дней, пока мой личный observability-стек на Railway тихо умирал. В этой статье разберу, как дедупликация алертов превратила постоянную ошибку в «разовый сбой», и как я это починил.
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 выполнялся на каждом запросе, а не раз в минуту. После перехода на тариф с оплатой за использование это грозило лишними расходами.
Решение — хранить время последней отправки и не слать повторно, пока не пройдёт час. Один час — не случайное число: это минимальный интервал, на котором правило алерта по частоте событий ещё может что-то считать. Раз в процесс — частота равна нулю, и правило молчит.
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 из горячего пути: теперь он запускается раз в минуту, а не на каждый запрос.
Retention пересчитан под реальный диск: было 7d/300MB для 500MB тома, стало 30d/3GB для 5GB. Иначе история обрезалась бы до 6% уже оплаченного диска. Теперь время истекает первым, а размер — страховка от повтора исходной ошибки.
Из этого опыта родился четвёртый тип исключения — PrometheusWatchdogBlind. Он срабатывает, когда входные данные самого watchdog отсутствуют. Если запрос watchdog возвращает пустой ряд, он вечно пишет «всё в порядке». Я только что неделю учился, сколько это стоит.
Каждый слой был слеп по-своему, поэтому проблема жила так долго. Сервис, который никто не скрейпил, — не могло быть правила. Watchdog, который говорил один раз за процесс, — постоянная ошибка выглядела временной. Статус-API читал RAM, пока диск умирал, — публичный сигнал оставался правдивым и бесполезным.
Зелёные дашборды — не доказательство здоровья. Это отсутствие доказательств, и они становятся доказательством только когда ты убедился, что твои инструменты могут покраснеть.
Что делать прямо сейчас: проверь, собираешь ли ты метрики со своих сборщиков метрик. Убедись, что дедупликация алертов имеет TTL, а не вечное окно. И добавь watchdog для watchdog — чтобы тишина не выглядела как «всё ок».
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →