Jaeger и трассировка: где именно теряется секунда
Запрос отвечает за 2.4 секунды, и это единственное, что говорят логи. Веб-сервер молчит о том, сколько ждал базу, база не знает про внешний API, а кеш вообще не в курсе, что кто-то жаловался на медленность. Вы открываете три разных лога, сверяете таймстемпы вручную и через полчаса всё ещё гадаете — трассировка в Jaeger решает эту задачу за секунды, показывая один конкретный запрос как единую временную диаграмму через все компоненты, которые он прошёл.
Содержание
Базовый принцип: trace ID и span
Распределённая трассировка решает проблему одной идеей: конкретному пользовательскому запросу присваивается уникальный trace ID, и этот идентификатор передаётся через все компоненты, которые участвуют в его обработке.
Практически это выглядит так. Когда запрос попадает на веб-сервер, инструментация (библиотека OpenTelemetry, встроенная в код приложения) генерирует trace ID — например, 4bf92f3577b34da6a3ce929d0e0e4736. Дальше каждый вызов внутри обработки этого запроса — обращение к базе, к внешнему API, к кешу — оборачивается в span: запись с полями «когда начался», «когда закончился», «что за операция», «какой родительский span у него был». Все spans одного запроса привязаны к одному trace ID.
Когда приложение делает исходящий HTTP-запрос к другому сервису (например, к отдельному микросервису оплаты, а не просто к внешнему API), trace ID передаётся дальше через HTTP-заголовок traceparent — это часть открытого стандарта W3C Trace Context:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Здесь 4bf92f3577b34da6a3ce929d0e0e4736 — это trace ID (общий для всего запроса), 00f067aa0ba902b7 — span ID текущего этапа. Сервис-получатель читает этот заголовок, создаёт свой span как «дочерний» и продолжает ту же цепочку. Именно так трассировка «путешествует» через границы процессов и даже через границы языков программирования — Python-сервис передаёт эстафету Go-сервису, тот передаёт Java-сервису, и всё это остаётся одной трассировкой.
В конце все spans с одинаковым trace ID стекаются в Jaeger, который собирает их вместе и рисует полную хронологию одного конкретного запроса.
Как выглядит диагностика в интерфейсе Jaeger
После того как трассировка собрана, в веб-интерфейсе Jaeger (по умолчанию порт 16686) вы открываете конкретную трассировку и видите диаграмму Ганта: горизонтальные полоски, каждая — один span, отсортированные по времени начала. Наглядно видно:
├─ http_request (веб-сервер) [====================================] 2400ms
│ ├─ db_query: SELECT stock [==] 120ms
│ ├─ external_call: payment_gateway_authorize [==========================] 1950ms
│ │ └─ (внутри — если платёжный шлюз тоже инструментирован — его собственные spans)
│ ├─ cache_get: redis exchange_rate [=] 15ms
│ └─ response_serialize [=] 8ms
Диагноз становится очевиден за секунды: из 2.4 секунды общего времени 1.95 секунды — это ожидание ответа от внешнего платёжного API. База данных и Redis тут почти ни при чём. Без трассировки вы могли бы неделю оптимизировать индексы в PostgreSQL и разбирать метрики базы данных через Grafana, гоняясь не за той причиной, — а трассировка сразу указывает пальцем на конкретный вызов конкретного компонента.
Это принципиально другой уровень диагностики по сравнению с ручным сопоставлением логов. Вместо вопроса «какой из пяти компонентов виноват в общей медленности» вы получаете прямой ответ, вычисленный автоматически: вот этот span, вот столько миллисекунд, вот его родитель. Дальше вопрос сужается до частного — например, «почему платёжный шлюз в этот раз отвечал 1.95 секунды при обычных 300 мс» — а это уже задача для мониторинга самого шлюза или для его саппорта, а не для гадания по логам трёх систем.
Отдельно полезна функция сравнения трассировок: берёте одну быструю (200 мс) и одну медленную (2.4 с) трассировку одного и того же эндпоинта и визуально видите, какой именно span «раздулся». Это быстрее, чем читать код и предполагать.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSИнструментация приложения: откуда берётся trace ID
Трассировка не появляется сама — код приложения нужно инструментировать. На практике сегодня для этого используют OpenTelemetry (открытый стандарт, который умеет экспортировать данные в Jaeger, Grafana Tempo и другие бэкенды). Минимальный пример для Python:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
trace.set_tracer_provider(TracerProvider())
exporter = OTLPSpanExporter(endpoint="jaeger:4317", insecure=True)
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(exporter))
tracer = trace.get_tracer(__name__)
def handle_order(order_id):
with tracer.start_as_current_span("handle_order") as span:
span.set_attribute("order.id", order_id)
with tracer.start_as_current_span("db_query_stock"):
check_stock(order_id)
with tracer.start_as_current_span("external_call.payment_gateway"):
authorize_payment(order_id)
Для популярных фреймворков (FastAPI, Flask, Django, Express, Spring) есть готовые автоинструментирующие библиотеки (opentelemetry-instrumentation-fastapi, например), которые оборачивают HTTP-запросы, обращения к БД через SQLAlchemy и HTTP-клиенты автоматически, без ручной расстановки spans в каждом обработчике. Это закрывает 70-80% случаев без изменения бизнес-логики — вручную дописывать имеет смысл только для действительно важных внутренних операций, которые хочется видеть отдельным span.
Важный нюанс: если у вас очередь сообщений (RabbitMQ, Kafka) между компонентами — trace ID нужно прокидывать вручную через заголовки сообщения, автоинструментация HTTP тут не поможет. Это частое место, где трассировка «обрывается» на границе асинхронной обработки, и разработчики об этом узнают только когда видят в Jaeger трассировку, у которой не хватает половины ожидаемых spans.
Развёртывание Jaeger: минимальная схема на VPS
Для знакомства и небольшой нагрузки достаточно all-in-one контейнера — он объединяет collector (принимает spans), storage (хранит их, по умолчанию в памяти) и UI в одном процессе:
# docker-compose.yml
services:
jaeger:
image: jaegertracing/all-in-one:1.57
ports:
- "16686:16686" # веб-интерфейс
- "4317:4317" # OTLP gRPC (сюда шлют spans приложения)
- "4318:4318" # OTLP HTTP
environment:
- COLLECTOR_OTLP_ENABLED=true
Поднимаете docker compose up -d, открываете http://<ip-сервера>:16686 — интерфейс готов. Подробный разбор пошаговой настройки Jaeger со всеми флагами и типичными граблями — в отдельной статье про установку и конфигурацию трассировки запросов. Для продакшена all-in-one с хранением в памяти не годится: при перезапуске контейнера все трассировки теряются, а под серьёзной нагрузкой памяти не хватит. Тогда Jaeger разворачивают в компонентной схеме — отдельно collector, отдельно query-сервис (UI/API), а хранилищем ставят Elasticsearch или Cassandra:
| Компонент | Роль | Где ставить |
|---|---|---|
| jaeger-agent / OTel Collector | принимает spans от приложений, буферизует, шлёт дальше | рядом с приложением или отдельным сервисом |
| jaeger-collector | валидирует, обрабатывает, пишет в хранилище | отдельный сервис, можно масштабировать горизонтально |
| Elasticsearch / Cassandra | долговременное хранение трассировок | отдельный сервер с достаточным диском |
| jaeger-query | отдаёт данные в веб-интерфейс | тот же сервер, что и collector, или отдельно |
Для сервера с несколькими сервисами под трассировку обычно достаточно VPS на 2-4 vCPU и 4-8 ГБ RAM, если хранилище — компактный Elasticsearch с ограниченным retention (например, хранить трассировки 3-7 дней, а не бесконечно — старые трассировки для отладки производительности почти никогда не нужны). Дисковое место растёт быстро при высокой нагрузке — это стоит закладывать заранее, а не разбираться постфактум, почему кончилось место.
Когда трассировка не нужна и когда усложнение не окупается
Трассировка — не универсальный must-have для любого проекта. Она решает конкретную проблему: невозможность понять распределение времени между несколькими независимыми компонентами. Если у вас монолитное приложение — один процесс, одна база данных, без внешних API и без микросервисов — эта проблема просто не возникает в том виде, для которого создавался Jaeger. Профилировщик языка (py-spy, встроенный профайлер Node.js, APM без распределённой части) даст те же ответы проще и с меньшими накладными расходами.
Трезвая оценка перед внедрением — примерно такая:
- Один сервис, одна база, без внешних интеграций — трассировка почти наверняка избыточна. Хватит логов с request ID и метрик времени ответа по эндпоинтам.
- 2-3 сервиса, есть внешний API или очередь между ними — трассировка уже начинает окупаться, особенно если бывают жалобы «то быстро, то медленно» без понятной причины.
- 5+ сервисов, несколько баз, внешние интеграции, асинхронная обработка через очереди — трассировка почти обязательна: без неё диагностика производительности превращается в гадание, которое отнимает часы у разработчиков на каждый инцидент.
Стоимость внедрения — это не только развёртывание Jaeger (оно как раз простое), а инструментация кода: расстановка spans, прокидывание контекста через очереди, обучение команды читать диаграммы. На монолите эта работа не окупится пользой. На системе из десятка сервисов — окупится в первую же неделю, когда трассировка укажет на конкретный медленный вызов вместо трёхчасового разбирательства по логам.
Есть и промежуточный вариант — трассировка «частично»: инструментировать только самые критичные и самые непрозрачные участки (внешние API, межсервисные вызовы), не трогая внутреннюю логику одного сервиса. Это разумный компромисс, если полноценное покрытие кажется избыточным прямо сейчас.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Jaeger — это то же самое, что логи или метрики?
Нет, это третий тип телеметрии. Логи — текстовые записи о событиях (см. лучшие практики логирования в Docker), метрики — числовые ряды во времени (например, RPS, время ответа), трассировка — хронология одного конкретного запроса через несколько компонентов. Они дополняют друг друга, а не заменяют: метрики говорят «стало медленно», трассировка — «медленно вот здесь конкретно».
Нужно ли инструментировать вообще весь код?
Нет. Автоинструментация HTTP-фреймворков и клиентов БД закрывает большинство случаев. Ручные spans добавляют точечно — там, где хочется видеть отдельную операцию на диаграмме (например, конкретный шаг сложной бизнес-логики).
Какая нагрузка на приложение от трассировки?
Накладные расходы есть, но обычно небольшие при разумной частоте сэмплирования (не 100% запросов, а, скажем, 10-20% или адаптивно — больше для медленных и ошибочных запросов). Точный процент замедления зависит от вашей нагрузки и конфигурации — проверяйте на тестовом окружении, а не полагайтесь на общие цифры из интернета.
Можно ли использовать Jaeger вместе с Grafana и Prometheus?
Да, это стандартная комбинация: Prometheus + Grafana — для метрик и алертов, Jaeger — для трассировки, Loki или Graylog — для логов. Grafana умеет показывать все три источника в одном интерфейсе, с переходом от метрики к конкретной трассировке одним кликом (это называют exemplars).
Что делать, если trace ID теряется между сервисами?
Чаще всего это очередь сообщений или устаревший HTTP-клиент, который не пробрасывает заголовок traceparent. Проверяйте вручную первым делом: залогируйте заголовки входящего и исходящего запроса на границе сервиса, где трассировка обрывается.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →