Модуль 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. Почему алерт не сработал: чек-лист из пяти проверок
Идти сверху вниз, не перескакивая:
- Правило загружено?
curl -s localhost:9092/api/v1/rules— есть лиCacheStampedeв списке. Нет — сначалаmake fix-6иmake reload-prom. - Цель скрапится? На
/targetsjobapiвUP(в full-профиле — ещёsinkиworker, их метрики на :9101/:9102)./metricsотдаёт api. Если цель лежит, выражение вернёт пусто, и правило промолчит. - Выражение даёт данные? Выполните
exprвручную в Prometheus UI: пустой результат и ноль — разные вещи; проверьте имена метрик и меткуroute. - Условие держится
for? Алерт вpendingпри истинномexpr— норма, если шторм короче 15 с. СмотритеALERTSи статус правила, а не только дашборд. - Алерт дошёл?
/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/. Время — ориентир для одного человека.
- Базовая линия (5 мин).
make up && make seed, откройте Grafana (http://localhost:3001/d/polygon-overview) и запуститеmake load-hot. Зафиксируйте p99/hot, hit ratio и соотношениеmisses/sиcomputes/s. - Сломать (5 мин).
make fail-6копирует базовые алерты, перезагружает Prometheus, пересоздаёт api вnaiveи 20 с гоняет hot-нагрузку со сбросом ключей: «базовые алерты (без CacheStampede); шторм 20с...», алерта нет. - Убедиться, что сигнала нет (5 мин).
bashcurl -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 мин).
bashmake 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 "Проверь себя"
for: 15s?✅ Итоги модуля
- Назвать четыре золотых сигнала и показать каждый на панелях 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 остаётся живым.