Модуль 6. Алерты и SLO: сигнал, а не шум

Цель модуля: превратить метрики стенда в работающий сигнал: сформулировать SLO, написать алерт на симптом, проверить его на живом шторме и научиться разбирать, почему правило молчит, когда инцидент уже идёт.

Перед началом: наблюдаемость и SLO на уровне arch v2 m6, эксплуатация — pg-clickhouse m8. Инструменты: Prometheus, Alertmanager, Grafana, curl, python3, make. Трудоёмкость: ~1.5 ч (разбор 40 мин · практика 50 мин).

🔥 Инцидент: «шторм был, а дежурный узнал от клиента»

15:25. Клиент пишет в поддержку: отчёт в кабинете «висит». Дежурный открывает Grafana: p99 /hot уже двадцать минут в секундах, панель «Кэш: промахи и вычисления» показывает misses/s и computes/s вплотную, hit ratio провалился. Инцидент начался в 15:05 — сигнала не было.

Разбор: метрики и дашборд были, но в alerts.yml лежал только ApiP99High — правило на p99 /orders. Шторм бил по /hot, порог 0.3 с не превышался, алерт молчал. О деградации узнали от клиента — самый дорогой канал оповещения.

Урок: наблюдаемость без алерта — музей, а не дежурство. Метрики отвечают на вопросы; алерт будит, когда ответ уже не ждёт.

📖 6.1. Четыре золотых сигнала и SLO

Четыре сигнала — минимум, который отвечает на вопрос «система здорова?»:

СигналЧто означаетМетрика стенда
Latencyсколько ждёт пользовательhttp_request_duration_seconds (p99, histogram_quantile)
Trafficсколько запросов идётhttp_requests_total
Errorsсколько запросов неуспешныhttp_requests_total{code=~"5.."}
Saturationнасколько близко к пределуdb_pool_waiting_conns, очередь пула

Для кэша к насыщению добавляется hit ratio: кэш защищает БД, пока попадания преобладают. SLO переводит сигнал в обязательство: «p99 /orders не выше 300 мс» — это же число стоит в ApiP99High. Бюджет ошибок — доля времени, которую SLO разрешает потратить: при 99 % это ~7 часов 12 минут в месяц. Пока бюджет цел, дежурный не реагирует на каждый всплеск; когда он горит — реагирует.

Глубина — arch v2 m6; эксплуатация — pg-clickhouse m8.

📖 6.2. Алерт на симптом, а не на причину

Причин деградации бесконечно много: пропал индекс, встал replay, вспыхнул stampede, партнёр завис. Симптомов — четыре. Алерт на симптом спрашивает «пользователю плохо?», алерт на причину — «Redis занял много памяти?». Второй срабатывает, когда всё в порядке, и молчит, когда причина новая.

ApiP99High — симптом: p99 /orders выше SLO. CacheStampede — тоже симптом, только на уровне защиты кэша: он отвечает не «много вычислений», а «вычисления не схлопываются». Отношение вычислений к промахам (выражение целиком — в 6.3) устойчиво к трафику: при исправном singleflight вычислений может быть много, но на промах их мало. clamp_min защищает от деления на ноль, вторая часть условия — от шума на почти пустом трафике. for: 15s требует, чтобы условие продержалось несколько вычислений подряд (evaluation идёт каждые 5 с), и отсекает одиночные всплески.

📖 6.3. Правила Prometheus: expr, for, labels, annotations

Правило — YAML-объект в группе. Четыре поля несут всю смысловую нагрузку:

  • expr — PromQL, который возвращает true/false для каждой серии;
  • for — сколько условие должно держаться до перехода в firing;
  • labels — по ним маршрутизируют и группируют (severity: critical|warning);
  • annotations — текст для человека: summary и runbook.
yaml
- alert: CacheStampede
  expr: >
    sum(rate(cache_compute_total[30s]))
      / clamp_min(sum(rate(cache_misses_total[30s])), 0.001) > 0.8
    and
    sum(rate(cache_misses_total[30s])) > 1
  for: 15s
  labels:
    severity: warning
  annotations:
    summary: "Кэш-шторм: каждый промах считает значение сам"
    runbook: "см. модуль 5 полигона: singleflight, TTL, инвалидация"

ApiP99High устроен так же: p99 /orders в окне 2 минуты, порог 0.3 с, for: 30s, severity: critical. Prometheus скрапит цели и вычисляет правила каждые 5 с; жизненный цикл — inactive → pending (условие истинно, for ещё не вышло) → firing. Состояние видно в серии ALERTS{alertname="CacheStampede"}, а через /api/v1/rules — текст правила и статус. Prometheus шлёт firing в Alertmanager; на стенде тот настроен на blackhole-получатель (group_wait: 5s), писем нет — смотреть в UI или /api/v1/alerts.

Правила лежат в infra/prometheus/alerts.yml — это файл-переключатель. Шаблонов два: alerts-base.yml (только ApiP99High) и alerts-stampede.yml (плюс CacheStampede). make fail-6 и make fix-6 копируют нужный шаблон, make reload-prom перечитывает конфиг без перезапуска.

📖 6.4. Почему алерт не сработал: чек-лист из пяти проверок

Идти сверху вниз, не перескакивая:

  1. Правило загружено? curl -s localhost:9092/api/v1/rules — есть ли CacheStampede в списке. Нет — сначала make fix-6 и make reload-prom.
  2. Цель скрапится? На /targets job api в UP (в full-профиле — ещё sink и worker, их метрики на :9101/:9102). /metrics отдаёт api. Если цель лежит, выражение вернёт пусто, и правило промолчит.
  3. Выражение даёт данные? Выполните expr вручную в Prometheus UI: пустой результат и ноль — разные вещи; проверьте имена метрик и метку route.
  4. Условие держится for? Алерт в pending при истинном expr — норма, если шторм короче 15 с. Смотрите ALERTS и статус правила, а не только дашборд.
  5. Алерт дошёл? /api/v1/alerts показывает активные алерты Prometheus. Alertmanager на стенде — blackhole: доставки нет, но состояние алерта обязано быть видно.

Пятая проверка чаще всего объясняет «мне ничего не пришло»: на стенде это ожидаемо. Первые четыре объясняют «алерта нет вовсе».

📖 6.5. Дашборд, который ведёт к причине

В Grafana семь панелей, порядок взгляда — от симптома к слою: «p99 latency по маршрутам» → «RPS по маршрутам» → «Доля ошибок 5xx» → «Кэш: промахи и вычисления» → «Партнёр: исходы вызовов» → «Пул соединений к мастеру» → «Cache hit ratio».

Для шторма: p99 растёт на /hot, RPS не меняется, 5xx нет, misses/s и computes/s идут вместе, hit ratio падает. Это не «медленная БД» и не «ошибки партнёра» — это кэш. Runbook из аннотации ведёт в модуль 5: режим кэша, TTL, инвалидация.

Дашборд отвечает «что происходит», алерт — «пора смотреть». Панель без runbook — красивая картинка; алерт без панели — тревога без маршрута.

📖 6.6. Компромиссы

  • Симптом vs причина. Симптом ловит неизвестные причины, но срабатывает позже и размыто; причина точна, но бесполезна для нового класса отказа.
  • Чувствительность vs шум. Низкий порог ловит всё, включая флаппинг; высокий пропускает короткие инциденты. for и отношение — компромисс.
  • Много алертов vs покрытие. Каждый алерт требует владельца и runbook; сто правил без владельца — это ноль правил.
  • Абсолют vs отношение. Абсолют прост, но зависит от трафика; отношение устойчивее, но шумит на малых числах — отсюда clamp_min и guard.
  • Blackhole vs маршрутизация. Стенд не шлёт писем намеренно; в проде те же labels управляют группами, подавлением и эскалацией.

💻 Практика

Все команды — из каталога polygon/. Время — ориентир для одного человека.

  1. Базовая линия (5 мин). make up && make seed, откройте Grafana (http://localhost:3001/d/polygon-overview) и запустите make load-hot. Зафиксируйте p99 /hot, hit ratio и соотношение misses/s и computes/s.
  2. Сломать (5 мин). make fail-6 копирует базовые алерты, перезагружает Prometheus, пересоздаёт api в naive и 20 с гоняет hot-нагрузку со сбросом ключей: «базовые алерты (без CacheStampede); шторм 20с...», алерта нет.
  3. Убедиться, что сигнала нет (5 мин).
bash
curl -s localhost:9092/api/v1/alerts | python3 -m json.tool
curl -s localhost:9092/api/v1/rules

В первом ответе нет CacheStampede; во втором правило отсутствует — в списке только ApiP99High. Добавьте | grep -i stampede и убедитесь, что он молчит. 4. Красный чек (3 мин). make check-6 — ожидаемо CHECK FAIL: правило CacheStampede не загружено — сначала make fix-6. Это не поломка стенда, а корректный отказ проверки. 5. Починить (2 мин).

bash
make fix-6
make reload-prom
curl -s localhost:9092/api/v1/rules | grep CacheStampede

fix-6 сам копирует alerts-stampede.yml в alerts.yml и делает reload; make reload-prom — идемпотентная повторная перезагрузка, чтобы привычка осталась. В выводе правил появился CacheStampede. 6. Зелёный чек (2 мин). make check-6 устраивает naive-шторм, ждёт до 45 с появления ALERTS{alertname="CacheStampede"} и возвращает singleflight: ожидаемо CHECK OK: сценарий 6: алерт CacheStampede сработал на шторме. 7. Своё правило (15 мин). Добавьте в alerts.yml (секция rules) правило на долю 5xx за минуту:

yaml
- alert: Api5xxHigh
  expr: >
    sum(rate(http_requests_total{code=~"5.."}[1m]))
      / clamp_min(sum(rate(http_requests_total[1m])), 0.001) > 0.05
  for: 1m
  labels:
    severity: critical
  annotations:
    summary: "Доля 5xx выше 5% за минуту"
    runbook: "проверить последние ошибки api и зависимости"

make reload-prom, затем curl -s localhost:9092/api/v1/rules — правило Api5xxHigh загружено, состояние inactive при здоровом стенде. Проверьте expr в UI Prometheus. for: 1m нужен потому, что один всплеск 5xx даёт короткий подъём в окне 1m: без for алерт флапает, и дежурный получает шум. 8. Разбор (5 мин). Сравните ApiP99High и CacheStampede: у обоих есть expr, for, labels, annotations и runbook на конкретный модуль. Почему «> 0.8 отношения» лучше «> 100 вычислений в секунду»?

🧩 Quiz "Проверь себя"

1. Зачем в правиле нужен for: 15s?
2. Почему в сценарии 6 шторм был, а дежурный узнал о нём от клиента?
3. Где на стенде смотреть, сработал ли алерт?
4. Что проверить первым, если алерт не сработал?
5. На что срабатывает CacheStampede?

✅ Итоги модуля

  • Назвать четыре золотых сигнала и показать каждый на панелях Grafana.
  • Сформулировать SLO /orders и объяснить, что такое бюджет ошибок.
  • Объяснить, почему CacheStampede — алерт на симптом, и что означает отношение.
  • Пройти цикл fail-6 → check-6 (red) → fix-6 → check-6 (green).
  • Прочитать правило и активные алерты через /api/v1/rules и /api/v1/alerts.
  • Написать правило на долю 5xx и объяснить роль for против флаппинга.

🔗 Мостик к Модулю 7

Алерт теперь будит дежурного, но сама система от отказа зависимости не защищена: /risk/{id} вызывает партнёра без таймаута, и один зависший внешний сервис съедает горутины, растягивая p99 уже на всех маршрутах. В модуле 7 добавим circuit breaker, bulkhead и управляемую деградацию, а на панели «Партнёр: исходы вызовов» появятся open и bulkhead. Сигнал из модуля 6 покажет, что защита работает: отказы быстрые, а API остаётся живым.