SLO и error budget¶
Правила, как определять Service Level Objectives (SLO) для сервиса,
считать error budget и превращать его в конкретные alerting-правила.
Инструментация (как писать метрики) — в
observability.md. Эта страница отвечает на другой
вопрос: какие SLO выбрать, что делать, когда бюджет сжигается, и как
перевести это в Prometheus alert rules.
Содержание¶
- Зачем SLO
- SLI vs SLO vs SLA
- Какие SLI выбрать
- Availability SLI
- Latency SLI
- Freshness SLI
- Error budget
- Multi-window multi-burn-rate alerts
- Fast burn и slow burn
- Документирование SLO
- Ревью SLO
- Что не делать
- См. также
Зачем 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:
- Availability — доля успешных запросов.
- Latency — доля запросов, уложившихся в целевой таргет по времени.
Для async-пайплайнов (Kafka consumer, background worker) добавляется третий:
- 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. Неincrease—rateстабильнее на границах 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 дней:
Что это значит в абсолютных числах:
- При 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¶
Если SLO 99.9%, budget = 0.001. При текущем error rate 0.01:
Это значит: при таком темпе бюджет сгорит за окно / 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.
См. также¶
observability.md— инструментовка, метрики, labels, histogram vs summary.../how-to/add-metric-and-alert.md— пошаговый рецепт добавления метрики и alert rule.../patterns/outbox.md— метрики outbox forwarder'а, freshness SLI на них.../patterns/retry-and-circuit-breaker.md— как считать SLI при graceful degradation (stale cache — этоsuccessfulилиerror?).