Модуль: 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_consumed

Error 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:

SeverityLong windowShort windowBurn rate% бюджета за long window
Page (срочно)1 час5 мин14.42%
Page6 часов30 мин65%
Ticket (не срочно)1 день2 часа310%
Ticket3 дня6 часов110%

Зачем два окна (long + short) на алерт:

  • Long window даёт высокий precision: проблема действительно значима (потрачено заметно бюджета).
  • Short window даёт быстрый reset / restore: алерт быстро гаснет, когда инцидент закончился, а не висит ещё час. Short также подтверждает, что проблема всё ещё происходит сейчас, а не «выгорела» в начале long-окна.

Алерт срабатывает, когда оба окна превышают порог burn rate:

burn_rate(long_window)  > threshold
AND
burn_rate(short_window) > threshold

Short-окно обычно = 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: практический процесс#

  1. Определите user journeys (критичные сценарии): «открыть ленту», «оформить заказ», «получить ответ API».
  2. Для каждого journey выберите 1–3 SLI (обычно availability + latency; для данных — freshness/correctness).
  3. Сформулируйте good/valid в формате событий, привязанных к точке измерения.
  4. Поставьте достижимое SLO: начните с измерения текущего поведения за 4 недели, поставьте чуть ниже фактического p, ужесточайте итеративно.
  5. Согласуйте error budget policy с продуктом и руководством.
  6. Настройте 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.