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

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

Здесь разберём, как выстроить систему алертов вокруг 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.

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}), а метка кода ответа — status. Перед написанием 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!~"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 минут». Он не отличает реальный инцидент от кратковременного шума и не говорит, насколько быстро исчерпывается бюджет.

окно 1 час доля 5xx за час больше 1,44 % (burn 14.4) критический алерт, for 2m окно 6 часов доля 5xx за шесть часов больше 0,6 % (burn 6) предупреждение, for 15m

Короткое окно ловит инцидент за минуты, длинное медленную деградацию; пороги считают от бюджета ошибок SLO 99,9 процента.

Подход из книги 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 означает, что за час сгорает 2% месячного бюджета, а за сутки такой темп съест почти половину — это инцидент;
  • burn rate 6 на окне 6h означает, что за шесть часов ушло 5% бюджета — медленная деградация, нужно разобраться, но не срочно;
  • burn rate ≤ 1 — нормальная скорость, ничего страшного.
groups:
  - name: orders.slo
    rules:
      - alert: OrdersSloFastBurn
        expr: |
          (
            sum(rate(http_requests_total{handler="/orders",method="POST",status=~"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=~"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!~"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 — утечка памяти
  • лаг цикла событий > 100ms (своя Gauge, которую фоновая задача меряет по задержке asyncio.sleep) — блокирующий вызов внутри async-кода
  • db_pool_active / db_pool_size > 0.9 — пул соединений с базой исчерпан (gauge из статьи про метрики)

Домен — бизнес-логика ведёт себя неожиданно:

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) скрывает медленные запросы. Только перцентили показывают реальный опыт пользователя.

Дополнительно: при первом чтении можно пропустить

Глубже: низкий трафик ломает этот рецептрасширенное

Всё, что посчитано выше, держится на предположении, что запросов много. Как только их мало, скорость сжигания начинает врать, и это самая частая причина, по которой команды выключают такие тревоги через месяц.

Арифметика простая. Эндпоинт получает один запрос в десять секунд, 360 в час. Одна ошибка это 0,28 процента, то есть скорость сжигания 2,8 при норме 1; шесть ошибок за час дают 1,7 процента и будят дежурного как «быстрое сжигание», хотя шесть ошибок за час у ночного эндпоинта могут быть одним клиентом с битым запросом. Поле for здесь не спасает: доля держится весь час.

Что делают. Удлиняют окна: для редкого трафика короткое окно это шесть часов, длинное трое суток, и порог считают заново под них. Ставят нижний предел на число запросов: к выражению тревоги добавляют условие and sum(rate(http_requests_total{handler="/orders"}[1h])) * 3600 > 1000, пока запросов меньше, тревога молчит, а защиту даёт внешняя проба. Объединяют редкие эндпоинты в один SLO по группе (handler=~"/orders.*"), где статистики хватает. И для совсем редких операций заменяют долю на событие: одна ошибка в ночной выгрузке это тревога сама по себе, без процентов.

Проверить, что рецепт применим, можно до его внедрения: если sum(rate(...[1h])) * 3600 на эндпоинте меньше нескольких тысяч, тревоги по сжиганию бюджета на часовом окне не заведутся.

Глубже: SLO для потребителя очереди: свежесть вместо кода ответарасширенное

У потребителя Kafka и у relay нет кода ответа, и RED к ним не приложить, а SLO нужен не меньше: пользователь видит проекцию, собранную из событий, и ждёт письмо, отправленное воркером.

Индикатор здесь свежесть: доля событий, обработанных не позже чем через N секунд после появления. Считают её гистограммой event_processing_lag_seconds, куда потребитель пишет разницу между временем события (occurredAt в теле, не временем приёма) и моментом завершения обработки; histogram_quantile и доля бакета le="30" дают и p95, и SLI «95 процентов событий за 30 секунд». Для outbox свежесть это возраст самой старой неотправленной строки, одна Gauge.

Цель формулируют так же, как для HTTP: «99 процентов событий обработаны за 30 секунд на 30-дневном окне», бюджет это оставшийся процент, тревоги по сжиганию с теми же окнами и порогами. Отличие одно: во время остановки потребителя событий не приходит вовсе, доля не растёт, и свежесть надо дополнить тревогой «отставание растёт пять минут подряд», иначе мёртвый потребитель с точки зрения SLO безупречен.

Рядом, вне SLO, стоят счётчики повторов и строк в DLQ: по ним видно, что потребитель живёт, но каждое третье событие проходит со второго раза.

Коротко

  • SLO — количественная цель (например, 99.9% non-5xx на 30-дневном окне). Error budget — допустимое количество ошибок, инструмент приоритизации.
  • В Python/FastAPI метрики подключаются через prometheus-fastapi-instrumentator; бизнес-метрики — через prometheus_client напрямую.
  • Метки запросов — handler и status, не uri и не status_code. Имена метрик — 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. Без инструкции алерт бесполезен.
  • На редком трафике сжигание бюджета врёт: удлиняют окна, добавляют нижний предел числа запросов, объединяют эндпоинты; у потребителей очередей индикатор — свежесть по occurredAt, а мёртвого потребителя ловит тревога на рост отставания.

Что почитать дальше

  • Метрики в Python — prometheus-client, гистограммы, бизнес-счётчики, кардинальность.
  • Tracing в Python — manual span через context manager, sampling, связь трейсов с алертами.
  • Health checks в Python — почему liveness/readiness не заменяют SLO.
  • Logging в Python — structlog, contextvars, связка trace_id → лог → алерт.