Files
progcode/editorial/agent-rewrites/262.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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>Цена ошибки — не только несколько миллисекунд в отчёте. Неверный диагноз закрепляет плохую конфигурацию, маскирует потерю контекста и переносит проблему на следующий релиз. Правильный разбор начинается с наблюдаемых полей: один ли это 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>Ниже — controlled example. Все значения придуманы для объяснения метода. Это не замер реального сервиса и не production-результат. Корневой 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 &gt;= start</code>. Child должен находиться внутри parent, если модель использует обычное дерево вложенных операций. Если child выходит за границы родителя, это не повод сразу рисовать красную полосу. Причиной может быть ошибка закрытия span, асинхронная работа после ответа или неверная модель связи. Запишите факт и остановите арифметику до выяснения.</p>\n<pre><code>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 &gt;= parent.start &amp;&amp; child.end &lt;= parent.end;\n}</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 мс. Поэтому путь до конца ответа проходит через <code>gateway.handle → catalog.lookup → inventory.fetch</code>. Adapter находится внутри inventory и помогает объяснить его работу, но его 70 мс уже входят в 170 мс inventory.</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>Связность trace не появляется из названий span. Клиент должен передать контекст, а принимающая сторона — извлечь его и создать новый child. Для W3C Trace Context заголовок <code>traceparent</code> содержит version, trace-id, parent-id и trace-flags. Пример ниже синтетический. Он показывает форму данных, но не является рабочим токеном и не доказывает прохождение через конкретный proxy.</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 потеряна. Если parser принимает нулевой или неверно оформленный 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/trace-context/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Trace Context</a> — формат и правила передачи trace context через HTTP.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/trace/api/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Tracing API</a> — определения Span, duration, context и требований к tracing API.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/context/api-propagators/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Propagators API</a> — правила inject/extract контекста через carrier.</li></ul>"
}