{ "index": 35, "slug": "editorial-2027-01-mechanism-debugging-decade", "title": "Trace ID связывает события, но не доказывает причину", "excerpt": "Как читать trace, log и metric вместе, проверять разрывы контекста и не принимать совпадение идентификатора за доказательство причины.", "contentHtml": "

В двух журналах найден один trace ID. Временные метки почти совпадают. Один span длится дольше остальных. Команда объявляет его причиной задержки и меняет таймаут в этом сервисе. Через день задержка возвращается: запросы ждали соединение в шлюзе, а длинный span лишь включал это ожидание. Цена ошибки — потерянное время, лишний rollback и новый побочный эффект.

\n

Trace ID отвечает на вопрос «к каким данным относится эта запись?». Он не отвечает на вопрос «какая операция вызвала отказ или задержку?». Причинный вывод требует проверить структуру трассы, интервалы, статус, локальные журналы и путь, по которому запрос действительно прошёл.

\n

Механизм: три сигнала и три разных вопроса

\n

Log фиксирует событие в одном процессе: сообщение, локальное состояние, уровень и время. Span описывает операцию: начало, конец, родителя, сервис и атрибуты. Metric агрегирует много запросов и показывает частоту, распределение или долю ошибок. Один сигнал не заменяет другой.

\n

W3C Trace Context задаёт формат передачи traceparent и tracestate между HTTP-границами. Так разные сервисы могут продолжить общий контекст. Инструмент может только передать контекст, не создав подробный span. Очередь, фоновая задача, retry или библиотека без интеграции могут остаться за пределами записи.

\n

Поэтому trace ID создаёт область поиска, а parent/child-связи задают наблюдаемую структуру. Если у span нет родителя, это не доказывает, что операция независима. Возможны потеря записи, неверное поле, sampling или отдельная работа, ошибочно попавшая в trace. Отсутствие события в одном источнике означает только, что его там не нашли.

\n
\"Сравнение
Корреляционный ключ связывает записи. Причину подтверждает только согласованный набор независимых признаков.
\n

Пример: найти разрыв, а не назначить виновника

\n

Следующий код — учебный пример. Он работает с заранее заданным массивом и ничего не знает о production-трафике. Его задача — показать отрицательный путь: система должна явно отметить отсутствующего родителя, а не дорисовать целую цепочку.

\n
const spans = [\n  { traceId: 't-7', spanId: 'gateway', parentSpanId: '', service: 'gateway', durationMs: 22 },\n  { traceId: 't-7', spanId: 'api', parentSpanId: 'gateway', service: 'api', durationMs: 81 },\n  { traceId: 't-7', spanId: 'db', parentSpanId: 'missing', service: 'db', durationMs: 4 }\n];\n\nconst byId = new Map(spans.map((span) => [span.spanId, span]));\nconst links = spans.map((span) => ({\n  service: span.service,\n  parent: span.parentSpanId\n    ? (byId.has(span.parentSpanId) ? 'present' : 'missing')\n    : 'root'\n}));\n\nconsole.log(links);\n// gateway: root; api: present; db: missing
\n

Результат даёт один проверяемый факт: у db нет родителя в принятом наборе. Он не говорит, что db вызвал задержку. Следующая проверка зависит от вопроса. Нужно узнать, потерялся ли span, не перепуталось ли поле parentSpanId, не создалась ли операция вне контекста и не отфильтровал ли сборщик запись.

\n

Длительность тоже требует контекста. Если gateway ждёт upstream 800 мс, эти 800 мс могут включать DNS, установку соединения, очередь, retry и чтение ответа. Долгий span показывает время, проведённое внутри его границ. Он не раскладывает это время по причинам без дочерних span-ов или дополнительных журналов.

\n

Симптомы и проверяемые действия

\n
Симптом → причина → проверка → действие
СимптомВозможная причинаПроверкаДействие
Один trace ID есть в gateway и API, но ответа нетСервис не записал span или запрос прервался до негоСверить access log, статус соединения, sampling и окно времениОтметить разрыв; не называть API причиной без записи операции
У span есть parentSpanId, но родителя нетПотеря span, ошибка экспорта или неверная связьПроверить полный экспорт, формат ID и дубликаты span-idИсправить передачу или сбор; сохранить missing parent как сигнал
Самый длинный span совпал с пиком latencySpan включает ожидание upstream, retry или очередьСопоставить дочерние интервалы, status, retry count и метрику populationРазделить время по операциям; не оптимизировать сервис по одному trace
В log нет записи с нужным trace IDПоле не попало в журнал, запись отбросил collector или выбран другой IDПроверить схему, доставку, источник и request-id на границеСчитать источник неполным и продолжить по access/metric, не делать вывод об отсутствии события
\n

Действия по порядку

\n
  1. Зафиксировать конверт симптома: метод, маршрут, статус, размер ответа, timestamp, длительность, trace ID и границу, на которой получена запись.
  2. Проверить формат trace-id и span-id. Убедиться, что сервисы не меняют trace ID без явной новой границы и не смешивают его с request-id.
  3. Построить граф parent/child. Отдельно отметить root, missing parent, duplicate span-id, пустой service.name и операции с разными trace ID.
  4. Сверить start/end span-ов с локальными временными метками. Учесть clock skew, асинхронную передачу, retry, очередь и время ожидания соединения.
  5. Сопоставить span с application log по span-id или request-id. Metric использовать для проверки масштаба: единичный trace должен быть сопоставим с общей картиной запросов.
  6. Сформулировать узкий вывод. Например: «gateway наблюдал задержку чтения ответа» или «контекст потерян между API и worker». Не писать «API был причиной» без различающего доказательства.
  7. Только после этого менять код, конфигурацию или лимит. Повторить тот же сценарий и проверить, исчез ли исходный симптом, не ухудшились ли соседние метрики и сохранился ли контекст.
\n

Ограничения

\n

Sampling может исключить нужный span. Tail-based filtering может оставить только часть цепочки. Collector может получить события не по порядку или отбросить запись при перегрузке. Разные часы на узлах искажают сравнение timestamps. Асинхронный consumer может законно продолжить работу после завершения parent span. Для него нужны отдельные связи producer, сообщения и consumer.

\n

Trace не доказывает контрфактическое утверждение: нельзя по одной цепочке узнать, что произошло бы без конкретного вызова. Не стоит помещать персональные параметры в атрибуты и журналы. Корреляционный ключ должен помогать искать запись, а не раскрывать содержимое запроса.

\n

Критерий готовности

\n

Разбор готов, когда другой инженер получает один симптом и может повторить маршрут проверки без устной истории. В записи видны граница симптома, полный или явно неполный граф, проверенные интервалы, источник каждого вывода и отрицательный путь для отсутствующей записи. Исправление готово, когда повторный сценарий подтверждает изменение на исходном сигнале, не создаёт нового отказа по соседней метрике, а проверка missing parent или другого разрыва остаётся наблюдаемой.

\n

Проверяемые источники

\n" }