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 | Инструкция, что проверить и как действовать. |
Пример:
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 | Формула | Почему важно |
|---|---|---|
| Availability | 1 - 5xx / total | Пользователь может выполнить conversion. |
| Latency | доля requests быстрее 300ms | Сервис не просто отвечает, а отвечает вовремя. |
| Freshness | возраст последнего курса меньше N минут | Ответы основаны на свежих данных. |
| Dependency correctness proxy | доля dependency failures | Видно, что ломается инфраструктурная часть. |
| Consumer freshness | lag/age обработанных событий | Система не отстает от входящего потока. |
Важно: 4xx и доменные ошибки не всегда портят availability. Если пользователь отправил невалидный запрос, это не production outage. А вот timeout PostgreSQL или provider API - уже системная проблема.
Symptom alerts вместо cause alerts
Cause-based alert:
CPU > 85%
Symptom-based alert:
/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:
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%:
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 плохих событий от общего числа.
burn rate = observed error ratio / allowed error ratio
allowed error ratio = 1 - SLO
Для 99.9%:
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:
sum(rate(http_request_duration_seconds_bucket{route="/convert",le="0.3"}[5m]))
/
sum(rate(http_request_duration_seconds_count{route="/convert"}[5m]))
Alert:
- 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:
(
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 за окно:
increase(http_requests_total{code=~"5.."}[30m]) > 20
Или на доменный SLI: "нет успешного обновления курса 10 минут", а не "error ratio высокая".
Freshness SLO
Для сервиса курсов валют freshness часто важнее обычной "живости" API.
Метрика:
ratedesk_last_successful_rate_update_timestamp_seconds
SLI:
time() - ratedesk_last_successful_rate_update_timestamp_seconds < 300
Alert:
- 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 раз в неделю пересматриваются.
Пример мыслительного фильтра:
Если alert сработал ночью, инженер может безопасно сделать что-то полезное за 10 минут?
Если ответ "нет", это, скорее всего, dashboard panel или ticket, а не page.
Пример Alertmanager routing:
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:
- Подтвердить impact: route, процент ошибок, latency, freshness.
- Проверить recent deploy/migration.
- Открыть API dashboard.
- Найти trace медленного или ошибочного запроса.
- Найти logs по
request_id/trace_id. - Проверить зависимости: DB, Redis, Kafka, provider.
- Выбрать mitigation: rollback, feature flag, stale cache, pause consumer.
- Проверить recovery.
- Создать postmortem/follow-up, если был SEV.
SLO worksheet
Перед тем как писать alert rule, заполните таблицу:
| Поле | Пример для RateDesk |
|---|---|
| User journey | POST /convert возвращает расчет. |
| SLI | Доля successful non-5xx requests. |
| Good event | HTTP 2xx/3xx и expected 4xx validation. |
| Bad event | 5xx, timeout, dependency failure. |
| Исключения | 400 validation, 404 по доменному not found, health checks. |
| Окно | 30 дней. |
| SLO | 99.9%. |
| Источник | http_requests_total recording rules. |
| Alert | fast burn page, slow burn ticket. |
| Owner | backend/on-call. |
| Runbook | docs/runbooks/high-5xx.md. |
Связка для dashboard:
| SLI | PromQL | Panel | Alert | Runbook |
|---|---|---|---|---|
| Availability | 1 - 5xx / total | SLO state, burn rate | Fast/slow burn | high-5xx |
| Latency | good events under 0.3 | p95 + good ratio | latency SLO | high-latency |
| Freshness | time() - last_success | freshness age | stale rates | stale-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.
Интерактивная практика
Какой alert лучше всего подходит для page ночью?
Что выведет этот код?
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))
}
Реализуй AlertPolicy: fast-burn должен вернуть page, slow-burn - ticket, cpu-high без user impact - dashboard.