← назад к разделу

Когда сервис ломается ночью, дежурный просыпается по алерту. Но какому? Если алерт срабатывает на каждую одиночную ошибку — команда быстро начинает их игнорировать. Если алерт срабатывает только когда сервис уже недоступен несколько часов — уже поздно.

Здесь разберём, как выстроить систему алертов вокруг 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 /orders99.9% non-5xxp95 < 500ms
POST /payments99.95% non-5xxp95 < 1s
GET /orders/{id}99.95% non-5xxp95 < 200ms
GET /products/search99.5% non-5xxp95 < 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 → лог → алерт.