Когда сервис ломается ночью, дежурный просыпается по алерту. Но какому? Если алерт срабатывает на каждую одиночную ошибку — команда быстро начинает их игнорировать. Если алерт срабатывает только когда сервис уже недоступен несколько часов — уже поздно.
Здесь разберём, как выстроить систему алертов вокруг 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 /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!~"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 минут». Он не отличает реальный инцидент от кратковременного шума и не говорит, насколько быстро исчерпывается бюджет.
Короткое окно ловит инцидент за минуты, длинное медленную деградацию; пороги считают от бюджета ошибок 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→ лог → алерт.