Коротко:
- Трейс - это сквозная запись пути запроса через все сервисы; span - отдельный шаг внутри этого пути.
- OpenTelemetry дает единый стандарт инструментирования: один SDK для любого бэкенда хранения.
- Jaeger и Grafana Tempo - два популярных хранилища трейсов; выбор зависит от того, есть ли у вас уже Grafana-стек.
- Узкое место видно по самому длинному span-у на waterfall-диаграмме - это первое, на что смотрят при разборе медленного запроса.
- Без передачи контекста между сервисами (propagation) трейс разорвется и не покажет полную картину.
- Начинать стоит с одного критичного пути, а не с инструментирования всего сразу.
Почему логи и метрики не дают полной картины
Представьте: пользователь жалуется, что оформление заказа занимает 8 секунд. Prometheus показывает нормальный CPU и память. В Kibana есть логи, но они разбросаны по пяти сервисам, и непонятно, в каком именно порядке выполнялись вызовы. Вы знаете, что что-то медленно, но не знаете - что именно и почему.
Метрики отвечают на вопрос «сколько» и «как часто». Логи говорят «что произошло» в конкретном сервисе. Но ни то ни другое не показывает причинно-следственную цепочку: какой сервис вызвал какой, сколько времени ушло на каждый шаг, где запрос завис в очереди, а где - в ожидании ответа от базы.
Именно этот пробел закрывает трейсинг - третий столп observability. Он дает сквозную картину: от HTTP-запроса на фронте до SQL-запроса в PostgreSQL, включая все промежуточные вызовы между сервисами.
Как устроены trace и span
Каждый входящий запрос получает уникальный trace ID - идентификатор, который путешествует вместе с запросом через все сервисы. Внутри одного трейса каждая операция фиксируется как span: отдельный временной отрезок с именем, временем начала, длительностью и набором атрибутов.
Спаны выстраиваются в дерево. Корневой span - это обычно входящий HTTP-запрос на API Gateway или BFF. Дочерние спаны - вызовы к другим сервисам, запросы к базе, обращения к кешу, отправка сообщений в очередь. Каждый дочерний span знает ID своего родителя - это и позволяет восстановить полную иерархию.
На практике span содержит:
- trace_id - общий для всего запроса
- span_id - уникальный для этой операции
- parent_span_id - ссылка на родителя
- name - что делали (например,
POST /checkoutилиdb.query users) - start_time и duration
- attributes - произвольные теги: http.status_code, db.statement, user.id
- status - OK, ERROR или UNSET
Когда вы открываете трейс в Jaeger или Tempo, вы видите waterfall-диаграмму: горизонтальные полосы, каждая из которых соответствует одному span-у. Длина полосы - это время выполнения. Вложенность - это иерархия вызовов. Самая длинная полоса и есть ваше узкое место.
OpenTelemetry: стандарт, который стоит выбрать сразу
До появления OpenTelemetry каждый инструмент - Jaeger, Zipkin, Datadog - требовал своего SDK. Переход с одного бэкенда на другой означал переписывание инструментирования во всех сервисах.
OpenTelemetry (OTel) решил эту проблему: один SDK, один протокол (OTLP), любой бэкенд. Вы инструментируете сервис один раз, а куда отправлять данные - решаете через конфигурацию коллектора.
Архитектура выглядит так:
- Сервис использует OTel SDK для генерации спанов.
- SDK отправляет данные в OTel Collector - легковесный агент или сайдкар.
- Коллектор обрабатывает, фильтрует и пересылает данные в хранилище - Jaeger, Tempo, Zipkin или любой другой OTLP-совместимый бэкенд.
Официальная документация OpenTelemetry доступна на opentelemetry.io. Там же есть готовые инструкции по инструментированию для Go, Java, Python, Node.js и других языков.
Пример конфигурации OTel Collector (фрагмент otel-collector-config.yaml):
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
exporters: [jaeger]Jaeger или Grafana Tempo: что выбрать
Оба инструмента хранят трейсы и умеют их визуализировать, но заточены под разные контексты.
| Критерий | Jaeger | Grafana Tempo |
|---|---|---|
| Хранилище | Cassandra, Elasticsearch, Badger | Object storage (S3, GCS, локальный диск) |
| Визуализация | Встроенный UI | Через Grafana (нет своего UI) |
| Интеграция с метриками | Отдельно | Нативно через Grafana + Prometheus |
| Стоимость хранения | Выше при больших объемах | Ниже за счет object storage |
| Когда выбирать | Нет Grafana-стека, нужен standalone | Уже используете Grafana + Loki + Prometheus |
Если у вас уже настроен Grafana-стек с Prometheus и Loki, Tempo - очевидный выбор: трейсы, метрики и логи будут в одном интерфейсе, и вы сможете переходить от лога к трейсу одним кликом. Если трейсинг нужен изолированно или команда не хочет тащить Grafana, Jaeger проще поднять и использовать самостоятельно.
Инструментирование сервисов: с чего начать
Самая частая ошибка - пытаться охватить всё сразу. Начните с одного критичного пути: например, оформление заказа или авторизация. Это даст быстрый результат и покажет, как работает инструментирование на практике.
Шаг 1. Добавьте SDK в сервис
Для Node.js это выглядит так:
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-otlp-grpcЗатем инициализируйте SDK в точке входа приложения:
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-grpc');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4317',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();Авто-инструментирование автоматически создает спаны для HTTP-запросов, работы с базой данных (через популярные драйверы), gRPC и других стандартных операций. Для кастомных операций добавляйте спаны вручную.
Шаг 2. Убедитесь, что контекст передается между сервисами
Это самый критичный момент. Если сервис A вызывает сервис B, trace ID должен передаться в заголовке HTTP-запроса. Стандарт называется W3C TraceContext: заголовок traceparent содержит trace ID и parent span ID.
Авто-инструментирование OTel делает это автоматически для HTTP и gRPC. Но если вы используете очереди сообщений (Kafka, RabbitMQ), контекст нужно передавать вручную через атрибуты сообщения - иначе трейс разорвется на границе очереди.
Частая ошибка: сервис создает новый trace ID при каждом входящем запросе, не проверяя, есть ли уже traceparent в заголовках. В результате трейс начинается не с фронта, а с этого сервиса, и вы теряете всю предысторию вызова.
Шаг 3. Добавьте атрибуты, которые помогут при разборе
Стандартные атрибуты от авто-инструментирования дают базу, но для реального расследования нужны бизнес-контекст и идентификаторы:
user.id- чтобы найти трейс конкретного пользователяorder.id,request.id- для корреляции с логамиdb.statement- какой именно SQL выполнялся (осторожно с чувствительными данными)http.route- маршрут, а не URL с параметрами
Добавить атрибут к текущему спану в Node.js:
const { trace } = require('@opentelemetry/api');
const span = trace.getActiveSpan();
if (span) {
span.setAttribute('order.id', orderId);
span.setAttribute('user.id', userId);
}Как читать трейс и находить проблему
Допустим, запрос на оформление заказа занимает 4 секунды вместо ожидаемых 400 миллисекунд. Вы открываете Jaeger, вводите trace ID из лога (или ищете по тегу http.route=/checkout с фильтром по длительности > 3s) и видите waterfall.
На что смотреть в первую очередь:
- Самый длинный span. Если это
db.queryв сервисе инвентаря - проблема в запросе к базе. Если это ожидание ответа от внешнего API - проблема во внешней зависимости. - Последовательные вызовы там, где можно параллельно. Если сервис заказов последовательно вызывает сервис пользователей, сервис инвентаря и сервис цен, а каждый занимает 300 мс - итого 900 мс. Если сделать вызовы параллельно, будет 300 мс.
- Пустые промежутки между спанами. Время между окончанием одного span-а и началом следующего - это накладные расходы: сериализация, сетевая задержка, ожидание в пуле соединений.
- Спаны со статусом ERROR. Даже если запрос в итоге завершился успешно, ошибка внутри (например, retry после первой неудачи) видна по красному статусу.
Реальный сценарий разбора: Представьте маркетплейс, где страница товара грузится 3 секунды. Waterfall показывает: корневой span занимает 3000 мс, из них 2800 мс - это span redis.get в сервисе рекомендаций. Атрибут redis.key указывает на ключ с TTL 0 - то есть кеш никогда не истекает, но при этом каждый раз делается полный пересчет из-за бага в логике инвалидации. Без трейса это заняло бы часы поиска по логам. С трейсом - 5 минут.
Sampling: как не утонуть в данных
Вакансии для DevOps-инженеров
В высоконагруженной системе записывать каждый запрос нецелесообразно - это дорого по хранилищу и создает нагрузку. Здесь нужен sampling - стратегия отбора трейсов для сохранения.
Два основных подхода:
- Head-based sampling. Решение принимается на входе запроса - например, сохранять 10% всех трейсов. Просто, но вы можете пропустить редкие ошибки.
- Tail-based sampling. Решение принимается после завершения трейса. Можно настроить: сохранять 100% трейсов с ошибками или длительностью > 1s, и только 1% остальных. Требует буферизации в коллекторе.
Для большинства команд хорошим стартом будет: 100% трейсов с ошибками + 5-10% успешных. OTel Collector поддерживает tail-based sampling через процессор tailsampling.
Связь трейсов с логами и метриками
Трейсинг становится значительно мощнее, когда он связан с остальными сигналами. Ключевой механизм - trace ID в логах.
Если каждый лог-запись содержит trace_id текущего запроса, вы можете из Grafana Tempo кликнуть на span и сразу перейти к соответствующим логам в Loki - без ручного копирования идентификаторов. Это называется exemplars и correlation, и в Grafana это работает нативно.
Для добавления trace ID в логи в Node.js с использованием winston:
const { trace } = require('@opentelemetry/api');
const addTraceContext = winston.format((info) => {
const span = trace.getActiveSpan();
if (span) {
const ctx = span.spanContext();
info.trace_id = ctx.traceId;
info.span_id = ctx.spanId;
}
return info;
});С метриками связь работает через exemplars в Prometheus: конкретная метрика (например, всплеск latency) может содержать ссылку на trace ID, который был в момент всплеска.
Когда трейсинг не нужен или не поможет
Инструментирование требует времени и поддержки. Есть ситуации, где затраты не оправданы:
- Монолит с одной базой данных. Если у вас один сервис, профилировщик и медленные запросы в pg_stat_statements дадут ответ быстрее.
- Проблема на уровне инфраструктуры. Если диск перегружен или сеть между нодами деградировала, трейс покажет симптом (все спаны медленные), но не причину - здесь нужны метрики инфраструктуры.
- Очень ранняя стадия продукта. Если у вас два сервиса и 100 пользователей, стоимость настройки трейсинга может быть выше пользы. Начните с нормального логирования.
- Команда не готова читать трейсы. Инструмент без культуры использования не работает. Если никто не открывает Jaeger при инцидентах, инвестиция бессмысленна.
Типичные ошибки при внедрении
Большинство проблем при первом внедрении предсказуемы:
- Не настроена передача контекста через очереди. Kafka-консьюмер начинает новый трейс, и вы никогда не увидите, что запрос пришел из конкретного HTTP-вызова.
- Слишком широкие имена спанов. Имя
db.queryбез уточнения таблицы или операции бесполезно при разборе. Лучше:db.select usersилиdb.insert orders. - Запись чувствительных данных в атрибуты. Пароли, токены, номера карт не должны попадать в трейсы. Настройте фильтрацию в коллекторе или на уровне SDK.
- Head-based sampling 100% в продакшене. При нагрузке 1000 RPS это тысяча трейсов в секунду - хранилище быстро переполнится.
- Инструментирование только одного сервиса. Трейс без дочерних спанов от других сервисов показывает только время ожидания, но не что происходило внутри вызываемого сервиса.
Чеклист внедрения
- Выбрать бэкенд: Jaeger (standalone) или Tempo (если есть Grafana-стек).
- Развернуть OTel Collector как DaemonSet или sidecar в Kubernetes.
- Добавить OTel SDK в первый критичный сервис, включить авто-инструментирование.
- Проверить, что trace ID появляется в заголовке
traceparentисходящих запросов. - Добавить SDK во все сервисы на критичном пути (не обязательно сразу во все).
- Настроить передачу контекста через очереди сообщений, если они есть.
- Добавить trace_id в логи для корреляции.
- Настроить sampling: 100% ошибок + 5-10% успешных запросов.
- Добавить бизнес-атрибуты: user.id, order.id, request.id.
- Провести первый разбор медленного запроса по waterfall-диаграмме.
- Договориться с командой: при инцидентах смотрим трейсы, а не только логи.
FAQ
Чем трейсинг отличается от логирования?
Логи фиксируют события внутри одного сервиса. Трейс показывает полный путь запроса через несколько сервисов с временными метками каждого шага. Они дополняют друг друга: trace ID в логах связывает оба инструмента.
Что такое span простыми словами?
Span - это один шаг в жизни запроса: например, HTTP-вызов к другому сервису или SQL-запрос к базе. У каждого шага есть имя, время начала и длительность. Из таких шагов складывается полная картина - трейс.
Нужен ли OpenTelemetry, если уже используется Jaeger?
Jaeger поддерживает прием данных в формате OTLP, поэтому OTel и Jaeger отлично работают вместе. OTel SDK - это инструментирование на стороне приложения, Jaeger - хранилище и UI. Использовать OTel стоит в любом случае: это защита от vendor lock-in.
Как найти узкое место, если трейс очень большой?
Сортируйте спаны по длительности - самый медленный будет первым. Также обращайте внимание на последовательные вызовы, которые можно было бы распараллелить, и на пустые промежутки между спанами (время ожидания в очередях или пулах соединений).
Что делать, если трейс обрывается на границе сервиса?
Скорее всего, один из сервисов не читает заголовок traceparent из входящего запроса или не передает его в исходящих. Проверьте, что OTel SDK инициализирован до обработки первого запроса, и что HTTP-клиент использует OTel-инструментированную версию.
Сколько это стоит в плане производительности?
Накладные расходы от OTel SDK при разумном sampling (5-10%) обычно составляют менее 1-2% CPU и памяти. Основная нагрузка - это сеть и хранилище. Tail-based sampling и object storage (для Tempo) существенно снижают стоимость.
Можно ли использовать трейсинг без Kubernetes?
Да. OTel Collector можно запустить как обычный процесс или Docker-контейнер. Jaeger тоже доступен как standalone-бинарник. Kubernetes упрощает деплой, но не является обязательным условием.
Итог
Трейсинг закрывает слепое пятно, которое остается после логов и метрик: он показывает, что именно происходит с запросом на каждом шаге его пути. Waterfall-диаграмма с временными метками спанов превращает расследование медленного запроса из многочасового перебора логов в пятиминутный анализ.
Начните с одного критичного пути, добавьте OTel SDK, настройте передачу контекста между сервисами и выберите хранилище под свой стек. Первые результаты будут видны уже после инструментирования двух-трех сервисов - и это быстро меняет то, как команда разбирает инциденты.