Алерты, дежурство и разбор инцидентов

SLO из прошлого урока отвечает на вопрос «достаточно ли хорошо работает сервис». Этот урок - про то, как узнать о проблеме вовремя и что делать дальше: кого разбудить, что он должен увидеть и как сделать, чтобы через месяц то же самое не повторилось.

Хороший алертинг - это баланс между молчанием и шумом, и промахнуться легко в обе стороны.

Grafana Dashboard - RED для сервиса

Минимальный полезный дашборд:

  1. Request Rate: sum(rate(http_requests_total[5m])) by (service)
  2. Error Rate: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
  3. Latency p50/p95/p99: histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
  4. Active Goroutines: go_goroutines
  5. Heap Memory: process_resident_memory_bytes
  6. DB connections: pg_pool_active_connections

Группируйте панели логически: «бизнес-метрики» наверху, «runtime» снизу. Используйте variables ($service, $environment) - один дашборд для всех сервисов.

Alert Rules в Prometheus

groups:
  - name: api-alerts
    rules:
  - alert: HighErrorRate
    expr: |
      sum(rate(http_requests_total{status=~"5.."}[5m]))
      / sum(rate(http_requests_total[5m])) > 0.01
    for: 5m
    labels:
      severity: critical
      team: backend
    annotations:
      summary: "Error rate above 1%"
      description: "Service {{ $labels.service }} has {{ $value | humanizePercentage }} errors"
      runbook: "https://wiki.company.com/runbooks/high-error-rate"

  - alert: HighLatency
    expr: |
      histogram_quantile(0.95,
        sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
      ) > 1
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "p95 latency above 1s"

  - alert: ServiceDown
    expr: up{job="api"} == 0
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: "API instance is down"

for: 5m - алерт срабатывает только если условие держится 5 минут. Это убирает шум от коротких всплесков.

Recording rules для alerting

В уроке про Prometheus уже обсуждали recording rules как ускорение дашбордов. Для alerting они критичнее: алерт-правило вычисляется каждые 30s, и дорогой PromQL умножается на множество правил. Кроме того, evaluation должно быть детерминированным - если рaspрос таймаутит, алерт не выстрелит.

# Сначала - recording rule
- record: job:error_rate:ratio5m
  expr: |
    sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
    /
    sum by (job) (rate(http_requests_total[5m]))

# Потом - простой alert на готовом ряду
- alert: HighErrorRate
  expr: job:error_rate:ratio5m{job="api"} > 0.01
  for: 5m

Выгода двойная: алерт читается мгновенно (одно сравнение, не агрегация по миллионам points), и burn-rate-формулы переиспользуют те же ряды, что и дашборды. Один источник правды для всех потребителей метрики.

Alertmanager: маршрутизация и группировка

route:
  receiver: default
  group_by: [alertname, service]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
  - matchers: [severity="critical"]
    receiver: pagerduty
    repeat_interval: 1h
  - matchers: [severity="warning"]
    receiver: slack-warnings

receivers:
  - name: pagerduty
    pagerduty_configs:
  - service_key: "..."
  - name: slack-warnings
    slack_configs:
  - api_url: "..."
    channel: "#alerts"

Группировка: 100 алертов «pod restart» от одного outage слепляются в одно уведомление. Разные severity → разные каналы: critical → PagerDuty/звонок, warning → Slack.

Inhibition rules в Alertmanager

Когда сервис лежит, одновременно срабатывают десятки алертов: error rate, latency, no traffic, downstream errors. Если все пойдут в pager - это flood. Inhibition rules говорят: «если firing-алерт X, не отправлять алерт Y».

inhibit_rules:
  # Если сервис целиком down - не слать всё остальное про него
  - source_matchers:
      - severity = critical
      - alertname = ServiceDown
    target_matchers:
      - severity =~ "warning|critical"
    equal: [service]

  # Если идёт деплой - гасим алерты, вызванные самим деплоем
  - source_matchers:
      - alertname = DeploymentInProgress
    target_matchers:
      - alertname =~ "HighErrorRate|HighLatency"
    equal: [service]

equal - список labels, которые должны совпадать в source и target, чтобы inhibition применилось (обычно service, cluster, env). Inhibition существенно снижает alert volume в крупных инцидентах, не теряя сигнал - первопричина остаётся в pager-е.

Capacity-based alerting: predict_linear

Реактивный алерт «диск 95%» сработает за 5 минут до полного. За это время дежурный успеет только проснуться. Предиктивный алерт по тренду даёт часы на реакцию.

- alert: DiskWillFillIn4Hours
  expr: |
    predict_linear(node_filesystem_avail_bytes{mountpoint="/data"}[1h], 4*3600) < 0
  for: 10m
  labels: {severity: warning}
  annotations:
    summary: "Disk {{ $labels.instance }} will fill in 4 hours"

- alert: ConnectionPoolExhausting
  expr: |
    predict_linear(pg_pool_available_connections[30m], 1800) < 5
  for: 5m
  labels: {severity: warning}

predict_linear(metric[window], seconds_ahead) экстраполирует линейный тренд: «по текущей скорости, какое значение будет через N секунд». Подходит для метрик с относительно гладкой динамикой: диск, утечка памяти, нарастающая очередь, пул соединений. Не подходит для метрик с резкими циклами (RPS, у которых дневной паттерн) - экстраполяция ночного нуля даст бессмыслицу.

Принципы алертинга (Google SRE)

  1. Алерт на симптомы, не на причины. Симптом - «error rate > 1%». Причина - «CPU 90%, OOM, медленный диск». Симптом видит пользователь; пока пользователь не страдает, причина - фоновый шум.

  2. Каждый алерт требует действия. Если на алерт нет известной реакции - это не алерт, это лог. Уберите его или превратите в дашборд.

  3. Severity - это обещание. Critical = разбуди в 3 ночи. Warning = посмотрим утром. Если warning надо смотреть сейчас - делайте critical.

  4. Runbook обязателен. К каждому алерту - ссылка на инструкцию: что проверить, как диагностировать, как чинить. Иначе on-call дежурный паникует.

Alert fatigue и борьба с ним

Если алертов слишком много, команда перестаёт на них реагировать. Это страшнее, чем отсутствие алертов: вы платите за ложное чувство безопасности.

Метрики качества алертинга:

  • Time to Acknowledge (TTA) - сколько минут до первой реакции
  • False positive rate - процент алертов, не приведших к действию
  • Alert volume per week - сколько алертов в неделю на дежурного

Ежеквартально пересматривайте алерты: что часто срабатывает без действий - удаляйте или ослабляйте пороги.

On-call: PagerDuty, OpsGenie, VictorOps

Эти сервисы решают:

  • Эскалация: если первый дежурный не ответил за 15 минут → второй.
  • Расписание: rotation между членами команды.
  • Подавление: planned maintenance не должен будить.
  • Аналитика: TTA, MTTR, частота инцидентов.

Бесплатные альтернативы для маленьких команд: Grafana OnCall, Slack-only с упоминанием @oncall.

Команда, привыкшая игнорировать алерты, пропустит реальный инцидент. Каждый алерт - обещание: «я проснусь ради этого». Если обещание не выполняется, пересматривайте порог или удаляйте правило.

Runbooks: формат

Хороший runbook содержит:

# Runbook: HighErrorRate

## Симптомы
- Алерт HighErrorRate firing
- В дашборде видим > 1% 5xx ошибок

## Диагностика
1. Открыть Jaeger, фильтр status=error → найти ошибочные spans
2. `kubectl logs deployment/api -n prod --tail=200`
3. Проверить дашборд DB: pg_pool_active_connections, slow queries

## Возможные причины
- Деплой за последний час → откат
- БД лочится на VACUUM → подождать или kill query
- Внешний API упал → включить fallback flag

## Эскалация
Если за 15 минут не разрешено - вызывать tech lead.

Post-mortem culture: blameless и actionable

Алертинг ловит инциденты. Post-mortem - что делать с ними, чтобы они не повторялись. Без культуры разбора каждый инцидент учит только тех, кто на нём был; через год команда меняется и сервис снова падает на тех же граблях.

Шаблон blameless post-mortem:

# Incident 2026-05-12: API latency spike

## Summary
13:42-14:08 UTC, p99 latency on /api/users поднялась до 5s, error rate 2%.
Затронуто ~3% пользователей.

## Timeline
- 13:42 - alert HighLatency fired
- 13:45 - oncall acknowledged, начал диагностику
- 13:51 - обнаружен медленный SQL-запрос после ALTER TABLE
- 13:58 - kill query, latency восстановилась
- 14:08 - alert resolved

## Contributing factors
1. ALTER TABLE прошёл без проверки lock duration
2. Slow query не имел timeout
3. Алерт сработал на симптом, а не на причину - диагностика заняла 6 минут

## What went well
- Алерт сработал быстро (3 минуты после начала)
- Runbook содержал команды диагностики БД

## Action items
- [ ] Добавить statement_timeout на pool (owner: @alice, by 2026-05-19)
- [ ] CI-проверка миграций на lock duration (owner: @bob, by 2026-05-26)
- [ ] Recording rule для slow queries (owner: @carol, by 2026-05-15)

## SLO impact
Error budget spend: 0.02% from monthly 0.1%. Remaining 0.08%.

Ключевые принципы:

  • Blameless - никаких «Иван виноват». Виновата система, не позволившая Ивану принять правильное решение. Это снимает страх и даёт команде честно описывать что произошло.
  • Action items имеют owner и deadline, попадают в backlog. Без этого post-mortem превращается в ритуал.
  • MTTR vs MTBF - Mean Time To Recovery (как быстро чиним) и Mean Time Between Failures (как часто ломается). Цель - снижать MTTR и увеличивать MTBF одновременно.
  • Error budget impact - отчёт о том, сколько бюджета съел инцидент. Если за квартал бюджет потрачен - заморозка фич, фокус на надёжности.

Итоги

  • ✅ Писать правила алертов на симптомы и с обязательным for
  • ✅ Маршрутизировать и группировать алерты в Alertmanager, гасить лишнее через inhibition rules
  • ✅ Предсказывать исчерпание ресурсов через predict_linear
  • ✅ Отличать алерт от лога: каждый алерт требует действия и имеет runbook
  • ✅ Проводить blameless post-mortem с owner и сроком у каждого пункта

Мини-практика

Создай в Grafana дашборд с RED-метриками для HTTP-сервера: RPS, error rate, p50/p95/p99. Добавь правило Prometheus на error rate выше 1% в течение 5 минут и подключи Alertmanager с двумя получателями: critical в почту, warning в Slack. Проверь срабатывание, временно добавив эндпоинт, который всегда отвечает 500. Затем напиши runbook для этого алерта по шаблону выше.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, вести прогресс.