Skip to Content
ConventionsSLO и error budget

SLO и error budget

Правила, как определять Service Level Objectives (SLO) для сервиса, считать error budget и превращать его в конкретные alerting-правила. Инструментация (как писать метрики) — в observability. Эта страница отвечает на другой вопрос: какие 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), не 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.
  • 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 resource99% < 200ms
Read: list с простым фильтром99% < 500ms
Read: list с агрегациями99% < 1s
Write: create/update99% < 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 — метрика outbox_forwarder_lag_seconds как alert source.

SLO для фоновых pipeline’ов

HTTP-запросы видит пользователь, но часть критичного поведения системы — фоновая: outbox forwarder публикует события в Kafka, saga orchestrator догоняет зависшие шаги, consumer применяет изменения. У каждого из них — собственный SLO, иначе «все HTTP зелёные, а downstream не получает событий час» — не ловится вообще.

Outbox forwarder

Forwarder — единственная точка, превращающая запись в outbox в Kafka-сообщение. Если он отстал, cross-service eventual consistency превращается из миллисекунд в часы. SLO на forwarder — не опциональный, он обязателен для каждого сервиса, у которого есть outbox.

SLIФормулаЦелевое
Publish freshnessдоля событий, опубликованных < 5s после INSERT INTO outbox99% за 30 дней
Unpublished backlogoutbox_unpublished_rows (gauge, текущее число строк с offset_acked IS NULL)< 100 устойчиво; < 1000 spike-допустимо
Forwarder livenessheartbeat metric outbox_forwarder_last_tick_timestamp_secondstime() - ... < 30s

Alerting — три правила одновременно:

- alert: OutboxForwarderLagHigh expr: | histogram_quantile(0.99, sum by (le, service) (rate(outbox_forwarder_publish_duration_seconds_bucket[5m])) ) > 5 for: 5m labels: { severity: page, slo: outbox-freshness } annotations: summary: "Outbox p99 publish latency > 5s" runbook: "https://backend-handbook.kazmaps.dev/how-to/debug-outbox-lag" - alert: OutboxBacklogGrowing expr: outbox_unpublished_rows > 1000 for: 10m labels: { severity: page, slo: outbox-backlog } annotations: summary: "Outbox backlog > 1000 rows for 10m" runbook: "https://backend-handbook.kazmaps.dev/how-to/debug-outbox-lag" - alert: OutboxForwarderStalled expr: time() - outbox_forwarder_last_tick_timestamp_seconds > 120 for: 2m labels: { severity: page, slo: outbox-liveness } annotations: summary: "Forwarder hasn't ticked in 2m"

Связь с runbook’ом — ../how-to/debug-outbox-lag. SLO на forwarder’е записывается в docs/slo.md сервиса наравне с HTTP-SLO; при нарушении бюджет сгорает так же, как у HTTP-endpoint’а.

Saga orchestrator

Для сервисов, координирующих саги (см. ../patterns/saga), три SLO:

SLIФормулаЦелевое
Saga completion freshnessдоля саг, завершённых (status succeeded / compensated) < N часов от старта99% < 1ч для обычных саг, 99% < 24ч для cross-service delete
Stuck-saga rategauge saga_stuck_total{type} — саги в running или compensating дольше deadline0 устойчиво
Failed-compensation ratecounter saga_failed_compensation_total{type} — саги, застрявшие в failed_compensation0 (любое срабатывание — инцидент)

Watchdog, подметающий зависшие саги, — отдельный фоновый компонент; у него свой liveness-heartbeat наравне с forwarder’ом.

- alert: SagaFailedCompensation expr: increase(saga_failed_compensation_total[1h]) > 0 for: 0m labels: { severity: page, slo: saga-integrity } annotations: summary: "Saga compensation failed — требуется ручной разбор" runbook: "https://backend-handbook.kazmaps.dev/troubleshooting/saga-failed-compensation" - alert: SagaStuck expr: saga_stuck_total > 0 for: 15m labels: { severity: ticket, slo: saga-freshness }

saga_failed_compensation_total > 0всегда page. В этом состоянии данные, возможно, уже в inconsistent-состоянии между сервисами, и каждый час без разбора ухудшает ситуацию.

Kafka consumer

Consumer SLO — доля событий, обработанных в окне, плюс отдельно лаг и DLQ-прирост:

SLIФормулаЦелевое
Consumer freshnessдоля событий с end-to-end latency (от publish до ack) < N99% < 10s для notifications; SLO per consumer-group
Consumer lagkafka_consumer_lag_messages{group}< 1000 устойчиво
DLQ growthincrease(messages_poisoned_total[1h])0 устойчиво (любой прирост = ticket)

Alert на messages_poisoned_total любого прироста — даже одно сообщение в DLQ говорит, что handler не справляется и кто-то должен посмотреть. Не откладывай разбор DLQ.

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, а не шум.

Multi-window burn-rate и spike handling

Проблема стационарной формулы. Single-window alert err_rate[1h] > 14.4 * budget срабатывает либо поздно (длинный инцидент уже идёт час, пока окно заполнится), либо ложно (30-секундный spike при малом RPS раздувает rate на коротком окне). Один порог на одном окне — компромисс, проигрывающий в обоих направлениях.

Решение — multi-window multi-burn-rate (Google SRE Workbook, «The Site Reliability Workbook», глава Alerting on SLOs). Несколько окон одновременно; alert срабатывает только если все окна подтвердили burn.

Две группы alerts:

  • Fast burn (долгий инцидент, быстрое сгорание бюджета):
    • Short window 5m, long window 1h, burn-rate threshold 14.4 (2% бюджета за час).
    • Логика: err_rate[5m] > 14.4 * budget AND err_rate[1h] > 14.4 * budget.
    • Реакция: page on-call, severity SEV-2.
  • Slow burn (постепенная деградация):
    • Short window 30m, long window 6h, burn-rate threshold 6 (10% бюджета за сутки).
    • Логика: err_rate[30m] > 6 * budget AND err_rate[6h] > 6 * budget.
    • Реакция: ticket / warn, severity SEV-3.

Пример fast-burn alert’а для review-service (SLO 99.9% → budget 0.001):

- alert: ErrorBudgetFastBurn expr: | ( sum(rate(http_requests_total{code=~"5..",job="review-service"}[5m])) / sum(rate(http_requests_total{job="review-service"}[5m])) > (14.4 * 0.001) ) and ( sum(rate(http_requests_total{code=~"5..",job="review-service"}[1h])) / sum(rate(http_requests_total{job="review-service"}[1h])) > (14.4 * 0.001) ) for: 2m labels: { severity: page, team: backend, slo: review-availability } annotations: summary: "Fast error budget burn on review-service"

Короткое окно как reset-защита. Alert автоматически reset’ится, когда свежее окно очистилось (инцидент закончился — err_rate[5m] падает ниже порога за ~5 минут). Long-window alert без short-window компаньона — антипаттерн: он висит часами после того, как проблема уже решена, потому что err_rate[1h] помнит всё окно.

Cooldown в Alertmanager:

# alertmanager.yml route - match: { severity: page } repeat_interval: 30m - match: { severity: ticket } repeat_interval: 4h

Эталонная таблица (Google-style)

Consumed budgetShort windowLong windowBurn rateSeverity
2% за 1ч5 мин14.4page
5% за 6ч30 мин6ticket
10% за 3д1review

Третья строка — опционально для сервисов с жёстким SLO: отдельный low-severity alert для отслеживания стабильного, но заметного burn’а на недельной дистанции.

Fast burn и slow burn

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

AlertBurn rateОкна (short/long)Severityfor:
Fast burn14.45m / 1hpage2m
Slow burn630m / 6hticket15m
  • 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-db.outbox → kazmaps.review.review - **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.

См. также

Last updated on