Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → OpenTelemetry: трейсинг распределённых систем

Распределённый трейсинг с OpenTelemetry: спаны, контекст, трассы запросов

Как видеть запрос сквозь сервисы: модель трейсов и спанов OpenTelemetry, распространение контекста, инструментирование Python-приложения, выбор бэкенда (Jaeger/Tempo), семантические конвенции.

Прямой ответ

Распределённый трейсинг отвечает на вопрос, который не закрывают ни метрики, ни логи: «где именно внутри цепочки из пяти сервисов запрос провёл свои три секунды». Модель OpenTelemetry (индустриальный стандарт, пришедший на смену Jaeger/Zipkin-форматам): трейс — дерево обработки одного запроса; спан — единица работы (обработчик запроса, вызов БД, внешний API) с длительностью, атрибутами и ссылкой на родителя; контекст передаётся между процессами через заголовки (W3C traceparent), и дерево собирается в бэкенде (Jaeger/Tempo) независимо от того, где какие спаны закончились. Для Python подключение почти автоматическое: opentelemetry-instrumentation-fastapi/sqlalchemy/requests + экспортёр в OTLP — и типовой веб-стек трейсится без ручной обвязки; вручную добавляются спаны вокруг бизнес-операций, которые автоматике не видны.

Модель: трейсы, спаны, контекст

Запрос приходит в сервис A: создаётся корневой спан (trace_id — уникальный на запрос, span_id — на операцию). A вызывает БД — дочерний спан; вызывает сервис B по HTTP — заголовок traceparent несёт trace_id и parent span_id, B продолжает то же дерево (его обработчик и его вызовы — потомки). Каждый спан знает: имя операции (HTTP-маршрут, SELECT users, имя очереди), начало/длительность, атрибуты (http.route, db.statement, user.tier — по семантическим конвенциям OTel, чтобы бэкенд и дашборды понимали их одинаково), статус/ошибки. Результат в UI бэкенда — «водопад» запроса: сразу видно, что 2.7 с из 3.0 проведены в одном вызове внешнего API или в N+1-запросов к БД (трейс с сотней одинаковых дочерних спанов SELECT — диагноз N+1 без единого слова в коде; сама проблема — в статье про N+1).

Инструментирование Python-приложения

Автоматический слой: opentelemetry-instrument (обёртка запуска) или явная настройка в коде — инструментаторы для FastAPI/Flask (спан на запрос, контекст из входящих заголовков), SQLAlchemy (спаны SQL-запросов), requests/httpx (исходящие вызовы с инъекцией traceparent), redis, Celery (контекст через заголовки задачи — трейс переживает очередь!), psycopg. Ручной слой — там, где автоматика слепа: with tracer.start_as_current_span("calculate_discount") as span: span.set_attribute("promo.code", code) — бизнес-операции, тяжёлые вычисления, обращения к кешу. Дисциплина атрибутов: семантические конвенции для стандартного (http/db/messaging), доменные — по своей схеме; высококардинальные значения — в атрибуты спанов можно (user_id в трейсинге допустим — это не метрики, спаны живут днями, а не агрегируются в серии), но чувствительное (пароли, токены, ПДн) — под запретом. Сэмплирование: head-based (решение на входе — «трейсим 10% запросов») против tail-based (в коллекторе — все ошибки и медленные сохраняются, рутина отбрасывается) — tail-based дороже инфраструктурно, но оставляет именно интересные трейсы.

Бэкенд: Jaeger, Tempo, коллектор

Схема приёма: приложение → OTLP-экспорт → OpenTelemetry Collector (опциональный, но полезный промежуточный слой: батчинг, ретраи, сэмплирование, маршрутизация) → бэкенд хранения/поиска. Выбор бэкенда: Jaeger — классика трейсинга, удобный UI сравнения трейсов; Grafana Tempo — хранение в объектном хранилище (дёшево, как Loki для логов), интеграция с Grafana (переходы метрики→трейсы→логи по одному клику — связка с Loki и Grafana даёт «три столпа» в одном UI). Локальная разработка: collector + Jaeger all-in-one в docker-compose, экспортёр в приложении включается переменной окружения — трейсы собственного запроса видны сразу, и «почему этот эндпоинт делает 40 запросов к БД» обнаруживается за минуту. Для локальной отладки одиночного сервиса трассировка избыточна — её время приходит с распределённостью.

Три столпа: трейсы, метрики, логи вместе

Зрелая observability — не три отдельных системы, а связанные: метрики говорят «что-то не так» (p95 вырос — алерт), трейсы — «где именно» (открываем медленный трейс из панели Grafana exemplars/ссылки), логи — «почему» (из спана — переход к логам того же request_id/trace_id; trace_id в каждой лог-строке — копеечное поле, которое склеивает всё; структурированные логи с trace_id — обязательная пара к трейсингу). Обратная связь: из ошибки в логе — поиск трейсов по атрибуту; из трейса — exemplar-точка на графике латентности. Практика внедрения: trace_id пробрасывается с первого дня (дешевле, чем добавлять потом), алерты остаются на метриках (трейсы — инструмент расследования, не мониторинга), дашборд инцидента содержит ссылку на сохранённый поиск трейсов по симптому.

Практический сценарий: расследование «тормозящего» заказа

Жалоба: создание заказа иногда занимает 8 секунд. Метрики показывают p99 = 7.5 с на POST /orders, но p95 в норме — значит, редкий сценарий. В Grafana — переход к трейсам маршрута с длительностью > 5 с (Tempo-запрос по атрибутам); открываем первый: водопад показывает — обработчик 8.1 с, внутри: спан SELECT валидации — 40 мс, спан внешнего API доставки — 7.8 с, спан записи заказа — 60 мс. Диагноз за минуту: редкий таймаут внешнего сервиса доставки (у которого мы держим таймаут 8 с без ретрая и без кеша тарифов). Лечение — по ситуации: сократить таймаут до 2 с с ретраем на идемпотентный вызов (дисциплина HTTP-клиента), кеш тарифов на минуту. Повторный замер через день: p99 маршрута 900 мс; в дашборд добавлена панель p99 внешнего API с алертом — следующий такой инцидент найдётся до жалоб. Без трейсинга это расследование — часы чтения логов с таймстампами.

FAQ

OpenTelemetry или Jaeger-клиент напрямую? OpenTelemetry — стандарт инструментирования (API/SDK/формат), Jaeger — один из бэкендов; «Jaeger-клиент» сегодня означает легаси-путь. Инструментируйте через OTel, бэкенд меняйте свободно (экспорт — OTLP).

Какие накладные расходы? Создание спанов — микросекунды; экспорт — батчами в фоне. Заметная стоимость — сеть/хранение при 100% сэмплировании высокого трафика; head-сэмплирование 1–10% + tail-based для ошибок — стандартный баланс.

Нужен ли трейсинг монолиту? Для одного процесса — редко (профилировщик эффективнее); исключения — монолит с активными внешними вызовами (видно, где время уходит вовне) и подготовка к распилу на сервисы (трейсы показывают фактические границы вызовов — аргумент для разрезания).

Как трейсить фоновые задачи и cron? Контекст через метаданные задачи (Celery-инструментатор делает это сам; для самописных очередей — передавать traceparent в payload вручную); у «беспричинных» фоновых запусков корневой спан создаётся на старте джоба — джоб виден в бэкенде как отдельный трейс.

Источники: документация OpenTelemetry (концепции, Python), Jaeger docs — ссылки в шапке.