8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 263,
|
||
"slug": "editorial-2020-09-mechanism-tracing-basics",
|
||
"title": "Trace context без иллюзий: как связать запрос и найти задержку",
|
||
"excerpt": "Если gateway и downstream видят разные trace, waterfall превращается в набор несвязанных чисел. Разбираем traceparent, parent/child span, синтетический пример и безопасный порядок проверки.",
|
||
"contentHtml": "<p>Симптом появляется после первого внедрения структурных логов: gateway сообщает о запросе, catalog сообщает о своей операции, inventory сообщает о таймауте, но нельзя доказать, что эти записи относятся к одной истории. Время ответа — 240 миллисекунд, а в логах видны 200, 170 и 70 миллисекунд. Команда выбирает самый большой показатель и меняет timeout. Цена ошибки — лишние повторы, перегрузка downstream и задержка, которую так и не измерили.</p>\n<p>Причина обычно не в dashboard. Контекст запроса потерялся на границе между процессами или parent/child связали неправильно. Trace-id заменили новым значением, span-id скопировали из родителя, а вложенные duration сложили повторно. Тезис простой: трассировка становится полезной только тогда, когда система сохраняет один trace-id, создаёт новый span на каждой операции, передаёт текущий span как parent и проверяет интервалы до диагноза.</p>\n<h2>Что именно связывает trace context</h2>\n<p>Trace — логическая история запроса. Span — одна операция внутри этой истории. У span есть имя, начало, конец, span-id и ссылка на parent. Trace-id общий для всех связанных span. Span-id различает gateway, catalog и inventory. Parent-id отвечает на вопрос «какая операция породила эту работу», но не заменяет trace-id.</p>\n<p>На HTTP-границе контекст нужно превратить в переносимые данные. W3C Trace Context описывает для этого заголовок <code>traceparent</code>. В учебной версии 00 он имеет четыре части: версию, trace-id, parent-id и trace-flags. Получатель извлекает входной parent-id, создаёт новый span-id для своей операции и передаёт дальше уже свой span-id. Trace-id при этом остаётся прежним.</p>\n<figure><img src='/assets/editorial/2020/tracing-context-propagation-2020.svg' alt='Схема передачи trace context: gateway передаёт свой span-id, catalog создаёт дочерний span и передаёт дальше новый span-id при неизменном trace-id' loading='lazy' /><figcaption>На каждой синхронной границе меняется текущий span-id. Общий trace-id сохраняет принадлежность операций к одной истории.</figcaption></figure>\n<p>Это не бизнес-заголовок. В <code>traceparent</code> нельзя переносить токен, email, полный URL или текст исключения. Контекст должен описывать связь операций. Данные для логов и диагностики живут по отдельным правилам. Особенно опасно бездумно доверять входному контексту публичного клиента: внешний отправитель не должен одним флагом управлять внутренней стоимостью сбора.</p>\n<h2>Минимальный пример</h2>\n<p>Ниже — учебная модель. Она не открывает сеть, не подключается к collector и не показывает production-данные. В ней gateway создаёт root span, catalog становится его child, а inventory и pricing идут параллельно внутри catalog. Значения времени выбраны только для проверки арифметики.</p>\n<pre><code>const traceId = '4bf92f3577b34da6a3ce929d0e0e4736';\nconst gateway = { spanId: 'a111111111111111', parentSpanId: null,\n startMs: 0, endMs: 240 };\n\n// Gateway передаёт свой текущий span как parent следующей операции.\nconst traceparent =\n `00-${traceId}-${gateway.spanId}-01`;\n\n// Catalog создаёт новый span, но сохраняет traceId.\nconst catalog = { spanId: 'b222222222222222',\n parentSpanId: gateway.spanId, startMs: 20, endMs: 220 };\n\n// Дочерние операции catalog перекрываются во времени.\nconst pricing = { parentSpanId: catalog.spanId, startMs: 30, endMs: 70 };\nconst inventory = { parentSpanId: catalog.spanId, startMs: 30, endMs: 200 };</code></pre>\n<p>В этой модели root длится 240 миллисекунд. Catalog занимает 200, pricing — 40, inventory — 170. Adapter внутри inventory может занимать 70 миллисекунд. Эти числа нельзя сложить: adapter уже входит в inventory, а pricing идёт параллельно с inventory. Inclusive duration показывает полный интервал операции вместе с ожиданием дочерних span. Он не показывает самостоятельное время без детей.</p>\n<p>Если gateway и catalog получили разные trace-id, дерево распалось. Если catalog сохранил span-id gateway как собственный, две операции стали неразличимы. Если parent у inventory ссылается на далёкого предка, а не на непосредственный catalog, визуальный граф может выглядеть правдоподобно, но причинность станет ложной. Проверка должна ловить эти ошибки на данных, а не по цветам интерфейса.</p>\n<h2>Проверяем header до создания span</h2>\n<p>Parser должен сначала проверить форму входа. Для version 00 нужны четыре части, lowercase hex, ненулевой trace-id длиной 32 символа и ненулевой parent-id длиной 16 символов. Неизвестную версию нельзя угадывать по первым символам. Невалидный контекст нужно отклонить или обработать по заранее описанной политике, а не превратить в доверенный parent.</p>\n<pre><code>function parseTraceparent(value) {\n const parts = String(value).split('-');\n if (parts.length !== 4 || parts[0] !== '00') {\n throw new Error('unsupported traceparent');\n }\n\n const [, traceId, parentId, flags] = parts;\n if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) {\n throw new Error('invalid trace-id');\n }\n if (!/^[0-9a-f]{16}$/.test(parentId) || /^0+$/.test(parentId)) {\n throw new Error('invalid parent-id');\n }\n if (!/^[0-9a-f]{2}$/.test(flags)) {\n throw new Error('invalid trace-flags');\n }\n return { traceId, parentId, flags };\n}</code></pre>\n<p>Функция ограничена учебной задачей: version 00, строковый carrier и базовая валидация. Она не заменяет библиотеку трассировки. Реальный adapter должен учитывать правила конкретного HTTP-клиента, сервера, прокси и фреймворка. Если middleware уже извлекает контекст и создаёт span, второй слой может породить дубликаты. Сначала нужно установить владельца extract, владельца inject и место создания root.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><table><caption>Разбор типичных разрывов в одном синхронном trace</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>Gateway и catalog имеют разные trace-id</td><td>Получатель создал новый root вместо child</td><td>Сверить trace-id и parent-id в обеих span</td><td>Исправить extract и создание child на границе</td></tr><tr><td>У двух операций один span-id</td><td>Получатель скопировал ID родителя</td><td>Проверить уникальность span-id в одной истории</td><td>Генерировать новый span-id для каждой операции</td></tr><tr><td>Заголовок принят, но граф пустой</td><td>Контекст передали, а span не записали или не экспортировали</td><td>Разделить проверку propagation, recording и export</td><td>Добавить отдельный тест на каждый слой</td></tr><tr><td>Сумма дочерних duration больше ответа</td><td>Перекрывающиеся интервалы сложили как последовательные</td><td>Нанести start/end на одну шкалу времени</td><td>Считать critical path и exclusive time, а не сумму строк</td></tr><tr><td>Нулевой или чужой trace-id проходит дальше</td><td>Parser проверяет только число частей</td><td>Подать отрицательные header-примеры до создания span</td><td>Остановить обработку или создать новый root по политике границы</td></tr></tbody></table></div>\n<h2>Как читать учебный waterfall</h2>\n<p>Допустим, waterfall содержит пять span: <code>gateway.handle</code> от 0 до 240, <code>catalog.lookup</code> от 20 до 220, <code>pricing.read</code> от 30 до 70, <code>inventory.fetch</code> от 30 до 200 и <code>inventory.adapter</code> от 100 до 170 миллисекунд. У всех один trace-id. Каждый child имеет существующего parent и лежит внутри его интервала. Это минимальный набор условий, чтобы обсуждать дерево и время вместе.</p>\n<p>Поздний конец inventory — 200 миллисекунд. Pricing заканчивается на 70 и не удерживает catalog до его конца. Adapter заканчивается на 170 и находится внутри inventory. Поэтому учебный critical path проходит через gateway, catalog и inventory, а затем через adapter только как вложенный участок. Это не означает, что adapter — production bottleneck. Это означает лишь, что в данной модели он находится на поздней последовательной ветви.</p>\n<p>Exclusive time можно получить, вычтя объединение дочерних интервалов из интервала parent. Для inventory это 170 минус 70, то есть 100 миллисекунд вне adapter. Для root остаётся 40 миллисекунд вне catalog. Такой расчёт помогает не считать одно ожидание дважды, но требует общей шкалы времени и корректных границ. При clock skew, неполных timestamp и асинхронной очереди результат нельзя считать доказанным.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выбрать одну синхронную границу, например gateway → catalog. Не начинать с массовой автоинструментации.</li><li>Назначить владельцев: кто создаёт root, кто извлекает incoming context, кто создаёт child и кто внедряет outgoing context.</li><li>Зафиксировать учебный trace-id, список span и ожидаемые parent-id. Не класть в идентификаторы пользовательские данные.</li><li>Проверить положительный round-trip: inject сохраняет trace-id и текущий span-id, extract возвращает их без изменения.</li><li>Проверить отрицательный путь: нулевые ID, неверную длину, uppercase, неизвестную версию и лишнее поле. До создания span вход должен получить предсказуемый отказ.</li><li>Запустить controlled fixture с одной шкалой времени. Проверить один root, уникальность span-id, существование parent и containment child.</li><li>Сделать transport test конкретного клиента или сервера. Проверить не только carrier, но и фактическую границу, где он проходит.</li><li>Только после этого смотреть duration. Сначала — конец root, затем прямые children, перекрытия и собственное время.</li><li>Записать, что не проверено: collector, sampling, storage, clock synchronization, очередь, retries и fan-out.</li></ol>\n<h2>Отрицательный путь и ограничения</h2>\n<p>Зелёный пример показывает, как механизм работает при правильных данных. Нужнее отрицательный: нулевой trace-id должен быть отвергнут; изменённый parent-id не должен незаметно связать операцию с чужим span; неизвестная версия не должна интерпретироваться как version 00. Если входной контекст нельзя доверенно обработать, система должна иметь явное решение: отклонить его, пропустить операцию без связи или начать новый root.</p>\n<p>Trace context не создаёт наблюдаемость сам. Заголовок может пройти прокси, но span не попадёт в exporter. Exporter может работать, но библиотека не создаст span вокруг важной операции. Sampling может убрать часть истории. Эти случаи требуют разных проверок. Нельзя объявлять propagation исправной только потому, что строка header дошла до обработчика.</p>\n<p>Модель выше не подходит без изменений для очереди, fan-out, batch и продолжения работы после HTTP-ответа. У асинхронной операции может не быть одного parent, который полностью охватывает её время. Появляются links, отдельные правила корреляции и несколько часов. Пока эти условия не проверены, нельзя рисовать уверенный critical path по простой вложенности.</p>\n<p>Статья также не обещает production latency. Все интервалы в примере синтетические. Они нужны, чтобы проверить связность, арифметику и отрицательные сценарии. Реальный вывод требует transport test и trace, полученного в разрешённом окружении с известной версией библиотек.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Минимальный критерий такой: выбранная граница имеет владельца extract и inject; положительный round-trip сохраняет trace-id и меняет текущий span-id по правилам; отрицательные header-ы не создают ложные связи; fixture содержит один root и уникальные span-id; parent существует; child укладывается в parent там, где это предусмотрено моделью; duration не складывают поверх перекрытий; непроверенные production-условия перечислены.</p>\n<p>Если хотя бы один пункт неизвестен, результат нужно назвать ограниченно: «формат разобран», «учебное дерево связно» или «transport boundary прошла тест». Фраза «трассировка работает» шире доказательств. Готовность начинается там, где команда может повторить проверку, увидеть красный отрицательный путь и безопасно остановиться.</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> — официальный формат propagation, поля traceparent и tracestate, а также правила обработки контекста.</li><li><a href='https://opentelemetry.io/docs/concepts/signals/traces/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Traces</a> — официальное описание trace, span и связей между операциями.</li><li><a href='https://opentelemetry.io/docs/concepts/context-propagation/' target='_blank' rel='noopener noreferrer'>OpenTelemetry: Context propagation</a> — официальное описание передачи контекста между процессами и сервисами.</li></ul>"
|
||
}
|