Перейти к содержанию

SLO и error budget

Правила, как определять Service Level Objectives (SLO) для сервиса, считать error budget и превращать его в конкретные alerting-правила. Инструментация (как писать метрики) — в observability.md. Эта страница отвечает на другой вопрос: какие SLO выбрать, что делать, когда бюджет сжигается, и как перевести это в Prometheus alert rules.

Содержание

Зачем SLO

Без SLO команда спорит, что считать «упавшим» сервисом. SLO даёт цифровое определение — 99.9% запросов за 30 дней должны ответить успешно и быстрее 500ms. Всё, что хуже — бюджет сгорает, команда останавливает feature-work и разбирается.

Правила:

  • SLO ставятся для user-facing endpoint'ов сервиса, которые видит клиент через gateway. Для internal endpoint'ов — отдельно или не ставятся, зависит от того, есть ли у них клиенты с SLA.
  • SLO устанавливает owner сервиса (см. ../onboarding/04-who-owns-what.md), не SRE извне и не кто-то снаружи.
  • SLO < 100%. Цель «ноль ошибок» бессмысленна: каждый 9-ка в SLO удорожает его экспоненциально, а user'ская сеть и так добавляет свои ошибки. Начинай с 99.9%, повышай только если есть явная бизнес-необходимость.

SLI vs SLO vs SLA

Три слова постоянно путают. Различия:

Термин Что Пример
SLI (Service Level Indicator) Метрика — что измеряем «доля успешных HTTP-запросов к /v1/reviews»
SLO (Service Level Objective) Целевое значение SLI за окно «99.9% успешных за 30 дней»
SLA (Service Level Agreement) Контрактное обязательство с внешними последствиями «99.5% в месяц; штраф за нарушение»

У нас внешних SLA нет (мы не продаём API клиентам с контрактами). Мы оперируем SLI и SLO. SLA — термин, который приходит извне, не используй его без контекста.

Какие SLI выбрать

На один endpoint обычно два SLI:

  1. Availability — доля успешных запросов.
  2. Latency — доля запросов, уложившихся в целевой таргет по времени.

Для async-пайплайнов (Kafka consumer, background worker) добавляется третий:

  1. Freshness — насколько свежи обработанные данные.

Не плоди 10 SLI на endpoint: чем больше, тем менее каждый actionable. Один availability + один latency = 95% нужной сигнализации.

Availability SLI

Формула: successful_requests / total_requests за окно.

«Успешным» считаются ответы с HTTP-кодом не-5xx (4xx — это ошибка клиента, а не сервиса):

(
  sum(rate(http_requests_total{service="review",route="/v1/reviews",code!~"5.."}[30d]))
) /
sum(rate(http_requests_total{service="review",route="/v1/reviews"}[30d]))

Важные детали:

  • code!~"5.." — не status="success". Тебе важно автоматическое попадание любого 5xx, не руками размеченного.
  • Группировка по route на паттерне (/v1/reviews/{id}), не по реальному URL — см. observability.md.
  • rate(...[30d]) — окно SLO. Не increaserate стабильнее на границах scrape.

Стандартные таргеты:

Тип endpoint'а Availability SLO
Критичный user-facing (auth, feed) 99.95%
Обычный user-facing (profile, reviews) 99.9%
Non-critical feature (search-suggest) 99.5%
Internal endpoint с несколькими клиентами 99.9%
Background worker (outbox forwarder, consumer) свой SLI, см. §Freshness

«Стандартный» — default для нового endpoint'а. Если команда считает, что бизнес требует жёстче/мягче — обоснуй в документе SLO (§Документирование).

Latency SLI

Формула: доля запросов, уложившихся в T.

(
  sum(rate(http_request_duration_seconds_bucket{service="review",route="/v1/reviews",le="0.5"}[30d]))
) /
sum(rate(http_request_duration_seconds_count{service="review",route="/v1/reviews"}[30d]))

Это уже корректная формула на histogram'е — считает, сколько попало в bucket le=0.5 (≤ 500ms).

Стандартные таргеты:

Тип endpoint'а Latency SLO
Read: GET single resource 99% < 200ms
Read: list с простым фильтром 99% < 500ms
Read: list с агрегациями 99% < 1s
Write: create/update 99% < 800ms
Upload (media) 99% < 5s

«99%» — не «p99». p99 < 500ms и «99% запросов < 500ms» — это одно и то же, но SLO-форма стандартная: доля, уложившаяся в таргет, не «95-й перцентиль такой-то».

Не делай SLO «p99 < 500ms and p50 < 100ms» — две цели → два бюджета → две причины для страницы. Один SLI, одно число.

Freshness SLI

Для async-пайплайнов (Kafka consumer, outbox forwarder) доля «быстрых» запросов не описывает реальность — пользователь не видит end-to-end задержку, он видит, что данные до сих пор не обновились.

Формула для outbox: доля событий, опубликованных в Kafka < N секунд после записи в outbox.

(
  sum(rate(outbox_forwarder_publish_duration_seconds_bucket{service="review",le="5"}[30d]))
) /
sum(rate(outbox_forwarder_publish_duration_seconds_count{service="review"}[30d]))

Для consumer — доля событий, обработанных < N секунд после публикации в Kafka:

(
  sum(rate(consumer_lag_seconds_bucket{service="notification",consumer_group="...",le="10"}[30d]))
) /
sum(rate(consumer_lag_seconds_count{service="notification",consumer_group="..."}[30d]))

Типовые таргеты:

Пайплайн Freshness SLO
Outbox → Kafka (publish) 99% < 5s
Kafka consumer → side effect (notification, search-index update) 99% < 10s
Batch aggregation (daily stats) 99% < 1 час после cutoff

См. также ../patterns/outbox.md — метрика outbox_forwarder_lag_seconds как alert source.

Error budget

Error budget = 1 - SLO за окно. Это сколько можно облажаться, не нарушив SLO.

Для SLO 99.9% за 30 дней:

budget = 1 - 0.999 = 0.001 = 0.1%

Что это значит в абсолютных числах:

  • При 100 RPS: 30 дней = 259,200,000 запросов. 0.1% = 259,200 ошибок разрешено за окно.
  • При 10 RPS: 25,920,000 запросов. 0.1% = 25,920 ошибок.
  • При 1 RPS: 2,592,000 запросов. 0.1% = 2,592 ошибок.

Переводы в «сколько времени можно лежать на 100%»:

SLO Допустимое downtime за 30 дней
99% 7h 12m
99.5% 3h 36m
99.9% 43m 12s
99.95% 21m 36s
99.99% 4m 19s

Правила обращения с бюджетом:

  • Бюджет сгорел быстрее положенного → останавливаем feature-work. Сервис-owner выкатывает только hotfix'ы, пока бюджет не восстановится. Это не формальная санкция — это напоминание, что стабильность сейчас важнее нового функционала.
  • Бюджет восстанавливается sliding window'ом. За 30-дневное окно старые ошибки «выпадают» каждую секунду; новые ошибки съедают бюджет. Не «обнуляется 1-го числа».
  • Не трать остаток бюджета специально. «У нас бюджет на этой неделе ещё 0.05%, можно не бояться плохого релиза» — мышление ведёт к нестабильности. SLO — потолок, не обязательство потратить.

Multi-window multi-burn-rate alerts

Один alert на «бюджет сгорел» бесполезен — к моменту срабатывания инцидент уже час как длится. Рабочая схема — два alert'а с разными окнами и порогами, комбинированные AND.

Формула burn rate

burn_rate = (error_rate_в_окне) / (1 - SLO)

Если SLO 99.9%, budget = 0.001. При текущем error rate 0.01:

burn_rate = 0.01 / 0.001 = 10

Это значит: при таком темпе бюджет сгорит за окно / 10. Окно 30 дней → за 3 дня бюджет на ноль.

Таблица действий:

Burn rate Что значит Реакция
14.4 Сгорит за 2 часа Page немедленно
6 Сгорит за 5 часов Page, если длится > короткого окна
1 Ровно по бюджету Штатный режим
< 1 Бюджет растёт Ничего не делай

14.4 — стандартный порог «page сразу» из SRE Workbook: даже короткий всплеск за 5 минут при этом rate сожрёт 2% бюджета.

Шаблон Prometheus alert rule

Fast-burn alert (высокий rate на коротком окне → page):

- alert: ReviewAvailabilityFastBurn
  expr: |
    (
      sum(rate(http_requests_total{service="review",route="/v1/reviews",code=~"5.."}[5m]))
      /
      sum(rate(http_requests_total{service="review",route="/v1/reviews"}[5m]))
    ) > (14.4 * 0.001)
    and
    (
      sum(rate(http_requests_total{service="review",route="/v1/reviews",code=~"5.."}[1h]))
      /
      sum(rate(http_requests_total{service="review",route="/v1/reviews"}[1h]))
    ) > (14.4 * 0.001)
  for: 2m
  labels:
    severity: page
    service: review
    slo: availability
  annotations:
    summary: "Review availability SLO fast burn"
    description: "Error rate {{ $value | humanizePercentage }}, burning budget 14.4x normal rate."
    runbook: "https://backend-handbook.kazmaps.dev/troubleshooting/..."

Slow-burn alert (низкий rate на длинном окне → ticket):

- alert: ReviewAvailabilitySlowBurn
  expr: |
    (
      sum(rate(http_requests_total{service="review",route="/v1/reviews",code=~"5.."}[30m]))
      /
      sum(rate(http_requests_total{service="review",route="/v1/reviews"}[30m]))
    ) > (6 * 0.001)
    and
    (
      sum(rate(http_requests_total{service="review",route="/v1/reviews",code=~"5.."}[6h]))
      /
      sum(rate(http_requests_total{service="review",route="/v1/reviews"}[6h]))
    ) > (6 * 0.001)
  for: 15m
  labels:
    severity: ticket
    service: review
    slo: availability
  annotations:
    summary: "Review availability SLO slow burn"
    description: "Error rate {{ $value | humanizePercentage }}, burning budget 6x normal rate."

Почему два окна в AND:

  • Короткое окно — быстро реагирует на реальные проблемы.
  • Длинное окно — отсекает одноразовые спайки (CI deploy, 30 секунд 502 из-за rollout).

Обе условия вместе → стабильный ускоренный burn, а не шум.

Fast burn и slow burn

Стандартная пара для одного SLI:

Alert Burn rate Окна (short/long) Severity for:
Fast burn 14.4 5m / 1h page 2m
Slow burn 6 30m / 6h ticket 15m
  • Page — немедленное внимание, on-call отвечает <15 мин.
  • Ticket — в рабочее время следующего дня.

Для latency SLI формула та же, только числитель считает «запросы медленнее таргета»:

sum(rate(http_request_duration_seconds_count{service="review",route="/v1/reviews"}[5m]))
-
sum(rate(http_request_duration_seconds_bucket{service="review",route="/v1/reviews",le="0.5"}[5m]))

Делишь на total — получаешь долю «медленных» запросов. Дальше та же формула burn rate.

Документирование SLO

Каждый сервис держит файл docs/slo.md в своём репо. Формат:

# Review Service — SLOs

Window: 30 days, rolling.

## Availability

- **Endpoint:** `/v1/reviews` (GET, POST, PUT, DELETE)
- **SLI:** non-5xx responses / total
- **SLO:** 99.9%
- **Alerts:** fast burn (14.4x, 5m/1h), slow burn (6x, 30m/6h)
- **Dashboard:** <grafana-link>

## Latency

- **Endpoint:** `/v1/reviews` (GET list)
- **SLI:** requests ≤ 500ms / total
- **SLO:** 99%
- **Alerts:** fast burn, slow burn

## Freshness (outbox)

- **Pipeline:** review.outbox → kazmaps.review.review.created
- **SLI:** publishes < 5s from row insert
- **SLO:** 99%
- **Alert:** `outbox_forwarder_lag_seconds > 60 for 5m`

Живёт в сервис-репо, не в handbook. Handbook описывает формат SLO, значения per-сервис — нет.

Ревью SLO

Пересматривай SLO раз в квартал или после крупных изменений архитектуры (migration на новую БД, переключение с sync на async pipeline):

  • SLO выполняется с запасом (error rate << бюджета) третий квартал подряд → подними SLO на 0.05% (например, 99.9% → 99.95%). Это делает alert'ы острее.
  • SLO постоянно нарушается → либо у команды реальная проблема со стабильностью, либо SLO нереалистичный. Разбираться, какой из двух. Снижение SLO «чтобы не гореть» без root-cause фикса — плохо.
  • Бизнес-требования изменились (critical endpoint стал less-critical) → SLO пересматривается.

Commit в slo.md + короткая запись в changelog: «что изменилось, почему».

Что не делать

  • SLO на каждый внутренний endpoint. /metrics, /healthz, /internal/debug — не user-facing, SLO не нужны. Alert на факт доступности — да, SLO — нет.
  • SLI «количество ошибок». Абсолютное число не нормализовано на трафик: 100 ошибок при 10k запросов — норма, при 100 запросах — провал. Всегда доля.
  • SLO 100%. Недостижимо и бесполезно: сетевой RTT и сбой на клиентской стороне добавят ошибок, ты получишь constant burn.
  • SLO на неверной метрике. http_requests_total{code=~"5.."} не включает таймауты, где connection reset — считай отдельно или уверься, что middleware превращает их в 503.
  • Alert на > 0 errors. Любая сетка даст вспышку на одном запросе ночью. Burn rate на окне — единственный масштабируемый подход.
  • Смотреть только p50 / p99 вместо bucket-based SLI. p99 нельзя надёжно усреднить между инстансами — PromQL avg(histogram_quantile(...)) даёт неверный результат. Правильно — histogram_quantile(...) с sum(rate(bucket)) глобально, либо SLI через bucket'ы.
  • Добавлять каждый edge-case в SLO. Один SLO per endpoint + один per critical pipeline. 20 SLO на сервис = ни одного actionable.

См. также