{ "index": 35, "slug": "editorial-2027-01-mechanism-debugging-decade", "title": "Trace ID связывает события, но не доказывает причину", "excerpt": "Как читать trace, log и metric вместе, проверять разрывы контекста и не принимать совпадение идентификатора за доказательство причины.", "contentHtml": "
В двух журналах найден один trace ID. Временные метки почти совпадают. Один span длится дольше остальных. Команда объявляет его причиной задержки и меняет таймаут в этом сервисе. Через день задержка возвращается: запросы ждали соединение в шлюзе, а длинный span лишь включал это ожидание. Цена ошибки — потерянное время, лишний rollback и новый побочный эффект.
\nTrace ID отвечает на вопрос «к каким данным относится эта запись?». Он не отвечает на вопрос «какая операция вызвала отказ или задержку?». Причинный вывод требует проверить структуру трассы, интервалы, статус, локальные журналы и путь, по которому запрос действительно прошёл.
\nLog фиксирует событие в одном процессе: сообщение, локальное состояние, уровень и время. Span описывает операцию: начало, конец, родителя, сервис и атрибуты. Metric агрегирует много запросов и показывает частоту, распределение или долю ошибок. Один сигнал не заменяет другой.
\nW3C Trace Context задаёт формат передачи traceparent и tracestate между HTTP-границами. Так разные сервисы могут продолжить общий контекст. Инструмент может только передать контекст, не создав подробный span. Очередь, фоновая задача, retry или библиотека без интеграции могут остаться за пределами записи.
Поэтому trace ID создаёт область поиска, а parent/child-связи задают наблюдаемую структуру. Если у span нет родителя, это не доказывает, что операция независима. Возможны потеря записи, неверное поле, sampling или отдельная работа, ошибочно попавшая в trace. Отсутствие события в одном источнике означает только, что его там не нашли.
\nСледующий код — учебный пример. Он работает с заранее заданным массивом и ничего не знает о production-трафике. Его задача — показать отрицательный путь: система должна явно отметить отсутствующего родителя, а не дорисовать целую цепочку.
\nconst 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| Симптом | Возможная причина | Проверка | Действие |
|---|---|---|---|
| Один trace ID есть в gateway и API, но ответа нет | Сервис не записал span или запрос прервался до него | Сверить access log, статус соединения, sampling и окно времени | Отметить разрыв; не называть API причиной без записи операции |
| У span есть parentSpanId, но родителя нет | Потеря span, ошибка экспорта или неверная связь | Проверить полный экспорт, формат ID и дубликаты span-id | Исправить передачу или сбор; сохранить missing parent как сигнал |
| Самый длинный span совпал с пиком latency | Span включает ожидание upstream, retry или очередь | Сопоставить дочерние интервалы, status, retry count и метрику population | Разделить время по операциям; не оптимизировать сервис по одному trace |
| В log нет записи с нужным trace ID | Поле не попало в журнал, запись отбросил collector или выбран другой ID | Проверить схему, доставку, источник и request-id на границе | Считать источник неполным и продолжить по access/metric, не делать вывод об отсутствии события |
Sampling может исключить нужный span. Tail-based filtering может оставить только часть цепочки. Collector может получить события не по порядку или отбросить запись при перегрузке. Разные часы на узлах искажают сравнение timestamps. Асинхронный consumer может законно продолжить работу после завершения parent span. Для него нужны отдельные связи producer, сообщения и consumer.
\nTrace не доказывает контрфактическое утверждение: нельзя по одной цепочке узнать, что произошло бы без конкретного вызова. Не стоит помещать персональные параметры в атрибуты и журналы. Корреляционный ключ должен помогать искать запись, а не раскрывать содержимое запроса.
\nРазбор готов, когда другой инженер получает один симптом и может повторить маршрут проверки без устной истории. В записи видны граница симптома, полный или явно неполный граф, проверенные интервалы, источник каждого вывода и отрицательный путь для отсутствующей записи. Исправление готово, когда повторный сценарий подтверждает изменение на исходном сигнале, не создаёт нового отказа по соседней метрике, а проверка missing parent или другого разрыва остаётся наблюдаемой.
\n