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 в порядке.
Связанные термины
- Prometheus — метрики, дополняющие трейсы
- Уведомления и алерты — что срабатывает по метрикам
- Микросервисы и монолит — среда, где трейсинг незаменим
- latency — то, что трейсинг раскладывает на составляющие
- Kubernetes — где обычно разворачивают коллектор
- DevOps — практики, включающие наблюдаемость
Готовы запустить GPU-задачу?
Запустить GPU-сервер