Когда сервис ломается ночью, дежурный просыпается по алерту. Но какому? Если алерт срабатывает на каждую одиночную ошибку — команда быстро начинает их игнорировать. Если алерт срабатывает только когда сервис уже недоступен несколько часов — уже поздно.
Здесь разберём, как выстроить систему алертов вокруг SLO: что такое error budget, почему важны разные временные окна и как всё это реализовать в Python с FastAPI и Prometheus.
Что такое SLO и зачем он нужен
Раньше команды мониторили сервис по принципу «всё работает / всё сломалось». Такой подход не даёт ответа на главный вопрос: насколько хорошо сервис работает для пользователей прямо сейчас?
SLO (Service Level Objective) — это количественное обещание уровня обслуживания. Не «мы стараемся», а «99.9% запросов успешны на скользящем окне 30 дней».
Почему 99.9%, а не 100%? Потому что 100% недостижимо — деплои, сетевые сбои, железо. А главное: при 100% цели нет инструмента, чтобы принять решение «мы можем выкатить рискованное изменение или нет».
Error budget — это сколько ошибок допускает SLO. При цели 99.9% на 30-дневном окне бюджет ошибок составляет 0.1%, или примерно 43 минуты недоступности в месяц. Если бюджет почти исчерпан — команда переключается с разработки новых функций на стабилизацию. Если бюджет в норме — можно рисковать.
Как подключить метрики в FastAPI
Источник SLI-метрик в Python/FastAPI — библиотека prometheus-fastapi-instrumentator. Она автоматически регистрирует счётчик запросов http_requests_total и гистограмму http_request_duration_seconds с метками handler, method, status_code.
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator
app = FastAPI()
Instrumentator(
should_group_status_codes=False,
should_ignore_untemplated=True,
excluded_handlers=["/health/live", "/health/ready", "/metrics"],
).instrument(app).expose(app, endpoint="/metrics", include_in_schema=False)
Параметр should_group_status_codes=False важен: без него все 5xx попадут в одну метку и вы не сможете различить 500 и 503.
Для бизнес-метрик добавляйте счётчики и гистограммы через prometheus_client напрямую:
from prometheus_client import Counter, Histogram
ORDER_CREATED = Counter(
"order_created_total",
"Successful order creations",
["payment_method"],
)
ORDER_FAILED = Counter(
"order_failed_total",
"Failed order creations",
["reason"],
)
CHECKOUT_DURATION = Histogram(
"checkout_duration_seconds",
"End-to-end checkout latency",
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
)
Соглашение по именованию: snake_case, единица измерения в суффиксе (_seconds, _total, _bytes).
Важный момент: метка называется handler (значение — шаблон пути, например /orders/{order_id}), а не uri как в Micrometer/Spring. Перед написанием PromQL-запросов проверьте реальные имена меток через /metrics.
SLO для конкретных endpoint'ов
Не все запросы одинаково важны. Обычно выделяют критические endpoint'ы и для каждого задают два SLO: по доступности и по задержке.
Пример набора SLO для сервиса заказов:
| Endpoint | Доступность | Задержка |
|---|---|---|
POST /orders | 99.9% non-5xx | p95 < 500ms |
POST /payments | 99.95% non-5xx | p95 < 1s |
GET /orders/{id} | 99.95% non-5xx | p95 < 200ms |
GET /products/search | 99.5% non-5xx | p95 < 800ms |
SLI (Service Level Indicator) — это текущее измеренное значение. В PromQL для POST /orders:
# Доступность — доля non-5xx запросов
sum(rate(http_requests_total{handler="/orders",method="POST",status_code!~"5.."}[30d]))
/
sum(rate(http_requests_total{handler="/orders",method="POST"}[30d]))
# Задержка — 95-й перцентиль
histogram_quantile(0.95,
sum by (le) (rate(http_request_duration_seconds_bucket{handler="/orders",method="POST"}[30d]))
)
Для задержки всегда используйте перцентили (histogram_quantile), а не среднее. Среднее скрывает хвосты: 90% запросов могут быть быстрыми, а 10% медленными — среднее покажет «всё хорошо».
Multi-window burn rate: быстрый и медленный сигнал
Классическая ошибка — единственный алерт вида «error rate > 1% за последние 5 минут». Он не отличает реальный инцидент от кратковременного шума и не говорит, насколько быстро исчерпывается бюджет.
Подход из книги Google SRE Workbook — multi-window multi-burn-rate. Идея простая: смотрим на скорость сжигания бюджета на разных окнах.
Burn rate — во сколько раз быстрее нормы сжигается error budget:
burn_rate = (error_rate_в_окне) / (1 - SLO_цель)
Для SLO 99.9% (budget = 0.001):
- burn rate 14.4 на окне 1h означает, что за час сгорает 5% месячного бюджета — это инцидент;
- burn rate 6 на окне 6h означает медленную деградацию — нужно разобраться, но не срочно;
- burn rate ≤ 1 — нормальная скорость, ничего страшного.
groups:
- name: orders.slo
rules:
- alert: OrdersSloFastBurn
expr: |
(
sum(rate(http_requests_total{handler="/orders",method="POST",status_code=~"5.."}[1h]))
/
sum(rate(http_requests_total{handler="/orders",method="POST"}[1h]))
) > (14.4 * (1 - 0.999))
for: 2m
labels:
severity: critical
team: order-service
annotations:
summary: "Orders SLO fast burn — потенциальный отказ"
runbook: https://runbooks.internal/orders-slo-fast-burn
- alert: OrdersSloSlowBurn
expr: |
(
sum(rate(http_requests_total{handler="/orders",method="POST",status_code=~"5.."}[6h]))
/
sum(rate(http_requests_total{handler="/orders",method="POST"}[6h]))
) > (6 * (1 - 0.999))
for: 15m
labels:
severity: warning
team: order-service
annotations:
summary: "Orders SLO slow burn — деградация без отказа"
runbook: https://runbooks.internal/orders-slo-slow-burn
Fast burn будит дежурного немедленно. Slow burn создаёт задачу на следующий рабочий день. Поле for обязательно — без него единственный всплеск будет поднимать тревогу.
Error budget exhaustion: сигнал команде
Отдельно от burn-rate стоит настроить алерт на исчерпание самого бюджета за скользящие 30 дней. Это не «срочно чинить ночью», а сигнал: следующие недели команда фокусируется на стабильности.
# Сколько бюджета осталось (1 = весь свободен, 0 = исчерпан)
1 - (
(1 - sum(rate(http_requests_total{handler="/orders",method="POST",status_code!~"5.."}[30d]))
/
sum(rate(http_requests_total{handler="/orders",method="POST"}[30d])))
/
(1 - 0.999)
)
- alert: OrdersErrorBudgetExhausted
expr: <budget_remaining_expression> < 0.1
for: 1h
labels:
severity: warning
annotations:
summary: "Остался только 10% error budget — /orders"
description: |
Команда переключается с новых функций на надёжность.
Рискованные выкатки приостановлены до восстановления бюджета.
runbook: https://runbooks.internal/orders-error-budget
Алерты помимо SLO
SLO измеряет успешность для пользователей. Но сервис может деградировать по причинам, которые SLO поймает только когда будет поздно. Поэтому рядом с SLO-алертами настраивают отдельные категории:
Инфраструктура — когда сервис перегружен, но ещё справляется:
process_resident_memory_bytes > 1.5G— утечка памяти- event-loop lag > 100ms — блокирующий вызов внутри async-кода
uvicorn_workers_busy / uvicorn_workers_total > 0.9— workers заканчиваются
Домен — бизнес-логика ведёт себя неожиданно:
PRODUCT_CHECKOUT_BLOCKED = Counter(
"product_checkout_blocked_total",
"Checkout blocked due to inventory hold failure",
["reason"],
)
- alert: ProductCheckoutBlockedHigh
expr: sum(rate(product_checkout_blocked_total[5m])) > 5
for: 5m
labels:
severity: warning
annotations:
summary: "Высокий процент заблокированных заказов — проверь инвентарь"
runbook: https://runbooks.internal/product-checkout-blocked
Устойчивость — состояние circuit breaker'а. Если он открыт, запросы идут на запасной путь — SLO ещё в норме, но основная зависимость недоступна, проблема назревает.
Kafka consumer lag — если сервис потребляет события, отставание очереди напрямую влияет на пользователей.
Частые ошибки
Алерт на каждую ошибку в логах. Это главный путь к усталости от алертов: команда начинает их игнорировать. Агрегируйте по типу и используйте burn-rate алерты.
SLO на 100%. Нечем оперировать: любая ошибка формально нарушает цель. Используйте 99.9% (43 минуты в месяц) или 99.95% для критических сервисов.
Алерт без runbook. Дежурный получает уведомление и не знает, что делать. Каждый алерт должен содержать ссылку на инструкцию: что проверить, кому звонить, как откатить.
Только одно временное окно. Burn-rate только по 30 дням реагирует слишком медленно на реальный инцидент. Нужны короткое окно (1h) для быстрого сигнала и длинное (6h) для медленной деградации.
Задержка по среднему. avg(http_request_duration_seconds) скрывает медленные запросы. Только перцентили показывают реальный опыт пользователя.
Коротко
- SLO — количественная цель (например, 99.9% non-5xx на 30-дневном окне). Error budget — допустимое количество ошибок, инструмент приоритизации.
- В Python/FastAPI метрики подключаются через
prometheus-fastapi-instrumentator; бизнес-метрики — черезprometheus_clientнапрямую. - Метка запросов —
handler(неuriкак в Spring). Имена метрик — snake_case с суффиксом единицы (_total,_seconds). - Multi-window burn rate: fast burn (1h, rate > 14.4) — критический алерт; slow burn (6h, rate > 6) — предупреждение. Поле
forобязательно. - Задержку измеряют перцентилями (
histogram_quantile(0.95, ...)), а не средним. - SLO-алерты — не единственные. Рядом нужны алерты инфраструктуры, домена, устойчивости и задержки очередей.
- Каждый алерт содержит ссылку на runbook. Без инструкции алерт бесполезен.
Что почитать дальше
- Метрики в Python —
prometheus-client, гистограммы, бизнес-счётчики, кардинальность. - Tracing в Python — manual span через context manager, sampling, связь трейсов с алертами.
- Health checks в Python — почему liveness/readiness не заменяют SLO.
- Logging в Python — structlog, contextvars, связка
trace_id→ лог → алерт.