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

Главная → Статьи → Анализ трейсов: Jaeger и Tempo на практике

Jaeger и Grafana Tempo: чтение трейсов и поиск узких мест

Как работать с трейсинг-бэкендом: spans и трейсы, sampling, UI-разбор водопада, переходы метрики→трейсы→логи и выбор между Jaeger и Tempo.

Прямой ответ

Инструментировать приложение трейсами (OpenTelemetry) — половина дела; вторая — бэкенд, где трейсы живут и читаются: Jaeger (ветеран трейсинга — свой UI: поиск по сервису/операции/тегам/длительности, водопад спанов, сравнение трейсов) и Grafana Tempo (бэкенд «трейсы как объекты» — дешёвое хранение в объектном хранилище (S3), без индексации содержимого — поиск через метрики-трейсы (TraceQL/metrics-generator) или по trace id из логов/метрик — глубже интегрирован с Grafana: переходы логи↔трейсы↔метрики одним кликом). Выбор: стек Grafana (Loki+Prometheus) — Tempo естественен (единый UI, exemplars,derived fields — связка «метрика→пример-трейс→водопад» бесшовна); самостоятельный трейсинг с богатым UI-поиском — Jaeger; оба читают OTLP — смена бэкенда дёшева (данные не заложены в проприетарный формат). Практика чтения трейса: водопад (спаны-полоски: ширина = длительность, вложенность = причинность) — ищем (1) длинные полосы (медленный спан — внешний вызов/БД-запрос — атрибуты спана покажут SQL/URL), (2) ступеньки (последовательные вызовы, которые могли быть параллельными — N+1 по сети — проблема в трейсах видна как «заборчик» одинаковых спанов), (3) пропасти (время между спанами — очередь/блокировка/GC — «50 мс ничего» — не в спанах, а между), (4) ошибки (красные спаны — статус/события). И критичная настройка — sampling: 100% трейсов на проде разоряет хранилище и сеть (head-based — решение на входе (probabilistic 5–10% + «все ошибки/медленные» — tail-based — сэмплиер-коллектор видит весь трейс и решает — дороже инфраструктурно, точнее отбор — ошибки и хвосты латентности попадают всегда — паттерн «10% случайных + 100% ошибок/медленных» — стандарт здравого смысла).

Бэкенды: Jaeger против Tempo, деплой

Jaeger: компоненты (collector — приём OTLP/Jaeger-протоколов, storage — Elastic/Cassandra/ClickHouse (колоночный бэкенд популярен для Jaeger-хранилища), query+UI); сильные стороны — UI-поиск («показать трейсы сервиса X операции Y дольше 2s с тегом error=true» — прямой запрос — расследования без trace id), compare-вид (два трейса рядом — «до/после» деградации); стоимость — индексированное хранилище (ES-кластер — эксплуатация/стоимость на объёмах). Tempo: приём OTLP, хранение — блоки в S3/GCS (объектное хранилище — копейки против ES), индекс — только по trace id (+metrics-generator считает RED-метрики из спанов — rate/errors/duration по сервисам бесплатно из трейсов); поиск «по атрибутам» — TraceQL (сканирующий — медленнее индексного Jaeger-поиска, но достаточен с фильтрами по времени/сервису) или — правильный путь — через связанные данные (ошибка в Loki-логе несёт trace_id — клик — трейс (логи); p95-панель с exemplars — точка на графике = конкретный трейс — клик — водопад (метрики); деплой — docker-compose/helm (монолитный режим малым командам, микро-сервисный на объёмах); UI — внутри Grafana (отдельного нет — для команд «живущих в Grafana» — плюс, для любителей отдельного трейс-UI — минус). Оба — OTLP-native: приложение не знает о выборе (экспорт настраивается endpoint'ом — смена бэкенда — правка конфига — конфиг-слой).

Sampling: head, tail, экономика

Задача: трейсы дороги (каждый запрос — десятки спанов — высоконагруженный сервис генерирует ТБ/сутки), но терять хочется только скучное. Head-based (решение при старте запроса): OTel-сэмплиер traceidratio 0.05 — 5% трейсов от начала до конца (дёшево: решение локально, лишние спаны не создаются/не шлются; минус — решение вслепую — «попал ли медленный/ошибочный трейс в 5%» — лотерея; компенсация — parent-based (решение наследуется — распределённый трейс целиком в или целиком out) + отдельные правила на ошибки невозможно — решения ещё нет). Tail-based (решение после завершения трейса): OTel Collector с tail_sampling-процессором буферизует спаны, видит трейс целиком и отбирает по политикам (latency > 1s, status = error, rate_limiting, probabilistic 5% остатка) — ошибки и хвосты попадают всегда — расследования находят материал; цена — коллектор с памятью/буферами (окно ожидания хвоста трейса — минуты — инфраструктурный компонент — collector-развёртывание). Практика: старт — head 10% (dev/staging — 100%), рост/инциденты — tail-политики (ошибки 100%, p99-хвосты 100%, случайные 5–10%); retention — трейсы живут дни-недели (не месяцы — расследования свежие; SLO-агрегаты живут в метриках — метрики дешевле); PII в атрибутах спанов — санитизация (секрет-гигиена — трейсы с дампами запросов — утечка паролей в хранилище — атрибуты проектируются как логи).

Чтение трейсов: паттерны разбора

Водопад-разбор по шаблонам: медленный спан-лист (вся ширина у одного — внешний вызов/запрос — атрибуты: db.statement, http.url, net.peer.name — виновник назван; частые: неиндексированный SQL (планы), внешний API без таймаута, сериализация гигантского ответа); заборчик (десятки одинаковых коротких спанов последовательно — N+1: HTTP-вызовы в цикле/SQL на каждую сущность — лечение — батчинг/joinedload — решения; в трейсах N+1 невооружённо виден — главный аргумент за трейсинг в ORM-спорах); ступени вместо параллели (три независимых внешних вызова по 200 мс подряд = 600 мс — asyncio.gather сокращает до ~200 — async-практика — трейс доказывает выигрыш наглядно); пропасти (между спанами пусто — время в самом приложении: очередь задач, блокировка, GC-пауза — спанов нет, потому что работа не инструментирована — добавить span вокруг подозрительного участка — итеративное уточнение); распределённый переход (span context через очереди/HTTP — трейс сквозь сервисы — провал = контекст не пропагирован (заголовки потеряны/очередь без propagation — доставка с трейс-контекстом в headers) — «трейсы рвутся на Celery» — inject/extract в задаче — типовой фикс). Поиск материала: в Jaeger — запросами по тегам/длительности; в Tempo/Grafana — от метрик (exemplar на p95-всплеске) и логов (trace_id из structured-лога — логи с trace_id — обязательная связка); расследование «p95 вырос вчера» — 3–5 exemplar-трейсов из окна деградации — паттерн виден сразу (все медленные через сервис X / все с SQL Y).

Практический сценарий: p95 вырос вдвое — разбор за 15 минут

Исходник: стек — FastAPI-сервисы за ингрессом, OTel-экспорт в Tempo (tail-сэмплинг: ошибки 100%, >800 мс 100%, случайные 8%), Loki-логи с trace_id, Grafana-дашборды. Событие: алерт «p95 /search > 700 мс» (было 320). Разбор: (1) дашборд — exemplar-точки на p95-панели в окне 14:00–14:20 — клик в первый медленный трейс; (2) водопад: span search_handler 1.4 с, внутри — 14 спанов pg.query SELECT ... FROM products WHERE ... по ~90 мс последовательно — классический заборчик N+1 (фильтры товаров шли отдельными запросами на категорию — рефакторинг прошлой недели — ревью не поймало); (3) подтверждение масштаба: TraceQL-выборка search_handler > 800ms за час — во всех трейсах тот же паттерн (не единичный случай); (4) фикс: батч-запрос категорий одним SQL (IN-запрос — индекс под него уже был) + joinedload позиций — 14 спанов → 2; (5) проверка: нагрузочный прогон locust на стейдже — p95 290 мс (лучше прежнего); трейс-сравнение (до/после водопады рядом) — в PR как доказательство; (6) постмортем-строка в runbook: «N+1 в новых хендлерах — трейс-ревью перед мержем для списочных эндпоинтов» + алерт-порог p95 снижен до 500 мс (раньше заметили бы раньше). Время от алерта до диагноза — 15 минут (до трейсинга аналогичный случай — 2 дня логов); стоимость инфраструктуры Tempo (S3-хранилище + коллектор) — десятки долларов в месяц — окупилась одним инцидентом.

FAQ

Нужен ли трейсинг монолиту? Да, но скромнее: внутри одного процесса «распределённость» не нужна — ценность в спанах участков (БД-запросы, внешние вызовы, тяжёлые функции — «пропасти» водопада локализуют время внутри кода — профилирование дополняет, не дублирует: профилировщик — CPU-время, трейс — хронология и ожидания); объём — head-sampling 1–5% достаточен; бэкенд — Tempo-монолит/Jaeger all-in-one в docker (локальный стек) — час настройки; микросервисы без трейсинга — слепы, монолит — «приглушён» — разница в степени, не в качестве пользы.

Чем TraceQL отличается от Jaeger-поиска? Jaeger-поиск — индексный (теги/операции/длительности — быстро на больших объёмах — ES-индексация стоит денег); TraceQL — язык структурных запросов по трейсам ({ .service = "api" && duration > 1s && span.http.status_code = 500 } — включая внутритрейсовую структуру («где родитель X, а ребёнок Y медленный» — выразительнее тегов) — исполнение сканирующее по блокам (медленнее, но S3-дешевизна компенсирует); практика: расследования «без зацепки» — быстрее в Jaeger; связка от логов/метрик — Tempo-путь короче (trace_id в руке — поиск не нужен вовсе).

Сколько хранить трейсы? Дни-недели (7–30 дней типично): расследования живут «свежими» (инцидент разбирается в дни, не месяцы), долгосрочная память — в метриках (RED/SLO-агрегаты — метрики хранятся годами дёшево — гистограммы и доли) и логах (Loki-retention свой); трейсы на год — дорогая заморозка без потребления; компромисс — сэмплированные 1% случайных на долгий срок (статистические срезы «как было полгода назад» — редкая, но бывает нужна — отдельный retention-поток).

Логи или трейсы — что первично при расследовании? Комплементарны: логи — «что случилось» (события/ошибки/контекст — структура), трейсы — «где и как долго» (хронология/взаимосвязи/узкие места); связующее звено — trace_id в каждом структурированном логе (прыжок лог↔трейс — главная практическая интеграция — Tempo/Grafana делают её кликом); метрики — «когда и насколько» (окно деградации → exemplar-трейсы → логи спанов — треугольник observability в действии); «только логи» на распределённой системе — разбор межсервисных лагов по таймстампам вручную — боль, которую трейсы устраняют.

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