DevOps & CI/CD

APM и трейсинг

APM — мониторинг производительности приложений; трейсинг показывает путь одного запроса через все сервисы и где именно он терял время.

Что такое APM и трейсинг

APM (Application Performance Monitoring, мониторинг производительности приложений) — класс инструментов, которые следят за тем, как приложение работает изнутри: время ответа эндпоинтов, доля ошибок, медленные SQL-запросы, где именно уходит время.

Трейсинг (distributed tracing) — техника внутри APM: отследить путь одного конкретного запроса через все сервисы, которые он задел, и увидеть на временной шкале, где он ждал.

Разница с обычным мониторингом принципиальна. Zabbix или Prometheus скажут «CPU 80%, ответ 2 секунды» — то есть что плохо. APM отвечает на вопрос почему: 1.8 из этих 2 секунд ушло на 340 одинаковых запросов к базе внутри цикла.

Trace и span

Что такое трейс (trace) — полная запись обработки одного запроса, дерево из span'ов.

Span — один отрезок работы: HTTP-вызов, SQL-запрос, обращение в кеш. У span'а есть имя, время начала и длительность, родитель и атрибуты. Вложенность span'ов и даёт дерево.

trace_id: 4bf92f3577b34da6  (общий на весь путь запроса)
│
├── span: POST /api/order                      420 ms   [api-gateway]
│   ├── span: auth.verify_token                 12 ms   [auth-service]
│   ├── span: SELECT * FROM users WHERE id=?     8 ms   [postgres]
│   ├── span: order.create                     380 ms   [order-service]
│   │   ├── span: SELECT ... FROM products      6 ms    ← ×50 раз! N+1
│   │   └── span: POST /billing/charge         210 ms   [billing-service]
│   └── span: publish order.created              9 ms   [rabbitmq]

Такая картинка сразу отвечает на вопрос «где тормозит»: 210 мс в billing и полсотни однотипных запросов к БД. Без трейсинга это выяснялось бы чтением логов четырёх сервисов с сопоставлением времени вручную.

Как трейс собирается

Каждому запросу на входе присваивается trace_id. При вызове следующего сервиса он передаётся в HTTP-заголовке — стандарт W3C Trace Context:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             ^  ^                                ^                ^
          версия  trace_id (весь путь)      span_id (родитель)  флаги

Каждый сервис создаёт свои span'ы с тем же trace_id и отправляет их в коллектор, который собирает дерево целиком. Пробрасывание trace_id через границы сервисов называется context propagation — и это единственное, что требует внимания при внедрении: потерянный заголовок разрывает трейс на несвязанные куски.

Три сигнала наблюдаемости

Сигнал Отвечает на вопрос Инструмент
Метрики Что происходит? Сколько? Prometheus, Zabbix
Логи Что произошло в конкретный момент? Loki, Elasticsearch
Трейсы Почему этот запрос был медленным? Jaeger, Tempo, Zipkin

По-настоящему они работают вместе: алерт по метрике → находим медленные трейсы → в span'е видим trace_id → по нему поднимаем логи именно этого запроса. Поэтому trace_id пишут в каждую строку лога.

Инструменты

OpenTelemetry (OTel) — вендоронезависимый стандарт и набор SDK: единый способ собирать метрики, логи и трейсы. Стал стандартом де-факто, поддерживается всеми. Смысл — инструментировать код один раз и менять бэкенд без правок приложения.

Jaeger, Tempo, Zipkin — open-source хранилища и UI для трейсов, ставятся у себя.

Datadog, New Relic, Dynatrace — коммерческие SaaS: больше готового из коробки, оплата по объёму телеметрии.

Sentry — формально про ошибки, но умеет и трейсинг производительности; частый минимальный первый шаг.

Минимальное внедрение

Для многих языков инструментирование не требует правки кода — агент подключается на старте:

pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install     # автоинструментирование библиотек

OTEL_SERVICE_NAME=api \
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317 \
OTEL_TRACES_SAMPLER=parentbased_traceidratio \
OTEL_TRACES_SAMPLER_ARG=0.1 \
opentelemetry-instrument python app.py

Автоинструментирование само оборачивает популярные библиотеки: HTTP-фреймворк, драйвер PostgreSQL, клиент Redis. Вручную добавляют только доменные span'ы:

from opentelemetry import trace
tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("model.generate") as span:
    span.set_attribute("model.name", "qwen2.5-7b")
    span.set_attribute("tokens.input", len(prompt_tokens))
    result = model.generate(prompt)
    span.set_attribute("tokens.output", len(result))

Сэмплирование

Хранить трейсы всех запросов дорого: на нагруженном сервисе это гигабайты в час. Поэтому сохраняют выборку.

  • Head-based (OTEL_TRACES_SAMPLER_ARG=0.1): решение на входе, хранится 10% случайных трейсов. Просто, дёшево, но редкая ошибка может не попасть в выборку.
  • Tail-based: решение после завершения трейса — коллектор хранит все медленные и все с ошибкой, а из успешных быстрых берёт долю. Дороже по ресурсам коллектора, но именно то, что нужно на практике.

Трейсинг для ML-сервисов

Для инференса трейсинг отвечает на вопрос, который метрики не раскрывают: из чего сложилась задержка ответа. Span'ы показывают ожидание в очереди перед батчингом, время prefill и decode, обращение к векторной базе в RAG-сценарии. Часто выясняется, что GPU не при чём: запрос полсекунды ждал свободного слота, а TTFT в порядке.

Связанные термины

Готовы запустить GPU-задачу?

Запустить GPU-сервер