8 lines
15 KiB
JSON
8 lines
15 KiB
JSON
{
|
||
"index": 264,
|
||
"slug": "editorial-2020-09-practice-tracing-basics",
|
||
"title": "Трассировка запроса: как найти задержку по одному trace",
|
||
"excerpt": "Разбираем разорванный trace: как передать trace context через границу сервисов, связать parent и child span, прочитать waterfall и остановиться, если данных недостаточно.",
|
||
"contentHtml": "<p>Сервис отвечает успешно, но один и тот же endpoint то укладывается в 40 миллисекунд, то ждёт почти секунду. В логах есть записи gateway, catalog и inventory. Связать их с одним запросом нельзя: у строк разные идентификаторы, а время запуска не совпадает. Цена ошибки — менять timeout, retry или запрос к базе вслепую. Можно убрать один видимый симптом и оставить настоящую задержку на следующем участке.</p>\n<p><strong>Тезис.</strong> Трассировка помогает не потому, что добавляет ещё один лог. Она связывает операции в дерево: общий <code>trace-id</code> описывает одну историю, <code>span-id</code> описывает отдельную операцию, а <code>parent-id</code> показывает прямую связь. На транспортной границе сервис должен передать контекст, создать свой span и передать уже его как родителя следующей операции. Если граница не передала контекст, waterfall распадается и вывод о причине задержки становится гипотезой.</p>\n<h2>Как читать один trace</h2>\n<p>Представим учебный запрос к каталогу. Gateway принимает HTTP-запрос и создаёт span <code>gateway.handle</code>. Затем он вызывает catalog. Catalog получает trace context, создаёт <code>catalog.lookup</code> с родителем gateway и запускает два дочерних участка: <code>pricing.read</code> и <code>inventory.fetch</code>. Inventory, в свою очередь, вызывает адаптер.</p>\n<p>В такой модели один trace содержит пять span. У всех один <code>trace-id</code>. Каждый span имеет собственный <code>span-id</code>. У дочернего span <code>parent-span-id</code> равен идентификатору операции, которая его вызвала. Эта связь важнее красивого имени сервиса: она показывает, кто породил ожидание.</p>\n<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>Компоненты создают разные trace-id</td><td>Сравнить trace-id у входящего и исходящего span</td><td>Передавать контекст через границу</td></tr><tr><td>Child span существует, но parent неизвестен</td><td>Сервис создал span без текущего контекста</td><td>Проверить parent-span-id и порядок времени</td><td>Создавать child из извлечённого context</td></tr><tr><td>Waterfall показывает невозможное перекрытие</td><td>Сложили вложенные duration</td><td>Проверить интервалы start/end и вложенность</td><td>Считать critical path, а не сумму всех span</td></tr><tr><td>Trace пропал после proxy или очереди</td><td>Carrier не прошёл через транспорт</td><td>Сравнить header до отправки и после получения</td><td>Проверить конкретный adapter или carrier</td></tr><tr><td>Задержка есть, но причина не видна</td><td>Нужный участок не создаёт span или не попал в sampling</td><td>Проверить покрытие, sampling и экспорт</td><td>Не объявлять виновника без следующего сигнала</td></tr></tbody></table>\n<h2>Контекст передаётся через границу</h2>\n<p>Внутри одного процесса контекст можно передать аргументом функции или средствами SDK. После HTTP-вызова, сообщения очереди или фоновой задачи получатель сам его не угадает. Для HTTP используется carrier. Один распространённый вариант — W3C <code>traceparent</code>. В версии <code>00</code> он содержит version, trace-id, parent-id и trace-flags.</p>\n<pre><code>traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01\n\n// gateway отправляет свой span как parent:\nconst outgoing = \\`00-\\${traceId}-\\${gatewaySpanId}-01\\`;\n\n// catalog создаёт новый span, но сохраняет traceId:\nconst catalogSpan = {\n traceId,\n spanId: '00f067aa0ba902b7',\n parentSpanId: gatewaySpanId,\n};</code></pre>\n<p>Код выше — учебный пример. Он не открывает сеть, не заменяет SDK и не доказывает, что конкретный proxy сохранит заголовок. Его задача — показать два инварианта: trace-id остаётся тем же, а текущий span-id меняется на каждой участвующей границе.</p>\n<p>Получатель должен проверить формат до использования. Trace-id и parent-id имеют фиксированную длину и не могут быть нулевыми. Неверный header нельзя принимать как доверенный контекст. Если header отсутствует, сервис начинает новую историю или применяет явно заданную политику. Нельзя молча приписывать запрос к случайному trace.</p>\n<figure><img src=\"/assets/editorial/2020/tracing-context-propagation-2020.svg\" alt=\"Схема передачи trace context: gateway передаёт traceparent в catalog, catalog создаёт дочерний span и передаёт новый parent дальше в inventory\" loading=\"lazy\" /><figcaption>На границе меняется parent span, но trace-id остаётся общим. Схема показывает контракт передачи, а не выполнение реального запроса.</figcaption></figure>\n<h2>Waterfall показывает путь, а не сумму строк</h2>\n<p>Пусть в учебном примере <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 миллисекунд. Эти числа придуманы для объяснения и не являются измерениями production.</p>\n<p>Pricing и inventory стартуют одновременно. Поэтому конец запроса определяется веткой inventory, а не суммой 40 и 170 миллисекунд. Время parent включает время дочерних span. Если сложить 240, 200, 40, 170 и 70, получится число, которое не описывает ни задержку запроса, ни critical path. Для анализа нужно найти цепочку зависимых операций и отдельно посмотреть участки parent, которые не закрыли child.</p>\n<figure><img src=\"/assets/editorial/2020/tracing-span-waterfall-2020.svg\" alt=\"Учебный waterfall trace на шкале 0–240 миллисекунд: pricing идёт параллельно с inventory, а adapter вложен в inventory\" loading=\"lazy\" /><figcaption>В учебном waterfall поздно завершающаяся ветка inventory определяет критический путь. Вывод применим только при общей шкале времени и корректных parent-child связях.</figcaption></figure>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите endpoint, примерный диапазон задержки, код ответа, входные условия и время наблюдения.</li><li><strong>Выберите одну историю.</strong> Найдите trace конкретного запроса по доступному идентификатору. Не смешивайте записи разных попыток.</li><li><strong>Проверьте корень.</strong> Убедитесь, что root span имеет начало и конец, а его trace-id не меняется внутри истории.</li><li><strong>Проверьте границы.</strong> Для каждого HTTP-вызова или сообщения сравните отправленный carrier с полученным. Отметьте место, где связь исчезает.</li><li><strong>Проверьте parent.</strong> У каждого child должен существовать ожидаемый родитель. Временной интервал child не должен выходить за границу parent без отдельного объяснения.</li><li><strong>Постройте критический путь.</strong> Отделите последовательные операции от параллельных. Не складывайте вложенные duration.</li><li><strong>Назовите следующий сигнал.</strong> Если самый длинный span — adapter, проверьте его запрос, очередь или внешний ответ. Один span не доказывает причину внутри себя.</li><li><strong>Проверьте отрицательную ветку.</strong> Если trace отсутствует, sampling исключил запрос или часы расходятся, остановите вывод и соберите недостающий сигнал.</li><li><strong>Сравните повтор.</strong> Повторите тот же сценарий с теми же входными условиями и убедитесь, что вывод не зависит от одной удачной истории.</li></ol>\n<h2>Что трассировка не доказывает</h2>\n<p>Наличие trace-id не означает, что история полная. Sampling может отбросить запрос. Экспорт может задержаться или завершиться ошибкой. Уровень логирования может скрыть событие. Прокси может удалить заголовок. Очередь может использовать другой carrier. Для каждой границы нужна отдельная проверка.</p>\n<p>Трассировка также не показывает автоматически бизнес-причину. Длинный span базы может быть следствием блокировки, плохого плана, холодного соединения или внешнего лимита. Название <code>inventory.fetch</code> не различает эти случаи. Следующий шаг должен читать собственный сигнал участка: план запроса, размер очереди, код внешнего ответа или время подключения.</p>\n<p>Не помещайте в trace context пароль, токен, email, полный URL с параметрами или тело запроса. Trace-id служит для связи операций. Доступ к trace и правила хранения должны учитывать, что span attributes часто попадают в журналы и хранилища наблюдаемости.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Разбор готов, если другой инженер может по одному запросу воспроизвести четыре факта: все нужные span имеют общий trace-id; каждая транспортная граница показывает отправленный и полученный context; parent-child связи согласуются с временем; критический путь объясняет задержку без сложения перекрывающихся интервалов. Для найденного участка существует следующий проверяемый сигнал.</p>\n<p>Если хотя бы один факт неизвестен, результатом должна быть запись «причина не доказана» и конкретный следующий fetch, лог или измерение. Это не провал метода. Это правильная граница вывода: trace показывает путь запроса, но не разрешает придумывать отсутствующие данные.</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> — Recommendation с форматом <code>traceparent</code>, правилами propagation и обработкой контекста.</li><li><a href=\"https://opentelemetry.io/docs/concepts/context-propagation/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Context propagation</a> — официальное описание передачи context между процессами и границами.</li><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — официальное описание traces, metrics и logs и их разных ролей.</li></ul>"
|
||
}
|