Мониторинг за пределами Grafana: как мы построили полноценную observability-платформу для 200 микросервисов

Ситуация: дашборды есть, понимания нет

Маркетплейс «ТоварыВсем» обратился к нам в itfresh.ru с типичной жалобой: «У нас 47 дашбордов в Grafana, 120 алертов в Alertmanager, но каждый инцидент — это хаос на 2-3 часа. Мы видим, что CPU вырос, но не знаем почему и какой сервис виноват».

Архитектура — 200 микросервисов на Kubernetes (3 кластера: prod, staging, dev). Стек мониторинга формально был: Prometheus, Grafana, Alertmanager, ELK. По факту:

  • Метрики — 200 сервисов экспортируют стандартные метрики Go/Java runtime, но бизнес-метрик нет. Никто не знает, сколько заказов в секунду обрабатывается прямо сейчас.
  • Логи — неструктурированный текст в Elasticsearch, объём 800 GB/день. Поиск по логам занимает 30-90 секунд. Корреляции между сервисами нет — trace_id не передаётся.
  • Алерты — 120 правил, из них 80% срабатывают ложно. Команда научилась игнорировать Telegram-канал с алертами.
  • Дежурства — неформальные, «кто первый увидел — тот и чинит». Результат: один senior-инженер затыкает 90% инцидентов, остальные не знают систему достаточно глубоко.

Четыре золотых сигнала как фундамент

Мы начали с концепции Four Golden Signals (из книги Google SRE). Вместо мониторинга «всего подряд» сфокусировались на четырёх метриках для каждого сервиса:

  • Latency — время ответа (отдельно для успешных и ошибочных запросов)
  • Traffic — количество запросов в секунду
  • Errors — процент ошибок (5xx, таймауты, бизнес-ошибки)
  • Saturation — насколько загружен ресурс (CPU, память, очередь запросов)

Для унификации мы написали middleware-библиотеку, которую подключил каждый сервис:

# prometheus_middleware.py — универсальный middleware для FastAPI
from prometheus_client import Histogram, Counter, Gauge
import time

REQUEST_LATENCY = Histogram(
    'http_request_duration_seconds',
    'Request latency in seconds',
    ['service', 'method', 'endpoint', 'status_code'],
    buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
)

REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['service', 'method', 'endpoint', 'status_code']
)

IN_PROGRESS = Gauge(
    'http_requests_in_progress',
    'Requests currently being processed',
    ['service']
)

async def metrics_middleware(request, call_next):
    service_name = "order-service"  # из переменной окружения
    endpoint = request.url.path
    method = request.method

    IN_PROGRESS.labels(service=service_name).inc()
    start_time = time.perf_counter()

    try:
        response = await call_next(request)
        status = str(response.status_code)
    except Exception as e:
        status = "500"
        raise
    finally:
        duration = time.perf_counter() - start_time
        REQUEST_LATENCY.labels(
            service=service_name,
            method=method,
            endpoint=endpoint,
            status_code=status
        ).observe(duration)
        REQUEST_COUNT.labels(
            service=service_name,
            method=method,
            endpoint=endpoint,
            status_code=status
        ).inc()
        IN_PROGRESS.labels(service=service_name).dec()

    return response

Аналогичные библиотеки сделали для Go (через gRPC interceptor) и Java (через Spring WebFilter). За две недели 200 сервисов начали отдавать одинаковые метрики в одинаковом формате.

SLO, SLI и error budgets: переводим мониторинг на язык бизнеса

Следующий шаг — определить SLO (Service Level Objectives) для ключевых пользовательских путей. Мы выделили 8 критических сценариев и для каждого зафиксировали SLI (Service Level Indicator) и целевой SLO:

СценарийSLISLO (30 дней)
Поиск товараLatency p99 < 500 мс99.5%
Оформление заказаSuccess rate (не 5xx)99.9%
ОплатаSuccess rate + latency p99 < 3 с99.95%
Страница товараLatency p95 < 200 мс99.5%
Личный кабинетAvailability99.0%

Error budget — это допустимый «запас ошибок». Если SLO = 99.9% за 30 дней, то error budget = 0.1% = 43 минуты простоя. Когда бюджет исчерпан — замораживаем релизы и занимаемся reliability.

Конфигурация SLO-мониторинга в Prometheus через recording rules:

# slo-rules.yaml — recording rules для SLO мониторинга
groups:
  - name: slo_order_checkout
    interval: 30s
    rules:
      # SLI: доля успешных оформлений заказа
      - record: sli:order_checkout:success_rate_5m
        expr: |
          sum(rate(http_requests_total{service="checkout-service",
              endpoint="/api/v1/orders",method="POST",
              status_code=~"2.."}[5m]))
          /
          sum(rate(http_requests_total{service="checkout-service",
              endpoint="/api/v1/orders",method="POST"}[5m]))

      # Error budget: сколько осталось за 30 дней
      - record: slo:order_checkout:error_budget_remaining
        expr: |
          1 - (
            (1 - sli:order_checkout:success_rate_30d)
            /
            (1 - 0.999)
          )

      # Burn rate: скорость расхода error budget
      - record: slo:order_checkout:burn_rate_1h
        expr: |
          (1 - sli:order_checkout:success_rate_1h)
          /
          (1 - 0.999)

      # Алерт: burn rate слишком высок
      - alert: SLOBurnRateHigh
        expr: slo:order_checkout:burn_rate_1h > 14.4
        for: 5m
        labels:
          severity: critical
          team: checkout
        annotations:
          summary: "Error budget сгорает в 14x быстрее нормы"
          description: "При текущей скорости error budget кончится через {{ $value | humanizeDuration }}"

Результат: вместо 120 абстрактных алертов — 8 SLO-алертов, которые говорят на языке бизнеса: «заказы ломаются быстрее допустимого». Ложных срабатываний стало на 94% меньше.

Structured logging и distributed tracing

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

Мы внедрили два изменения одновременно:

1. Structured logging в JSON. Все сервисы перешли на единый формат:

// Пример структурированного лога (Go, zerolog)
log.Info().
    Str("trace_id", span.SpanContext().TraceID().String()).
    Str("span_id", span.SpanContext().SpanID().String()).
    Str("service", "order-service").
    Str("user_id", req.UserID).
    Str("order_id", orderID).
    Float64("amount", req.Amount).
    Str("currency", "RUB").
    Dur("duration_ms", time.Since(start)).
    Msg("order created successfully")

// Выход:
// {"level":"info","trace_id":"abc123def456","span_id":"789ghi",
//  "service":"order-service","user_id":"u-4521","order_id":"ord-98712",
//  "amount":15490.0,"currency":"RUB","duration_ms":42,
//  "message":"order created successfully","timestamp":"2026-03-15T14:23:01Z"}

2. Distributed tracing через OpenTelemetry. Каждый запрос получает уникальный trace_id на входе (на Ingress Controller) и передаёт его через все сервисы. В Grafana Tempo мы видим полный waterfall вызовов:

# docker-compose для Tempo (бэкенд трейсинга)
services:
  tempo:
    image: grafana/tempo:2.4.1
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
      - tempo-data:/var/tempo
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "3200:3200"   # Tempo API

# tempo.yaml
stream_over_http_enabled: true
server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: "0.0.0.0:4317"
        http:
          endpoint: "0.0.0.0:4318"

storage:
  trace:
    backend: s3
    s3:
      bucket: tovary-vsem-traces
      endpoint: s3.ru-central1.storage.yandexcloud.net
      region: ru-central1
    wal:
      path: /var/tempo/wal
    block:
      bloom_filter_false_positive: 0.05

metrics_generator:
  registry:
    external_labels:
      source: tempo
      cluster: production
  storage:
    path: /var/tempo/generator/wal
    remote_write:
      - url: http://prometheus:9090/api/v1/write

Ключевое: в Grafana мы связали логи, метрики и трейсы через trace_id. Клик по трейсу показывает логи всех сервисов в хронологическом порядке, а клик по алерту открывает трейсы с ошибками за нужный период.

Кастомные бизнес-метрики и anomaly detection

Технические метрики показывают, что «сервер жив». Бизнес-метрики показывают, что «деньги приходят». Мы внедрили второй слой:

  • orders_per_minute — количество оформленных заказов в минуту
  • payment_success_rate — процент успешных оплат по провайдерам
  • cart_abandonment_rate — доля брошенных корзин
  • search_zero_results_rate — доля поисковых запросов без результатов
  • delivery_estimation_accuracy — точность прогноза доставки

Для аномалий мы настроили z-score детектирование в Prometheus:

# Anomaly detection: отклонение текущего значения от среднего за 7 дней
# с учётом дня недели и часа
groups:
  - name: business_anomalies
    rules:
      - alert: OrderRateAnomaly
        expr: |
          (
            sum(rate(orders_total[5m])) * 300
            -
            avg_over_time(
              (sum(rate(orders_total[5m])) * 300)
              [7d:5m]
            )
          )
          /
          stddev_over_time(
            (sum(rate(orders_total[5m])) * 300)
            [7d:5m]
          )
          < -3
        for: 10m
        labels:
          severity: warning
          team: business
        annotations:
          summary: "Аномальное падение заказов"
          description: "Текущий rate заказов на {{ $value }} стандартных отклонений ниже нормы для этого времени"

Этот алерт спас нас дважды: один раз поймал сломанную корзину (фронтенд обновили, а один API endpoint поменял формат), второй раз — проблему с платёжным провайдером, который молча отклонял карты определённого банка.

On-call ротация и incident management

Мониторинг без процесса реагирования — просто красивые графики. Мы выстроили полный incident management pipeline:

1. On-call ротация. Настроили PagerDuty с недельными сменами, два уровня эскалации:

  • Level 1 — дежурный инженер (ответ за 5 минут, 24/7)
  • Level 2 — senior/team lead (если L1 не ответил за 15 минут)
  • Level 3 — CTO (если SLO нарушено более 30 минут)

2. Severity levels. Чёткая градация вместо субъективных оценок:

SeverityКритерийВремя реакцииПример
SEV1Полная недоступность / потеря данных5 минБаза данных не отвечает
SEV2Деградация для >10% пользователей15 минПоиск работает медленно
SEV3Деградация для <10% / один сервис1 часНе работают push-уведомления
SEV4Косметическое / некритичноеСледующий рабочий деньДашборд показывает неверный график

3. Runbooks. Для каждого алерта написали runbook — пошаговую инструкцию по диагностике и устранению. Runbook привязан к алерту через аннотацию:

# В правиле Alertmanager:
annotations:
  runbook_url: "https://wiki.internal/runbooks/checkout-high-latency"
  summary: "Checkout latency p99 > 3s"
  dashboard: "https://grafana.internal/d/checkout-slo"
  steps: |
    1. Открой дашборд: {{ $labels.dashboard }}
    2. Проверь, какой downstream-сервис тормозит (панель "Dependency Latency")
    3. Если payment-service: проверь статус провайдера на statuspage.io
    4. Если inventory-service: проверь нагрузку на PostgreSQL (панель "DB Stats")
    5. Если проблема не в downstream: проверь pod-ы kubectl get pods -n checkout

Культура постмортемов

После каждого инцидента SEV1 или SEV2 мы проводим blameless postmortem в течение 48 часов. Формат фиксированный:

  • Timeline — хронология событий с точностью до минуты
  • Impact — сколько пользователей затронуто, какой финансовый ущерб
  • Root cause — 5 Whys до корневой причины
  • What went well — что сработало хорошо (алерт сработал вовремя, runbook помог)
  • What went wrong — что сломалось в процессе (алерт не сработал, runbook устарел)
  • Action items — конкретные задачи с дедлайнами и ответственными

Пример action items из реального постмортема (инцидент: каскадный отказ из-за OOM в search-service):

# Postmortem Action Items — Incident #2026-037
# Date: 2026-03-18

P0 (до конца недели):
  - [ ] Установить memory limits на search-service: 2Gi request, 4Gi limit
  - [ ] Добавить алерт на container_memory_working_set_bytes > 80% limit

P1 (до конца спринта):
  - [ ] Внедрить circuit breaker между catalog-service и search-service
  - [ ] Обновить runbook: добавить секцию "OOM в search"
  - [ ] Настроить PDB (PodDisruptionBudget) для search-service: minAvailable=2

P2 (в backlog):
  - [ ] Провести load testing для определения реальных limits всех сервисов
  - [ ] Исследовать утечку памяти в Elasticsearch client (подозрение на незакрытые scroll contexts)

За 3 месяца мы провели 11 постмортемов. MTTR (Mean Time To Recovery) снизился с 127 минут до 18 минут. Количество повторных инцидентов по той же причине — ноль.

Результаты и выводы

Через 4 месяца работы observability-платформа полностью изменила культуру эксплуатации:

МетрикаДоПосле
MTTR (среднее время восстановления)127 мин18 мин
MTTD (среднее время обнаружения)34 мин3 мин
Ложных алертов в неделю855
Инцидентов SEV1 в месяц40.5
Время диагностики инцидента45 мин8 мин
Стоимость логов (Elasticsearch)$2400/мес$600/мес (Loki)

Главное: observability — это не набор инструментов, а дисциплина. Grafana и Prometheus — необходимый фундамент, но без SLO, structured logging, трейсинга и культуры постмортемов они превращаются в дорогую коллекцию дашбордов, которые никто не смотрит. Если вам нужна помощь в построении observability для микросервисной архитектуры — обращайтесь к нам в itfresh.ru.

Часто задаваемые вопросы

Мониторинг показывает заранее известные проблемы: CPU высокий, диск заканчивается. Observability позволяет исследовать неизвестные проблемы: почему конкретный пользователь видит ошибку, где именно в цепочке из 10 сервисов тормозит запрос. Для этого нужны три столпа — метрики, логи и трейсы — связанные через общий trace_id.
Сам Loki бесплатный и потребляет в 5-10 раз меньше ресурсов, чем Elasticsearch, потому что не индексирует тело лога. Основная стоимость — рефакторинг: перевод логов в structured JSON формат и настройка Promtail/Alloy агентов. Для 200 сервисов это заняло 3 недели работы одного инженера.
Начните с текущей реальности: измерьте фактический success rate и latency за 30 дней. Если сервис показывает 99.7% успешных запросов, не ставьте SLO 99.99% — вы будете в постоянном нарушении. Поставьте 99.5%, достигните стабильности, затем поднимайте. SLO должен отражать ожидания пользователей, а не амбиции инженеров.
Для команд до 20 человек разница минимальна. PagerDuty лучше интегрируется с Prometheus и Grafana из коробки, Opsgenie дешевле и входит в Atlassian-стек. Критично не инструмент, а процесс: расписание дежурств, эскалации, runbooks. Мы видели команды с Excel-таблицей дежурств и MTTR 10 минут — и команды с PagerDuty Enterprise и MTTR 2 часа.
Для SEV1 и SEV2 — обязательно, это основной инструмент снижения повторных инцидентов. Для SEV3 достаточно краткой записи в трекере с root cause и action items. Ключевое правило: постмортем должен быть blameless — без обвинений конкретных людей. Если инженер боится написать правду в постмортеме, вы не найдёте корневую причину.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#observability#golden signals#SLO#SLI#error budget#distributed tracing#structured logging#anomaly detection
Комментарии 0

Оставить комментарий

загрузка...