{ "index": 262, "slug": "editorial-2020-09-field-tracing-basics", "title": "Как читать распределённый trace: duration, parent и critical path", "excerpt": "Практический разбор медленного запроса: как проверить связность trace, не сложить вложенные интервалы дважды и выбрать следующую проверку вместо поспешного увеличения timeout.", "contentHtml": "

Пользователь ждёт ответ API 240 мс, а в trace видит несколько span: gateway, catalog, pricing и inventory. Команда складывает все duration, получает 720 мс и начинает искать «лишнюю» задержку. Другая команда видит самый длинный span inventory и сразу увеличивает timeout. Обе реакции могут ошибиться. Первая удваивает время вложенных операций. Вторая меняет лимит, но не проверяет, кто удерживает ответ.

\n

Цена ошибки — не только несколько миллисекунд в отчёте. Неверный диагноз закрепляет плохую конфигурацию, маскирует потерю контекста и переносит проблему на следующий релиз. Правильный разбор начинается с наблюдаемых полей: один ли это trace, кто родитель span, где начинается и заканчивается каждый интервал, какая ветка завершается последней.

\n

Тезис: сначала связность, потом арифметика

\n

Trace показывает путь операции через границы компонентов. Span описывает отдельный участок этого пути. Parent связывает участок с предыдущей операцией. Duration равен разности end - start. Эти факты позволяют объяснить задержку, но только если записи относятся к одной логической истории и используют сопоставимую шкалу времени.

\n

Родительский span обычно включает интервалы своих children. Поэтому duration родителя и duration child нельзя складывать как последовательные операции. Если две дочерние ветки перекрываются, их время тоже не складывается. Сначала нужно найти интервал, который удерживает ответ до конца, затем проверить его собственные участки.

\n

Учебный пример с одной шкалой

\n

Ниже — controlled example. Все значения придуманы для объяснения метода. Это не замер реального сервиса и не production-результат. Корневой span gateway.handle живёт от 0 до 240 мс. Он вызывает catalog.lookup от 20 до 220 мс. Внутри catalog две параллельные ветки: pricing.read от 30 до 70 мс и inventory.fetch от 30 до 200 мс. Inventory запускает inventory.adapter от 100 до 170 мс.

\n
Синтетический waterfall одного trace
SpanParentИнтервалDuration
gateway.handleroot0–240 ms240 ms
catalog.lookupgateway.handle20–220 ms200 ms
pricing.readcatalog.lookup30–70 ms40 ms
inventory.fetchcatalog.lookup30–200 ms170 ms
inventory.adapterinventory.fetch100–170 ms70 ms
\n

Из таблицы нельзя заключить, что запрос занял 240 + 200 + 40 + 170 + 70 мс. Children находятся внутри parent. Pricing и inventory идут параллельно с 30 до 70 мс. Поэтому pricing не добавляет 40 мс после inventory. Корневой ответ заканчивается в 240 мс, а не в сумме всех строк.

\n

Как duration превращается в проверяемый вывод

\n

Сначала проверьте форму интервалов. Для каждого span должно выполняться end >= start. Child должен находиться внутри parent, если модель использует обычное дерево вложенных операций. Если child выходит за границы родителя, это не повод сразу рисовать красную полосу. Причиной может быть ошибка закрытия span, асинхронная работа после ответа или неверная модель связи. Запишите факт и остановите арифметику до выяснения.

\n
const spans = [\n  { name: 'gateway.handle', parent: null, start: 0, end: 240 },\n  { name: 'catalog.lookup', parent: 'gateway.handle', start: 20, end: 220 },\n  { name: 'pricing.read', parent: 'catalog.lookup', start: 30, end: 70 },\n  { name: 'inventory.fetch', parent: 'catalog.lookup', start: 30, end: 200 },\n  { name: 'inventory.adapter', parent: 'inventory.fetch', start: 100, end: 170 },\n];\n\nfunction duration(span) {\n  return span.end - span.start;\n}\n\nfunction isInside(child, parent) {\n  return child.start >= parent.start && child.end <= parent.end;\n}
\n

Эта функция проверяет только учебные интервалы. Она не исправляет часы разных машин, не восстанавливает потерянный span и не доказывает, что transport передал контекст. В настоящей системе сначала установите, откуда пришли timestamps и как инструмент описывает асинхронные связи.

\n
\"Учебный
Схема показывает только controlled example. Pricing и inventory пересекаются, поэтому их inclusive duration нельзя складывать. Позднее завершение inventory определяет конец ветки catalog.
\n

Critical path без двойного счёта

\n

В этом примере последний child catalog — inventory.fetch: он заканчивается в 200 мс, тогда как pricing заканчивается в 70 мс. Поэтому путь до конца ответа проходит через gateway.handle → catalog.lookup → inventory.fetch. Adapter находится внутри inventory и помогает объяснить его работу, но его 70 мс уже входят в 170 мс inventory.

\n

Для более точной проверки разложите выбранные интервалы на exclusive-участки. У gateway остаются 40 мс вне catalog: 0–20 и 220–240. У catalog остаются 30 мс вне объединения children: 20–30 и 200–220. У inventory остаются 100 мс вне adapter: 30–100 и 170–200. Adapter даёт 70 мс собственного участка. Сумма 40 + 30 + 100 + 70 = 240 мс совпадает с root duration. Это проверка разложения конкретной модели, а не универсальный алгоритм для любого trace-хранилища.

\n
Симптом → причина → проверка → действие
СимптомВозможная причинаПроверкаДействие
Все duration в сумме больше rootВложенные span посчитали повторноСверить интервалы parent и childСчитать overlap и exclusive-участки
У соседнего сервиса новый trace-idContext не передали или создали новый rootСравнить traceparent на границеПроверить inject/extract и решение о новой границе
Child заканчивается после parentSpan закрыли рано или работа асинхроннаСопоставить lifecycle операции и timestampsИсправить закрытие либо описать link/async-модель
Самый длинный span не объясняет ответОн перекрывается с другой веткойНайти ветку с последним endПроверить critical path, а не максимум duration
Trace обрывается на proxySampler, фильтр или transport не сохранил contextСравнить входной и исходящий carrierДобавить точечную проверку boundary и не обещать полноту
\n

Контекст на границе сервиса

\n

Связность trace не появляется из названий span. Клиент должен передать контекст, а принимающая сторона — извлечь его и создать новый child. Для W3C Trace Context заголовок traceparent содержит version, trace-id, parent-id и trace-flags. Пример ниже синтетический. Он показывает форму данных, но не является рабочим токеном и не доказывает прохождение через конкретный proxy.

\n
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\n\n// gateway: inject current span context\n// catalog: extract traceparent, create child span\n// inventory: inject catalog child as the next parent
\n

Если catalog создаёт новый root, downstream trace может выглядеть аккуратно, но связь с gateway потеряна. Если parser принимает нулевой или неверно оформленный ID, система получает ложную иерархию. Если выборка не сохраняет часть span, trace остаётся неполным. В каждом случае отрицательный путь важнее красивого графика: нужно уметь сказать «связь не доказана».

\n

Порядок действий

\n
  1. Сформулируйте симптом без причины: какой ответ задержан, в каком диапазоне и какую ошибку может вызвать неверная правка.
  2. Возьмите один trace и выпишите trace-id, span-id, parent-id, start и end для нужных записей.
  3. Проверьте, что записи относятся к одной истории и parent образует допустимое дерево.
  4. Проверьте интервалы: duration равен end - start, child не выходит за parent в выбранной модели.
  5. Нарисуйте waterfall и отметьте перекрытия. Не складывайте inclusive duration.
  6. Найдите ветку с самым поздним завершением и разложите её на exclusive-участки.
  7. Выберите одну следующую проверку: transport boundary, adapter, sampler или источник времени.
  8. Повторите тот же trace-level анализ после изменения и сравните заранее выбранный сигнал.
\n

Ограничения метода

\n

Обычное дерево span плохо описывает fan-out с несколькими родителями, очередь, retry и работу, которая продолжается после ответа. Для таких случаев нужны links или другая модель причинности. Нельзя объявлять critical path доказанным, если timestamps пришли с несинхронизированных часов. Нельзя считать отсутствие span доказательством отсутствия работы: его мог отфильтровать sampler.

\n

Trace также не заменяет профиль CPU, план базы данных, сетевой capture или бизнес-метрику. Он показывает наблюдаемую структуру и интервалы выбранной instrumentation. Если span широк, следующая проверка должна сузить его до конкретного adapter или внешнего вызова. Если контекст потерян, сначала восстановите boundary, а не оптимизируйте случайный участок.

\n

Критерий готовности

\n

Разбор готов, когда для одного проверочного запроса выполнены четыре условия: все использованные записи имеют объяснимую связь с root; для каждого вывода указана опора в trace; арифметика не считает вложенные или параллельные интервалы дважды; после изменения есть повторяемая проверка того же симптома и отрицательного пути. Если хотя бы одно условие не выполнено, честный результат — «причина пока не доказана». Такой ответ полезнее, чем уверенный, но неверный виновник.

\n

Проверяемые источники

" }