8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 262,
|
||
"slug": "editorial-2020-09-field-tracing-basics",
|
||
"title": "Как читать распределённый trace: duration, parent и critical path",
|
||
"excerpt": "Практический разбор медленного запроса: как проверить связность trace, не сложить вложенные интервалы дважды и выбрать следующую проверку вместо поспешного увеличения timeout.",
|
||
"contentHtml": "<p>На учебном стенде инженер ждёт ответ API за 240 мс и видит в одном trace несколько span: gateway, catalog, pricing и inventory. Команда складывает все duration, получает 720 мс и начинает искать «лишнюю» задержку. Другая команда видит самый длинный span inventory и сразу увеличивает timeout. Обе реакции могут ошибиться. Первая удваивает время вложенных операций. Вторая меняет лимит, но не проверяет, кто удерживает ответ.</p>\n<p>Ниже — controlled example с вымышленными интервалами, а не запись production-инцидента. Цена ошибки всё равно реальна как инженерная ловушка: неверный диагноз закрепляет плохую конфигурацию, маскирует потерю контекста и переносит проблему на следующий релиз. Правильный разбор начинается с наблюдаемых полей: один ли это trace, кто непосредственный родитель span, где начинается и заканчивается каждый интервал, какая ветка завершается последней.</p>\n<h2>Тезис: сначала связность, потом арифметика</h2>\n<p>Trace показывает путь операции через границы компонентов. Span описывает отдельный участок этого пути. Parent указывает на непосредственную родительскую операцию, а не просто на предыдущий по времени вызов. Duration равен разности <code>end - start</code>. Эти факты объясняют задержку только тогда, когда записи относятся к одной логической истории и используют сопоставимую шкалу времени.</p>\n<p>Родительский span обычно включает интервалы своих children. Поэтому duration родителя и duration child нельзя складывать как последовательные операции. Если две дочерние ветки перекрываются, их время тоже не складывается. Сначала нужно найти интервал, который удерживает ответ до конца, затем проверить его собственные участки.</p>\n<h2>Учебный пример с одной шкалой</h2>\n<p>Все значения ниже придуманы для объяснения метода. Корневой span <code>gateway.handle</code> живёт от 0 до 240 мс. Он вызывает <code>catalog.lookup</code> от 20 до 220 мс. Внутри catalog две параллельные ветки: <code>pricing.read</code> от 30 до 70 мс и <code>inventory.fetch</code> от 30 до 200 мс. Inventory запускает <code>inventory.adapter</code> от 100 до 170 мс. Такой пример проверяет арифметику одной модели, но не измеряет настоящий сервис.</p>\n<div class=\"table-scroll\"><table><caption>Синтетический waterfall одного trace</caption><thead><tr><th scope=\"col\">Span</th><th scope=\"col\">Parent</th><th scope=\"col\">Интервал</th><th scope=\"col\">Duration</th></tr></thead><tbody><tr><td><code>gateway.handle</code></td><td>root</td><td><code>0–240 ms</code></td><td><code>240 ms</code></td></tr><tr><td><code>catalog.lookup</code></td><td><code>gateway.handle</code></td><td><code>20–220 ms</code></td><td><code>200 ms</code></td></tr><tr><td><code>pricing.read</code></td><td><code>catalog.lookup</code></td><td><code>30–70 ms</code></td><td><code>40 ms</code></td></tr><tr><td><code>inventory.fetch</code></td><td><code>catalog.lookup</code></td><td><code>30–200 ms</code></td><td><code>170 ms</code></td></tr><tr><td><code>inventory.adapter</code></td><td><code>inventory.fetch</code></td><td><code>100–170 ms</code></td><td><code>70 ms</code></td></tr></tbody></table></div>\n<p>Из таблицы нельзя заключить, что запрос занял 240 + 200 + 40 + 170 + 70 мс. Children находятся внутри parent. Pricing и inventory идут параллельно с 30 до 70 мс. Поэтому pricing не добавляет 40 мс после inventory. Корневой ответ заканчивается в 240 мс, а не в сумме всех строк.</p>\n<h2>Как duration превращается в проверяемый вывод</h2>\n<p>Сначала проверьте форму интервалов. Для каждого span должно выполняться <code>end >= start</code>. Child должен находиться внутри parent, если модель использует обычное дерево вложенных операций. Если child выходит за границы родителя, это не повод сразу рисовать красную полосу. Причиной может быть ошибка закрытия span, асинхронная работа после ответа или неверная модель связи. Запишите факт и остановите арифметику до выяснения.</p>\n<pre><code>const spans = [{ name: 'gateway.handle', parent: null, start: 0, end: 240 }, { name: 'catalog.lookup', parent: 'gateway.handle', start: 20, end: 220 }, { name: 'pricing.read', parent: 'catalog.lookup', start: 30, end: 70 }, { name: 'inventory.fetch', parent: 'catalog.lookup', start: 30, end: 200 }, { name: 'inventory.adapter', parent: 'inventory.fetch', start: 100, end: 170 }]; const duration = (span) => span.end - span.start; const isInside = (child, parent) => child.start >= parent.start && child.end <= parent.end; const byName = new Map(spans.map((span) => [span.name, span])); if (duration(byName.get('gateway.handle')) !== 240) throw new Error('root duration'); if (!isInside(byName.get('inventory.adapter'), byName.get('inventory.fetch'))) throw new Error('child is outside parent');</code></pre>\n<p>Этот фрагмент проверяет только учебные интервалы и две явные инварианты. Он не исправляет часы разных машин, не восстанавливает потерянный span и не доказывает, что transport передал контекст. В настоящей системе сначала установите, откуда пришли timestamps и как инструмент описывает асинхронные связи.</p>\n<figure><img src=\"/assets/editorial/2020/tracing-critical-path-2020.svg\" alt=\"Учебный waterfall: gateway охватывает catalog, pricing и inventory идут параллельно, adapter вложен в inventory\" loading=\"lazy\" /><figcaption>Схема показывает только controlled example. Pricing и inventory пересекаются, поэтому их inclusive duration нельзя складывать. Позднее завершение inventory определяет конец ветки catalog.</figcaption></figure>\n<h2>Critical path без двойного счёта</h2>\n<p>В этом примере последний child catalog — <code>inventory.fetch</code>: он заканчивается в 200 мс, тогда как pricing заканчивается в 70 мс. Поэтому в пределах этой synthetic-модели путь к концу catalog проходит через <code>gateway.handle → catalog.lookup → inventory.fetch</code>. Это выбор ветки по времени завершения, а не утверждение, что самый длинный span всегда является причиной задержки.</p>\n<p>Для более точной проверки разложите выбранные интервалы на exclusive-участки. У gateway остаются 40 мс вне catalog: 0–20 и 220–240. У catalog остаются 30 мс вне объединения children: 20–30 и 200–220. У inventory остаются 100 мс вне adapter: 30–100 и 170–200. Adapter даёт 70 мс собственного участка. Сумма <code>40 + 30 + 100 + 70 = 240</code> мс совпадает с root duration. Это проверка разложения конкретной модели, а не универсальный алгоритм для любого trace-хранилища.</p>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Возможная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Все duration в сумме больше root</td><td>Вложенные span посчитали повторно</td><td>Сверить интервалы parent и child</td><td>Считать overlap и exclusive-участки</td></tr><tr><td>У соседнего сервиса новый trace-id</td><td>Context не передали или создали новый root</td><td>Сравнить traceparent на границе</td><td>Проверить inject/extract и решение о новой границе</td></tr><tr><td>Child заканчивается после parent</td><td>Span закрыли рано или работа асинхронна</td><td>Сопоставить lifecycle операции и timestamps</td><td>Исправить закрытие либо описать link/async-модель</td></tr><tr><td>Самый длинный span не объясняет ответ</td><td>Он перекрывается с другой веткой</td><td>Найти ветку с последним end</td><td>Проверить critical path, а не максимум duration</td></tr><tr><td>Trace обрывается на proxy</td><td>Sampler, фильтр или transport не сохранил context</td><td>Сравнить входной и исходящий carrier</td><td>Добавить точечную проверку boundary и не обещать полноту</td></tr></tbody></table></div>\n<h2>Контекст на границе сервиса</h2>\n<p>Материал датирован сентябрём 2020 года. К этому моменту W3C Trace Context Level 1 уже был Recommendation, а спецификация OpenTelemetry ещё менялась. Поэтому здесь используется стабильная форма HTTP-заголовка W3C и общая модель parent/child; статья не обещает современный стабильный SDK, collector или готовую платформу наблюдаемости.</p>\n<p>Связность trace не появляется из названий span. Клиент должен передать контекст, а принимающая сторона — извлечь его и создать новый child. Для версии <code>00</code> W3C Trace Context заголовок <code>traceparent</code> содержит version, 32-символьный trace-id, 16-символьный parent-id и trace-flags. Нулевые ID недействительны, а дополнительные флаги этой версии зарезервированы. Flags — это сигнал от вызывающей стороны, а не безусловная команда для получателя.</p>\n<pre><code>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</code></pre>\n<p>Если catalog создаёт новый root, downstream trace может выглядеть аккуратно, но связь с gateway потеряна. Если реализация принимает нулевой или неверно оформленный ID вместо того, чтобы отклонить контекст, система получает ложную иерархию. Если выборка не сохраняет часть span, trace остаётся неполным. В каждом случае отрицательный путь важнее красивого графика: нужно уметь сказать «связь не доказана».</p>\n<h2>Порядок действий</h2>\n<ol><li>Сформулируйте симптом без причины: какой ответ задержан, в каком диапазоне и какую ошибку может вызвать неверная правка.</li><li>Возьмите один trace и выпишите trace-id, span-id, parent-id, start и end для нужных записей.</li><li>Проверьте, что записи относятся к одной истории и parent образует допустимое дерево.</li><li>Проверьте интервалы: duration равен <code>end - start</code>, child не выходит за parent в выбранной модели.</li><li>Нарисуйте waterfall и отметьте перекрытия. Не складывайте inclusive duration.</li><li>Найдите ветку с самым поздним завершением и разложите её на exclusive-участки.</li><li>Выберите одну следующую проверку: transport boundary, adapter, sampler или источник времени.</li><li>Повторите тот же trace-level анализ после изменения и сравните заранее выбранный сигнал.</li></ol>\n<h2>Ограничения метода</h2>\n<p>Обычное дерево span плохо описывает fan-out с несколькими родителями, очередь, retry и работу, которая продолжается после ответа. Для таких случаев нужны links или другая модель причинности. Нельзя объявлять critical path доказанным, если timestamps пришли с несинхронизированных часов. Нельзя считать отсутствие span доказательством отсутствия работы: его мог отфильтровать sampler.</p>\n<p>Trace также не заменяет профиль CPU, план базы данных, сетевой capture или бизнес-метрику. Он показывает наблюдаемую структуру и интервалы выбранной instrumentation. Если span широк, следующая проверка должна сузить его до конкретного adapter или внешнего вызова. Если контекст потерян, сначала восстановите boundary, а не оптимизируйте случайный участок.</p>\n<h2>Критерий готовности</h2>\n<p>Разбор готов, когда для одного проверочного запроса выполнены четыре условия: все использованные записи имеют объяснимую связь с root; для каждого вывода указана опора в trace; арифметика не считает вложенные или параллельные интервалы дважды; после изменения есть повторяемая проверка того же симптома и отрицательного пути. Если хотя бы одно условие не выполнено, честный результат — «причина пока не доказана». Такой ответ полезнее, чем уверенный, но неверный виновник.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.w3.org/TR/2020/REC-trace-context-1-20200206/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Trace Context Level 1, Recommendation от 6 февраля 2020 года</a> — формат <code>traceparent</code>, его поля, валидация ID и правила trace-flags.</li><li><a href=\"https://github.com/open-telemetry/opentelemetry-specification/releases/tag/v0.5.0\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Specification v0.5.0</a> — зафиксированная историческая точка спецификации; ссылка нужна для датированной рамки, а не для утверждения о современной стабильности.</li><li><a href=\"https://github.com/open-telemetry/opentelemetry-specification/blob/v0.5.0/CHANGELOG.md\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Specification v0.5.0 CHANGELOG</a> — изменения версии, использованные для проверки исторического ограничения.</li></ul>"
|
||
}
|