Files
progcode/editorial/agent-rewrites/264.json
T
2026-09-03 22:06:19 +03:00

8 lines
19 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": 264,
"slug": "editorial-2020-09-practice-tracing-basics",
"title": "Трассировка запроса: как доказать задержку по одному trace",
"excerpt": "Разбираем учебный разрыв trace: как проверить traceparent на границах сервисов, не сложить перекрывающиеся span дважды и выбрать следующий измеримый сигнал.",
"contentHtml": "<p>После условного выката дежурный инженер получает два одинаковых запроса: один завершился за 40 миллисекунд, другой ждал почти секунду. Gateway, catalog и inventory записали события, но их нельзя уверенно собрать в одну историю: идентификаторы различаются, а времена запуска не совпадают. Это учебный incident, поэтому числа ниже придуманы для проверки метода, а не выданы за замер production.</p>\n<p>Первое действие — взять один запрос и сравнить контекст на каждой границе: какой <code>traceparent</code> отправил gateway, что извлёк catalog и какой span стал родителем следующего вызова. Цена ошибки здесь практическая: если сразу увеличить timeout или retry, можно скрыть потерю контекста и перенести задержку дальше. Задача статьи — показать, какие факты trace доказывает, а где нужен следующий сигнал.</p>\n<h2>Сценарий: один запрос и пять span</h2>\n<p>Разберём контролируемую схему. Gateway принимает HTTP-запрос и создаёт корневой span <code>gateway.handle</code>. Он вызывает catalog. Catalog создаёт <code>catalog.lookup</code> как дочернюю операцию и параллельно запускает <code>pricing.read</code> и <code>inventory.fetch</code>. Inventory вызывает <code>inventory.adapter</code>. Так получается пять span в одной простой trace-истории.</p>\n<p><code>trace-id</code> идентифицирует всю историю, а <code>span-id</code> — одну операцию. У каждого span может быть не более одного parent в обычном дереве, но дочерних span может быть несколько. Поэтому имя сервиса ещё ничего не доказывает: нужно проверить идентификаторы, parent-child связь и интервалы <code>start</code>/<code>end</code>. Ниже приведены только значения учебной модели.</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>Строки нельзя собрать в один запрос</td><td>На границе создан новый root или потерян context</td><td>Сравнить trace-id во входящем и исходящем span</td><td>Проверить extract/inject и carrier</td></tr><tr><td>Child есть, но parent неизвестен</td><td>Span создан без активного Context</td><td>Сверить parent-span-id и момент создания</td><td>Создавать child из извлечённого Context</td></tr><tr><td>Сумма duration намного больше ответа</td><td>Вложенные или параллельные интервалы посчитали повторно</td><td>Сопоставить start/end и перекрытия</td><td>Искать критический путь, а не сумму строк</td></tr><tr><td>Trace обрывается после proxy или очереди</td><td>Carrier не прошёл через транспорт либо сработал sampling</td><td>Сравнить заголовок до отправки и после получения</td><td>Проверить конкретную boundary и полноту выборки</td></tr><tr><td>Самый длинный span не объясняет ответ</td><td>Он перекрывается с поздней веткой или слишком широк внутри себя</td><td>Найти последний end и следующий внутренний сигнал</td><td>Проверить адаптер, очередь или внешний ответ</td></tr></tbody></table></div>\n<h2>Контекст на HTTP-границе</h2>\n<p>Внутри процесса Context можно передать средствами SDK. После HTTP-вызова или сообщения очереди получатель не угадывает его по имени сервиса. Propagator записывает context в carrier, например в HTTP-заголовки, а принимающая сторона извлекает его обратно. Это две разные операции: <em>inject</em> выполняет отправитель, <em>extract</em> — получатель.</p>\n<p>В W3C Trace Context заголовок <code>traceparent</code> версии <code>00</code> имеет четыре поля: version, trace-id, parent-id и trace-flags. Для <code>trace-id</code> используется 32 строчных шестнадцатеричных символа, для parent-id — 16; нулевые значения недействительны. При некорректном контексте реализация должна его проигнорировать, а решение о новой root-истории принимает политика инструмента.</p>\n<p>Порядок для исходящего HTTP-вызова важен. Получив запрос, catalog сначала извлекает remote context и создаёт свой server span как child. Перед вызовом inventory он создаёт span исходящего обращения, обычно client span, делает его текущим и только потом inject-ит его context в новый carrier. Inventory извлекает carrier и создаёт свой server span. Тогда в исходящем заголовке меняется parent-id текущего вызова, а trace-id сохраняется.</p>\n<pre><code>traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\n\n// Схема операций SDK, без сетевого вызова:\nconst incoming = propagator.extract(request.headers);\nconst serverSpan = tracer.startSpan('catalog.lookup', { parent: incoming });\nconst outgoing = tracer.startSpan('inventory.fetch', { parent: serverSpan });\npropagator.inject(outgoing.context(), requestToInventory.headers);\n\n// На стороне inventory:\nconst remote = propagator.extract(requestToInventory.headers);\nconst inventorySpan = tracer.startSpan('inventory.server', { parent: remote });</code></pre>\n<p>Этот фрагмент показывает последовательность, но не является готовым API для конкретного языка: названия методов у SDK различаются. Он также не обещает, что proxy сохранит заголовок. Для воспроизводимой проверки нужно записать carrier непосредственно перед отправкой и сразу после extract на стороне получателя.</p>\n<figure><img src=\"/assets/editorial/2020/tracing-context-propagation-2020.svg\" alt=\"Схема передачи traceparent: gateway передаёт context в catalog, а catalog создаёт новый дочерний span для inventory\" loading=\"lazy\" /><figcaption>На каждой границе создаётся новый span, но при корректной propagation trace-id остаётся общим. Схема показывает контракт, а не сетевой прогон.</figcaption></figure>\n<h2>Waterfall: путь запроса, а не сумма строк</h2>\n<p>В учебной модели root <code>gateway.handle</code> идёт от 0 до 240 мс, <code>catalog.lookup</code> — от 20 до 220, <code>pricing.read</code> — от 30 до 70, а <code>inventory.fetch</code> — от 30 до 200. Внутри inventory адаптер занимает интервал от 100 до 170 мс. Pricing и inventory стартуют одновременно, поэтому их duration перекрываются.</p>\n<p>Inclusive duration родителя включает работу children. Если сложить 240, 200, 40, 170 и 70, получится 720 мс, хотя root завершается за 240. Это не задержка запроса. Правильный вопрос другой: какая последовательность интервалов удерживает ответ до конца? В этой модели поздний child catalog — inventory, а pricing остаётся параллельной веткой, не входящей в критический путь после 70 мс.</p>\n<p>Для этой простой шкалы критическая цепь раскладывается так: 0–20 мс до catalog, 20–30 мс до его children, 30–100 мс до adapter, 100–170 мс работы adapter, 170–200 мс после adapter, 200–220 мс после children и 220–240 мс до ответа. Сумма этих неперекрывающихся участков равна 240 мс. Это разложение относится к выбранной модели; для асинхронных links, retry и разных часов нужен другой анализ.</p>\n<figure><img src=\"/assets/editorial/2020/tracing-span-waterfall-2020.svg\" alt=\"Учебный waterfall от 0 до 240 миллисекунд: pricing идёт параллельно с inventory, а adapter вложен в inventory\" loading=\"lazy\" /><figcaption>Позднее завершение inventory определяет конец ветки catalog. Вложенный adapter уже входит в interval inventory и не должен добавляться второй раз.</figcaption></figure>\n<h2>Воспроизводимая проверка интервалов</h2>\n<p>Ниже — самодостаточный пример для Node.js без внешних пакетов. Он проверяет duration, наличие parent и то, какой child catalog завершился последним. Код не строит полноценный trace backend и не учитывает асинхронные links; его граница намеренно ограничена одной согласованной шкалой.</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\nconst byName = new Map(spans.map((span) =&gt; [span.name, span]));\nconst duration = (span) =&gt; span.end - span.start;\nconst invalid = spans.filter((span) =&gt; {\n const parent = span.parent ? byName.get(span.parent) : null;\n return parent &amp;&amp; (span.start &lt; parent.start || span.end &gt; parent.end);\n});\nconst catalogChildren = spans.filter((span) =&gt; span.parent === 'catalog.lookup');\nconst latestChild = catalogChildren.reduce((latest, span) =&gt;\n span.end &gt; latest.end ? span : latest,\n);\n\nconsole.log({\n rootDuration: duration(byName.get('gateway.handle')),\n latestCatalogChild: latestChild.name,\n latestCatalogChildEnd: latestChild.end,\n invalidParentRanges: invalid.length,\n});\n// { rootDuration: 240, latestCatalogChild: 'inventory.fetch',\n// latestCatalogChildEnd: 200, invalidParentRanges: 0 }</code></pre>\n<p>Результат подтверждает три локальных факта: root длится 240 мс, inventory завершается позже pricing, каждый child находится внутри parent. Одна шкала — условие входных данных, а не вывод программы. Код не подтверждает, что catalog действительно получил traceparent, что sampling сохранил все span или что задержка внутри adapter вызвана базой. Для последнего вывода нужен собственный сигнал adapter.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите endpoint, диапазон задержки, код ответа, входные условия и время наблюдения.</li><li><strong>Выберите одну историю.</strong> Найдите конкретный trace по доступному идентификатору. Не смешивайте повторные попытки и записи разных часов.</li><li><strong>Проверьте root.</strong> Убедитесь, что корневой span имеет start и end, а sampling и экспорт не скрыли критическую часть.</li><li><strong>Проверьте transport boundary.</strong> Сравните исходящий carrier с тем, что реально извлёк получатель. Отметьте первую границу, где trace-id исчезает.</li><li><strong>Проверьте формат.</strong> Для W3C context проверьте version, длины полей, строчные hex-символы и запрет нулевых trace-id и parent-id.</li><li><strong>Проверьте parent-child.</strong> Сверьте ожидаемого родителя и убедитесь, что child не выходит за его интервал в выбранной модели.</li><li><strong>Постройте critical path.</strong> Отделите последовательные участки от параллельных. Inclusive duration не складывайте со временем вложенного span.</li><li><strong>Назовите следующий сигнал.</strong> Для широкого span выберите один fetch: запрос адаптера, размер очереди, внешний код ответа, профиль или источник времени.</li><li><strong>Повторите проверку.</strong> Повторите тот же сценарий после изменения и отдельно проверьте отрицательный путь: потерянный carrier, sampling или невалидный timestamp.</li></ol>\n<h2>Границы вывода и безопасность</h2>\n<p>Trace показывает наблюдаемую структуру и интервалы, но не автоматически бизнес-причину. Длинный span базы может быть следствием блокировки, плохого плана, холодного соединения или внешнего лимита. Если span заканчивается после parent, сначала зафиксируйте противоречие: это может быть раннее закрытие, асинхронная работа или неверно выбранная модель связи.</p>\n<p>Обычное дерево плохо описывает fan-out, очередь, retry и работу после ответа. Для операций с несколькими причинными источниками могут понадобиться links, а для разнесённых часов — проверка источника времени и допустимой погрешности. Отсутствие span не доказывает отсутствие работы: запись могла не попасть в sampling, экспорт мог задержаться или instrumentation могла не охватить участок.</p>\n<p>Не передавайте в trace context пароль, токен, email, тело запроса или полный URL с параметрами. Trace-id нужен для корреляции, а не для хранения полезной нагрузки. Если следующий сигнал содержит персональные данные, применяйте правила доступа и маскирования вашей системы наблюдаемости.</p>\n<h2>Критерий готовности</h2>\n<p>Разбор готов, когда для одного проверочного запроса выполнены четыре условия: записи связаны с root и имеют объяснимые parent-child отношения; на каждой транспортной границе указан фактически отправленный и полученный context; критический путь рассчитан без двойного счёта вложенных и параллельных интервалов; после изменения повторён тот же симптом и отрицательный путь. Если один факт неизвестен, честный результат — «причина пока не доказана» и конкретный следующий fetch.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.w3.org/TR/trace-context/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C Trace Context</a> — формат <code>traceparent</code>, ограничения trace-id и parent-id и правила обработки входящего context.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/trace/api/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Tracing API</a> — модель Span, parent Context, root/child связи, timestamps и links.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/context/api-propagators/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry Propagators API</a> — обязанности inject/extract и работа Propagator с carrier на удалённых вызовах.</li></ul>"
}