SLI, SLO и alerting без шума

Alert - это просьба отвлечь человека. Иногда разбудить. Поэтому alert должен быть привязан к пользовательскому impact и понятному действию, а не к любому красному графику.

Большая ошибка начинающих команд: алертить на все подряд. CPU 85%, Redis latency дернулась, Kafka lag чуть вырос, GC стал чаще, один pod перезапустился. Через неделю все привыкают игнорировать уведомления. Это называется alert fatigue: сигнал вроде есть, но доверия к нему нет.

Термины

ТерминСмысл
SLIЧисло, которое измеряет качество сервиса.
SLOЦель по SLI за окно времени.
Error budgetСколько "плохого" поведения допустимо до нарушения SLO.
Burn rateВо сколько раз быстрее нормы расходуется error budget.
AlertСигнал, что нужен action.
RunbookИнструкция, что проверить и как действовать.

Пример:

text
SLI: доля /convert requests без 5xx SLO: 99.9% успешных requests за 30 дней Error budget: 0.1% requests могут быть плохими

Если за 30 дней пришел 1 000 000 requests, error budget = 1 000 плохих requests.

Хорошие SLI для RateDesk

SLIФормулаПочему важно
Availability1 - 5xx / totalПользователь может выполнить conversion.
Latencyдоля requests быстрее 300msСервис не просто отвечает, а отвечает вовремя.
Freshnessвозраст последнего курса меньше N минутОтветы основаны на свежих данных.
Dependency correctness proxyдоля dependency failuresВидно, что ломается инфраструктурная часть.
Consumer freshnesslag/age обработанных событийСистема не отстает от входящего потока.

Важно: 4xx и доменные ошибки не всегда портят availability. Если пользователь отправил невалидный запрос, это не production outage. А вот timeout PostgreSQL или provider API - уже системная проблема.

Symptom alerts вместо cause alerts

Cause-based alert:

text
CPU > 85%

Symptom-based alert:

text
/convert 5xx ratio > 2% for 10m

CPU может быть высоким и без user impact. А 5xx ratio прямо говорит: часть операций не выполняется. Cause-графики нужны на dashboard, но page-алерты лучше строить по симптомам.

Хороший компромисс:

  • page: высокий user impact, быстрый burn rate, сервисная деградация;
  • ticket: медленная деградация, capacity risk, noisy dependency;
  • dashboard only: диагностические причины.

Availability SLO

Recording rules:

yaml
groups: - name: ratedesk:sli interval: 30s rules: - record: job:http_requests:rate5m expr: | sum by (job) (rate(http_requests_total[5m])) - record: job:http_errors:ratio_rate5m expr: | sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m])) - record: job:http_errors:ratio_rate30m expr: | sum by (job) (rate(http_requests_total{code=~"5.."}[30m])) / sum by (job) (rate(http_requests_total[30m])) - record: job:http_errors:ratio_rate1h expr: | sum by (job) (rate(http_requests_total{code=~"5.."}[1h])) / sum by (job) (rate(http_requests_total[1h])) - record: job:http_errors:ratio_rate6h expr: | sum by (job) (rate(http_requests_total{code=~"5.."}[6h])) / sum by (job) (rate(http_requests_total[6h]))

Burn-rate alert для SLO 99.9%:

yaml
groups: - name: ratedesk:slo-alerts rules: - alert: RatedeskAvailabilityFastBurn expr: | ( job:http_errors:ratio_rate5m{job="ratedesk"} > (14.4 * 0.001) ) and ( job:http_errors:ratio_rate1h{job="ratedesk"} > (14.4 * 0.001) ) for: 2m labels: severity: page slo: availability annotations: summary: "RateDesk burns availability budget too fast" runbook_url: "https://gitlab.example.com/ratedesk/docs/runbooks/high-5xx" - alert: RatedeskAvailabilitySlowBurn expr: | ( job:http_errors:ratio_rate30m{job="ratedesk"} > (6 * 0.001) ) and ( job:http_errors:ratio_rate6h{job="ratedesk"} > (6 * 0.001) ) for: 15m labels: severity: ticket slo: availability annotations: summary: "RateDesk availability budget drains over hours" runbook_url: "https://gitlab.example.com/ratedesk/docs/runbooks/high-5xx"

Числа 14.4, 6, 0.001 не магия, а SLO-математика. В уроке важно проговорить: burn rate - это кратность превышения допустимого уровня ошибок, а не "проценты в минуту".

Откуда берутся burn-rate числа

Для SLO 99.9% error budget равен 0.1%, то есть 0.001 плохих событий от общего числа.

text
burn rate = observed error ratio / allowed error ratio allowed error ratio = 1 - SLO

Для 99.9%:

text
allowed error ratio = 0.001 error ratio 1.44% = 0.0144 burn rate = 0.0144 / 0.001 = 14.4

14.4x означает: бюджет тратится в 14.4 раза быстрее нормы. Если так продолжать час, команда быстро потеряет заметную часть месячного бюджета, поэтому это page. 6x на более длинных окнах - медленнее, но тоже требует реакции, часто как ticket или daytime page.

В production не копируйте числа слепо. Сначала выберите:

  • окно SLO: 7d, 28d, 30d;
  • допустимый error budget;
  • какую часть budget можно сжечь до page;
  • минимальный traffic, при котором ratio имеет смысл;
  • кто владелец реакции.

Latency SLO

Допустим: 95% /convert должны быть быстрее 300ms.

SLI через histogram bucket:

promql
sum(rate(http_request_duration_seconds_bucket{route="/convert",le="0.3"}[5m])) / sum(rate(http_request_duration_seconds_count{route="/convert"}[5m]))

Alert:

yaml
- alert: RatedeskConvertLatencySLO expr: | ( sum(rate(http_request_duration_seconds_bucket{route="/convert",le="0.3"}[10m])) / sum(rate(http_request_duration_seconds_count{route="/convert"}[10m])) ) < 0.95 for: 10m labels: severity: page slo: latency annotations: summary: "Less than 95% of /convert requests are faster than 300ms" runbook_url: "https://gitlab.example.com/ratedesk/docs/runbooks/high-latency"

p95 на dashboard полезен, но SLO часто лучше формулировать как долю good events.

Low traffic и деление на ноль

На маленьком трафике один 500 может дать страшный error ratio, хотя impact минимальный. Добавляйте traffic guard:

promql
( sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) ) > 0.02 and sum(rate(http_requests_total[5m])) > 1

Для batch/low-traffic сервисов иногда лучше alertить на absolute count за окно:

promql
increase(http_requests_total{code=~"5.."}[30m]) > 20

Или на доменный SLI: "нет успешного обновления курса 10 минут", а не "error ratio высокая".

Freshness SLO

Для сервиса курсов валют freshness часто важнее обычной "живости" API.

Метрика:

text
ratedesk_last_successful_rate_update_timestamp_seconds

SLI:

promql
time() - ratedesk_last_successful_rate_update_timestamp_seconds < 300

Alert:

yaml
- alert: RatedeskRatesStale expr: time() - ratedesk_last_successful_rate_update_timestamp_seconds > 300 for: 5m labels: severity: page slo: freshness annotations: summary: "RateDesk rates are stale for more than 5 minutes" runbook_url: "https://gitlab.example.com/ratedesk/docs/runbooks/stale-rates"

Это хороший пример доменного SLI: пользователю может быть все равно, что API отвечает 200, если данные устарели.

Alert routing

Минимальные правила:

  • каждый page-alert имеет owner;
  • каждый page-alert имеет runbook_url;
  • alert grouping объединяет одинаковые симптомы;
  • inhibition подавляет вторичные alerts при известной корневой проблеме;
  • for защищает от одиночных spikes;
  • noisy alerts раз в неделю пересматриваются.

Пример мыслительного фильтра:

text
Если alert сработал ночью, инженер может безопасно сделать что-то полезное за 10 минут?

Если ответ "нет", это, скорее всего, dashboard panel или ticket, а не page.

Пример Alertmanager routing:

yaml
route: group_by: ["service", "alertname"] group_wait: 30s group_interval: 5m repeat_interval: 2h receiver: default routes: - matchers: - severity="page" receiver: oncall - matchers: - severity="ticket" receiver: backlog inhibit_rules: - source_matchers: - alertname="RatedeskPostgresUnavailable" target_matchers: - dependency="postgres" equal: ["service"]

Что обычно не должно будить само по себе:

  • CPU 85%;
  • рост goroutines без user impact;
  • single pod restart;
  • Redis memory warning;
  • один failed scrape;
  • Kafka rebalance, который быстро завершился.

Эти сигналы нужны на dashboard и как cause-alert ticket, но page лучше держать за user pain: ошибки, latency, stale data, потеря обработки.

Runbook

Минимальный runbook:

  1. Подтвердить impact: route, процент ошибок, latency, freshness.
  2. Проверить recent deploy/migration.
  3. Открыть API dashboard.
  4. Найти trace медленного или ошибочного запроса.
  5. Найти logs по request_id/trace_id.
  6. Проверить зависимости: DB, Redis, Kafka, provider.
  7. Выбрать mitigation: rollback, feature flag, stale cache, pause consumer.
  8. Проверить recovery.
  9. Создать postmortem/follow-up, если был SEV.

SLO worksheet

Перед тем как писать alert rule, заполните таблицу:

ПолеПример для RateDesk
User journeyPOST /convert возвращает расчет.
SLIДоля successful non-5xx requests.
Good eventHTTP 2xx/3xx и expected 4xx validation.
Bad event5xx, timeout, dependency failure.
Исключения400 validation, 404 по доменному not found, health checks.
Окно30 дней.
SLO99.9%.
Источникhttp_requests_total recording rules.
Alertfast burn page, slow burn ticket.
Ownerbackend/on-call.
Runbookdocs/runbooks/high-5xx.md.

Связка для dashboard:

SLIPromQLPanelAlertRunbook
Availability1 - 5xx / totalSLO state, burn rateFast/slow burnhigh-5xx
Latencygood events under 0.3p95 + good ratiolatency SLOhigh-latency
Freshnesstime() - last_successfreshness agestale ratesstale-rates

Практика

Для RateDesk заведите:

  • availability SLO;
  • latency SLO для /convert;
  • freshness SLO;
  • recording rules;
  • 2-3 alert rules;
  • docs/runbooks/high-5xx.md;
  • docs/runbooks/high-latency.md;
  • docs/runbooks/stale-rates.md.

Acceptance:

  • каждый alert имеет runbook_url;
  • page-alert привязан к user impact;
  • есть хотя бы один burn-rate alert;
  • dashboard показывает SLO state и error budget;
  • MR объясняет, почему выбранные 4xx/доменные ошибки входят или не входят в SLI.

Интерактивная практика

Quiz+10 XP

Какой alert лучше всего подходит для page ночью?

  • CPU 85% на одном pod
  • Один failed scrape
  • Redis memory warning без user impact
  • Fast burn availability SLO для /convert
Predict+15 XP

Что выведет этот код?

go
package main import "fmt" func AlertRoute(burnRate float64) string { if burnRate >= 14 { return "page" } return "ticket" } func main() { fmt.Println(AlertRoute(20)) fmt.Println(AlertRoute(3)) }
Задача+20 XP

Реализуй AlertPolicy: fast-burn должен вернуть page, slow-burn - ticket, cpu-high без user impact - dashboard.