Модуль: Observability · Уровень: Senior
TL;DR#
- SLI (Service Level Indicator) — измеримая метрика поведения сервиса с точки зрения пользователя: доля успешных запросов, доля быстрых запросов, свежесть данных. Формула почти всегда
good events / valid events. - SLO (Service Level Objective) — целевое значение SLI на окне времени: «99.9% запросов за 30 дней успешны». Внутренняя цель команды.
- SLA (Service Level Agreement) — юридический/контрактный обязательство перед клиентом с компенсациями (refund, credits). SLA обычно слабее SLO (если SLO=99.9%, SLA=99.5%), чтобы у команды был запас.
- Error budget =
1 − SLO. При SLO 99.9% бюджет ошибок = 0.1% запросов за окно. Бюджет — это разрешённое количество «плохого», которое можно тратить на риск (релизы, эксперименты). - Burn rate — скорость расходования бюджета. Алертить надо НЕ на «упал один запрос», а на скорость прожигания бюджета через multi-window multi-burn-rate алерты (быстрый + медленный burn). Это даёт высокий precision и recall и убирает шум.
- SLI выбирают по принципу: метрика должна отражать опыт пользователя, быть измеримой на границе, где пользователь «видит» сервис, и иметь чёткий критерий good/bad.
Теория#
Зачем вообще SLO, а не «аптайм 100%»#
100% надёжности — недостижимая и вредная цель: каждый «девятка» удорожает систему экспоненциально, а пользователь всё равно ограничен надёжностью своего Wi-Fi, DNS, браузера. SLO — это инженерный компромисс: формальное согласие команды и бизнеса о том, какой уровень ненадёжности приемлем. Из этого согласия рождается error budget — единый язык для конфликта «фичи vs стабильность». Пока бюджет есть — катим релизы; бюджет исчерпан — фриз фич и работа над надёжностью.
SLI: что именно измеряем#
SLI — отношение «хороших» событий к «валидным» за окно:
SLI = good_events / valid_events × 100%«Valid» — это события, которые в принципе должны учитываться (исключаем, например, запросы от ботов или health-чеки). «Good» — соответствующие критерию качества.
Типы SLI#
| Тип SLI | Что измеряет | Пример good/valid |
|---|---|---|
| Availability | доля успешных запросов | 2xx,3xx,4xx-кроме-429 / все запросы |
| Latency | доля быстрых запросов | запросы < 300ms / все запросы |
| Quality / Correctness | корректность ответа | ответы без деградации / все |
| Freshness | свежесть данных (батчи, репликация) | записи моложе N мин / все |
| Coverage | доля обработанных данных | обработанные записи / все поступившие |
| Throughput / Durability | для очередей, хранилищ | недропнутые сообщения / все |
Ключевой момент для senior: latency SLI — это НЕ среднее и НЕ p99 само по себе, а доля запросов в пределах порога. То есть «доля запросов быстрее 300ms ≥ 99%», а не «p99 ≤ 300ms». Так SLI остаётся в формате good/valid и складывается в error budget. Часто используют два порога (например, 99% быстрее 300ms И 99.9% быстрее 1s), чтобы ловить и типичный опыт, и хвост.
Где измерять#
SLI измеряют как можно ближе к пользователю, но в точке, которую вы контролируете:
- Load balancer / ingress / API gateway — лучший компромисс: видит реальный трафик, не зависит от того, дошёл ли запрос до приложения. Минус — не видит проблем DNS/CDN до балансировщика.
- На сервере (внутри приложения) — проще инструментировать, но не учитывает запросы, которые умерли до входа (перегруженный LB, отказ соединения).
- На клиенте (RUM) — самый честный опыт, но шумно (сеть пользователя) и трудно отделить вашу вину.
Senior-ответ: для большинства request/response SLI берут метрики с балансировщика; для критичного UX добавляют клиентский RUM как отдельный SLI.
SLO: формулировка и окна#
SLO = SLI + целевое значение + окно измерения:
«99.9% HTTP-запросов к API за rolling 28 дней возвращают не-5xx за < 500ms.»
Rolling window vs calendar window#
- Rolling (скользящее, напр. последние 28/30 дней) — отражает текущее здоровье, не «прощает» в начале нового месяца. Лучше для внутренних SLO и алертинга.
- Calendar (месяц/квартал) — совпадает с биллинг-циклом, удобно для SLA-отчётности; но в начале периода бюджет «сбрасывается» и команда может расслабиться.
28 дней (а не 30) часто выбирают, чтобы окно всегда содержало одинаковое число выходных — убирает недельную сезонность из графика.
Сколько «девяток» = сколько бюджета#
| SLO | Допустимый downtime/мес (~30 дней) | Бюджет ошибок (доля) |
|---|---|---|
| 99% (two nines) | ~7.2 часа | 1% |
| 99.9% (three nines) | ~43 мин | 0.1% |
| 99.95% | ~21.6 мин | 0.05% |
| 99.99% (four nines) | ~4.3 мин | 0.01% |
| 99.999% (five nines) | ~26 сек | 0.001% |
Важно: «downtime» — это эквивалент в непрерывном простое; реально бюджет тратится размазанно (фоновые ошибки + всплески).
Error budget#
error_budget (доля) = 1 − SLO
error_budget (события)= valid_events × (1 − SLO)
budget_consumed = bad_events / (valid_events × (1 − SLO))
budget_remaining = 1 − budget_consumedError budget policy — заранее согласованный документ: что происходит при разных уровнях бюджета. Типичный пример:
- Бюджет > 0 → релизы идут как обычно.
- Бюджет потрачен (< 0) → freeze новых фич, все силы на надёжность, обязательный postmortem.
- Бюджет тратится подозрительно быстро → ревью рисковых изменений.
Политика — самая важная часть, потому что без согласованных последствий SLO превращается в график, на который никто не смотрит.
Alerting на burn rate#
Главная идея: алертить не на нарушение SLI в моменте, а на скорость прожигания error budget.
Burn rate = во сколько раз быстрее, чем «равномерно», тратится бюджет.
burn_rate = (error_rate за окно) / (1 − SLO)- Burn rate = 1 → при такой скорости бюджет на весь период истратится ровно к концу окна SLO.
- Burn rate = 10 → бюджет 30-дневного SLO сгорит за 3 дня.
- Burn rate = 14.4 → за окно 1 час сгорит ~2% месячного бюджета.
Сколько бюджета сгорает#
Доля бюджета, израсходованная за окно длиной alert_window:
budget_burned = burn_rate × (alert_window / SLO_window)Например, burn_rate=14.4 за 1ч при 30-дневном SLO: 14.4 × (1h / 720h) = 2% бюджета.
Почему простые алерты плохи#
| Подход | Проблема |
|---|---|
| Алерт «error rate > X% за 5 мин» | Низкий precision: шумит на любом всплеске, не связан с бюджетом |
| Алерт «потрачено N% бюджета» (только) | Низкий recall к скорости: медленная утечка не триггерит, а быстрая катастрофа триггерит слишком поздно |
| Один длинный window | Долгий time-to-detect для острых инцидентов |
| Один короткий window | Шум, флапы, ложные срабатывания |
Multi-window, multi-burn-rate (рекомендация SRE Workbook)#
Комбинируем несколько алертов с разными парами (burn rate, окно). Канонический набор для 30-дневного SLO:
| Severity | Long window | Short window | Burn rate | % бюджета за long window |
|---|---|---|---|---|
| Page (срочно) | 1 час | 5 мин | 14.4 | 2% |
| Page | 6 часов | 30 мин | 6 | 5% |
| Ticket (не срочно) | 1 день | 2 часа | 3 | 10% |
| Ticket | 3 дня | 6 часов | 1 | 10% |
Зачем два окна (long + short) на алерт:
- Long window даёт высокий precision: проблема действительно значима (потрачено заметно бюджета).
- Short window даёт быстрый reset / restore: алерт быстро гаснет, когда инцидент закончился, а не висит ещё час. Short также подтверждает, что проблема всё ещё происходит сейчас, а не «выгорела» в начале long-окна.
Алерт срабатывает, когда оба окна превышают порог burn rate:
burn_rate(long_window) > threshold
AND
burn_rate(short_window) > thresholdShort-окно обычно = 1/12 от long-окна.
Пример на PromQL#
SLI на recording rules, считаем долю ошибок по multi-window:
# Записываем error ratio за разные окна (recording rule)
job:slo_errors_per_request:ratio_rate5m = sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
job:slo_errors_per_request:ratio_rate1h = sum(rate(http_requests_total{code=~"5.."}[1h])) / sum(rate(http_requests_total[1h]))
# Page-алерт: быстрый burn (14.4x), окна 1h + 5m, SLO 99.9% → бюджет 0.001
- alert: ErrorBudgetBurnFast
expr: |
job:slo_errors_per_request:ratio_rate1h > (14.4 * 0.001)
and
job:slo_errors_per_request:ratio_rate5m > (14.4 * 0.001)
labels: { severity: page }
# Ticket-алерт: медленный burn (3x), окна 1d + 2h
- alert: ErrorBudgetBurnSlow
expr: |
job:slo_errors_per_request:ratio_rate1d > (3 * 0.001)
and
job:slo_errors_per_request:ratio_rate2h > (3 * 0.001)
labels: { severity: ticket }Latency-SLI через гистограмму (доля медленных запросов):
# good = быстрее 300ms; считаем долю МЕДЛЕННЫХ как (1 - доля быстрых)
1 - (
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[1h]))
/
sum(rate(http_request_duration_seconds_count[1h]))
) > (14.4 * 0.001)Важно:
le="0.3"должен реально присутствовать как граница бакета в гистограмме, иначе придётся интерполировать. Выбирайте бакеты под пороги SLO заранее.
Go: инструментирование SLI#
var (
httpRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total"},
[]string{"code", "method", "route"},
)
httpDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
// бакеты подобраны под пороги SLO: 300ms и 1s
Buckets: []float64{0.05, 0.1, 0.2, 0.3, 0.5, 1, 2, 5},
},
[]string{"route"},
)
)
func sloMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rec, r)
route := routePattern(r) // нормализованный шаблон, НЕ raw path (кардинальность!)
httpRequests.WithLabelValues(
strconv.Itoa(rec.status), r.Method, route,
).Inc()
httpDuration.WithLabelValues(route).Observe(time.Since(start).Seconds())
})
}Ключевое для senior: метки нормализуются по route pattern (/users/{id}), иначе высокая кардинальность убьёт Prometheus. И 4xx, как правило, исключают из «bad» для availability-SLI (это вина клиента), кроме 429/некоторых 408.
Как выбирать SLI: практический процесс#
- Определите user journeys (критичные сценарии): «открыть ленту», «оформить заказ», «получить ответ API».
- Для каждого journey выберите 1–3 SLI (обычно availability + latency; для данных — freshness/correctness).
- Сформулируйте good/valid в формате событий, привязанных к точке измерения.
- Поставьте достижимое SLO: начните с измерения текущего поведения за 4 недели, поставьте чуть ниже фактического p, ужесточайте итеративно.
- Согласуйте error budget policy с продуктом и руководством.
- Настройте multi-burn-rate алерты и дашборд с остатком бюджета.
Меньше SLI — лучше: 1–3 на сервис. Каждый лишний SLI размывает фокус и создаёт шум.
Подводные камни / gotchas#
- Latency SLI как p99, а не как доля — p99 нельзя усреднять и складывать в бюджет; используйте «долю запросов под порогом».
- Усреднение перцентилей между инстансами —
avg(p99)математически бессмысленно. Считайте перцентили из гистограмм черезhistogram_quantileпо агрегированным бакетам, а не усредняя готовые квантили. - Бакеты гистограммы не совпадают с порогом SLO — если SLO про 300ms, а ближайший бакет 250ms/500ms, доля считается неточно. Закладывайте границы бакетов под пороги заранее.
- Высокая кардинальность меток (raw URL, user_id, request_id в метках) — взрывает TSDB. Только нормализованные route/method/code.
- Включение 4xx в bad для availability — раздувает мнимые отказы; обычно 4xx (кроме 429/408) — это валидный «good» с точки зрения доступности сервиса.
- Алерт на «потрачено N% бюджета» без burn rate — острый инцидент задетектится слишком поздно (бюджет уже сгорит), медленная утечка — вообще не задетектится вовремя.
- Один single-window алерт — либо шумит (короткое окно), либо медленно детектит (длинное). Нужен multi-window.
- SLO выше реального опыта пользователя — измерять availability на сервере при том, что половина запросов умирает на перегруженном LB; SLI зелёный, пользователи злые.
- SLA жёстче или равен SLO — нет запаса прочности; любой внутренний инцидент сразу превращается в выплату компенсаций. SLA должен быть слабее SLO.
- Calendar reset расхолаживает — в начале месяца бюджет «полный», команда катит рискованные релизы; rolling window честнее.
- SLO без error budget policy — это просто график, который игнорируют. Без согласованных последствий цель не работает.
- Двойной учёт ретраев — если клиент ретраит, один пользовательский запрос = N серверных; SLI на серверной стороне исказит реальный опыт.
- Зависимые сервисы и композиция SLO — последовательная цепочка из 3 сервисов по 99.9% даёт ~99.7% сквозного; SLO нельзя «наследовать» наивно.
Вопросы на собеседовании#
В: В чём разница между SLI, SLO и SLA и как они связаны?
О: SLI — измеримая метрика опыта пользователя (good/valid). SLO — внутренняя цель по SLI на окне (99.9% за 28 дней). SLA — внешнее контрактное обязательство с компенсациями. Связь: SLI измеряем, по нему ставим SLO, SLA делаем строго слабее SLO (запас прочности). Из SLO выводим error budget = 1−SLO.
В: Почему latency-SLI правильно формулировать как «долю быстрых запросов», а не как p99?
О: Чтобы SLI оставался в формате good/valid и складывался в единый error budget. Перцентили нельзя усреднять между инстансами/окнами и нельзя напрямую конвертировать в бюджет ошибок. «Доля запросов < 300ms ≥ 99%» — это счётчик хороших событий, которым можно оперировать как и availability.
В: Что такое error budget и зачем он нужен?
О: 1 − SLO — допустимая доля «плохого» за окно. Это объективный, согласованный бизнесом ресурс на риск: пока бюджет есть — катим фичи, эксперименты, рискованные релизы; кончился — freeze и работа над надёжностью. Превращает спор «скорость vs стабильность» в арифметику вместо политики.
В: Что такое burn rate и как считать порог для алерта?
О: Burn rate = error_rate / (1−SLO) — во сколько раз быстрее равномерного тратится бюджет. BR=1 истратит весь бюджет ровно к концу окна SLO. Для алерта выбираем, какой процент бюджета готовы потерять за время детекта: budget_burned = burn_rate × (alert_window / SLO_window). Напр., BR=14.4 за 1ч = 2% месячного бюджета.
В: Почему рекомендуют multi-window multi-burn-rate алерты? О: Длинное окно даёт precision (проблема значима, потрачено реально много), короткое окно — быстрый reset (алерт гаснет сразу после конца инцидента) и подтверждение, что проблема происходит сейчас. Несколько пар burn rate/окно покрывают и острые инциденты (быстрый page), и медленные утечки (ticket). Это даёт хорошие precision, recall и time-to-detect одновременно.
В: Где измерять SLI и какие тут компромиссы? О: Ближе к пользователю, но в контролируемой точке. LB/ingress — золотая середина: видит реальный трафик, не зависит от того, дошёл ли запрос до приложения. На сервере — проще, но не видит запросов, умерших до входа. На клиенте (RUM) — честнее всего, но шумно из-за сети пользователя. Часто комбинируют LB-SLI + клиентский RUM как отдельный индикатор.
В: Как выбрать значение SLO для нового сервиса? О: Измерить фактическое поведение за ~4 недели, поставить SLO чуть ниже наблюдаемого p (чтобы он был достижим, но не тривиален), согласовать error budget policy, затем итеративно ужесточать. Нельзя ставить 100% и нельзя ставить «с потолка» выше, чем сервис реально способен держать — иначе бюджет постоянно в минусе и алерты обесцениваются.
В: Чем rolling window отличается от calendar window и что выбрать? О: Rolling (последние N дней) отражает текущее здоровье и не «сбрасывает» бюджет на стыке месяцев — лучше для алертинга. Calendar совпадает с биллинг-циклом — удобно для SLA-отчётности, но расхолаживает в начале периода. На практике: rolling для внутренних SLO/алертов, calendar для контрактной отчётности.
На что копают на senior+#
- Математика композиции SLO: как сквозной SLO зависит от цепочки зависимостей (последовательное умножение надёжностей, влияние параллелизма/ретраев/фолбэков), как ставить SLO компонентам, чтобы выдержать целевой пользовательский SLO.
- Статистическая корректность перцентилей: почему
avg(p99)неверно, какhistogram_quantileработает на агрегированных бакетах, ошибка интерполяции при неудачных границах бакетов, ограничения классических vs native/exponential histograms в Prometheus. - Дизайн error budget policy как организационного инструмента: кто владелец, что именно происходит при исчерпании, как избежать gaming (подкрутка SLO под факт), связь с release-процессом и postmortem-культурой.
- Тонкая настройка multi-burn-rate: выбор конкретных пар (rate, window), trade-off precision/recall/time-to-detect/reset-time, влияние low-traffic сервисов (мало событий → шумный SLI, нужны min-traffic guards или агрегация).
- SLI для не-request/response систем: батчи (freshness, coverage), стримы/очереди (lag, throughput, durability), асинхронные пайплайны — где «valid event» неочевиден.
- Учёт ретраев и идемпотентности в SLI: как не считать один пользовательский запрос за N, разница user-perceived vs server-side reliability.
- Связь SLO с capacity/autoscaling и нагрузочным тестированием: как latency-SLO задаёт точку насыщения и триггеры масштабирования.
- Инструменты SLO-as-code: Sloth, OpenSLO, Pyrra — генерация recording/alerting rules из декларативного SLO-спека, версионирование SLO в Git.