Files

8 lines
22 KiB
JSON
Raw Permalink 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": 35,
"slug": "editorial-2027-01-mechanism-debugging-decade",
"title": "Trace ID связывает события, но не доказывает причину",
"excerpt": "Как читать trace, log и metric вместе, проверять разрывы контекста и не принимать совпадение идентификатора за доказательство причины.",
"contentHtml": "<p>В двух журналах найден один trace ID. Временные метки почти совпадают, а один span заметно длиннее соседних. Команда объявляет его причиной задержки и увеличивает таймаут сервиса. На следующем пике задержка возвращается: запросы ждали соединение в шлюзе, а длинный span только включал это ожидание. Ошибка стоила времени, rollback и нового побочного эффекта.</p>\n<p>Trace ID отвечает на вопрос «к каким данным относится запись?». Он не отвечает на вопрос «какая операция вызвала отказ или задержку?». Чтобы перейти от корреляции к рабочей гипотезе, нужно сверить граф span-ов, интервалы, статусы, локальные события, метрики и реальный путь запроса.</p>\n<h2>Сначала разделите четыре сущности</h2>\n<p>Trace — вся наблюдаемая цепочка, которая может проходить через несколько компонентов. Span — отдельная операция с началом, концом, атрибутами и связью с другой операцией. Log — запись события, произошедшего в процессе. Metric — агрегированное измерение за окно времени: счётчик, распределение, доля или значение. Эти сущности дополняют друг друга, но не имеют одинаковой доказательной силы.</p>\n<p>В стандарте W3C Trace Context поле <code>traceparent</code> переносит версию, <code>trace-id</code>, <code>parent-id</code> и флаги. <code>trace-id</code> идентифицирует весь trace, а <code>parent-id</code> показывает, какой идентификатор операции передал вызывающий компонент. Это контракт передачи контекста, а не протокол доказательства причинности.</p>\n<p>Для HTTP такой контекст передаётся заголовками <code>traceparent</code> и необязательным <code>tracestate</code>. Компонент может только переслать полученный контекст, не создав подробный span. Поэтому наличие одинакового <code>trace-id</code> в двух записях ещё не говорит, что между ними корректно записана связь parent/child или что одна операция вызвала другую.</p>\n<p>OpenTelemetry прямо разделяет traces, metrics и logs: trace показывает путь запроса, metric — измерение во время работы, log — запись события. На практике полезный вывод появляется на пересечении сигналов. Trace локализует участок, log объясняет локальное состояние, metric проверяет, является ли наблюдение единичным или массовым.</p>\n<figure><img src=\"/assets/editorial/2027/debugging-decade-2027-signal-tool-limit-table.svg\" alt=\"Сравнение log, span, metric и trace ID: вопрос, для которого полезен сигнал, соседняя проверка и вывод, который нельзя сделать автоматически\" loading=\"lazy\" /><figcaption>Trace ID сужает поиск. Причину задержки подтверждает согласованный набор наблюдений, а не самое заметное значение в одном сигнале.</figcaption></figure>\n<h2>Почему одинаковый ID не равен причине</h2>\n<p>У trace есть две разные структуры. Идентификатор собирает записи в одну область поиска, а parent/child-связи описывают наблюдаемое отношение операций. Даже корректное отношение не означает, что родитель «виноват»: родитель может включать ожидание очереди, DNS, установку соединения, retry или чтение ответа. Для причины нужно найти различающий признак внутри интервала.</p>\n<p>Рассмотрим запрос <code>GET /profile</code>, который проходит через gateway, API и worker. Gateway записал 802 миллисекунды, API — 91 миллисекунду, worker — 14 миллисекунд. Если внутри gateway нет span ожидания пула соединений, число 802 показывает длительность границы gateway, но не объясняет все 802 миллисекунды. Утверждение «медленный API» противоречит этим данным: его дочерний интервал покрывает только часть времени.</p>\n<p>С sampling нужно быть особенно осторожным. Флаг <code>sampled</code> в W3C Trace Context сообщает о решении записи, но не гарантирует, что каждая система сохранила все события. Отдельный span может не попасть в экспорт, collector может быть перегружен, а часть пути может проходить через библиотеку без инструментирования. Отсутствующий span — это разрыв наблюдения, а не доказательство отсутствия операции.</p>\n<h2>Воспроизводимый отрицательный путь</h2>\n<p>Ниже учебная проверка заранее записанного набора. Она не подключается к production и не определяет виновный сервис. Её задача — не дорисовать связь, если заявленный родитель отсутствует, и вернуть данные для следующей проверки.</p>\n<pre><code>const spans = [\n { traceId: 't-7', spanId: 'gateway', parentSpanId: null, service: 'gateway', durationMs: 802 },\n { traceId: 't-7', spanId: 'api', parentSpanId: 'gateway', service: 'api', durationMs: 91 },\n { traceId: 't-7', spanId: 'worker', parentSpanId: 'missing', service: 'worker', durationMs: 14 }\n];\n\nfunction inspectParents(items) {\n const byId = new Map(items.map((span) =&gt; [span.spanId, span]));\n\n return items.map((span) =&gt; ({\n service: span.service,\n durationMs: span.durationMs,\n parent: span.parentSpanId === null\n ? 'root'\n : (byId.has(span.parentSpanId) ? 'present' : 'missing')\n }));\n}\n\nconsole.log(inspectParents(spans));\n// gateway: root; api: present; worker: missing</code></pre>\n<p>Результат подтверждает только три свойства набора: gateway — корень, api ссылается на существующий span, worker ссылается на отсутствующий. Он не подтверждает, что worker вызвал задержку или что запись worker действительно не существовала.</p>\n<p>Для разрыва нужно проверить четыре альтернативы: span потерялся при экспорте, поле связи записано неверно, операция действительно создана на новой границе или в набор попал другой trace. Если разрыв воспроизводится на нескольких запросах, это уже основание проверить propagation на конкретной границе. Один случай остаётся сигналом для поиска.</p>\n<h2>Как читать время внутри trace</h2>\n<p>Начало и конец span отвечают за интервал, который инструмент отнёс к операции. Дочерние span-ы могут пересекаться, идти асинхронно или отсутствовать. Поэтому сумму их длительностей нельзя автоматически сравнивать с длительностью родителя: пересечения дадут двойной счёт, а невидимая работа создаст остаток.</p>\n<p>Сначала сравните четыре числа: длительность пользовательского запроса, корневого span, критичного дочернего span и внешнего ответа. Затем отметьте пропуски. Если gateway ждёт upstream 700 миллисекунд, проверьте pool wait, connect, DNS, retry и read. Если есть только общий span на 700 миллисекунд, честный вывод звучит так: «задержка наблюдалась на границе gateway; причина внутри интервала не разделена».</p>\n<p>Wall-clock полезен, чтобы сопоставить записи разных узлов, но рассинхрон часов меняет порядок близких событий. Для измерения длительности одного процесса используйте его монотонные часы. Если collector доставил записи не по порядку, сортируйте их по времени начала с оговоркой и восстанавливайте связь по ID, а не по строкам в интерфейсе.</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>Trace ID есть в gateway и API, но ответа API нет</td><td>Вызов прерван до span или span не экспортирован</td><td>Сверить access log, статус соединения, sampling и окно времени</td><td>Пометить неполный путь; не назначать API причиной</td></tr><tr><td>У span есть parentSpanId, но родителя нет</td><td>Потеря записи, ошибка propagation или другой trace</td><td>Проверить полный экспорт, формат ID, trace-id и дубликаты span-id</td><td>Сохранить missing parent как сигнал и проверить границу передачи</td></tr><tr><td>Самый длинный span совпал с p95 latency</td><td>Span включает ожидание upstream, пула или retry</td><td>Сопоставить дочерние интервалы, status, retry и population метрики</td><td>Разделить время; не менять таймаут по одному trace</td></tr><tr><td>В log нет нужного trace ID</td><td>Поле не записалось, запись потерялась или журнал усечён</td><td>Проверить схему, доставку, лимит сообщения и альтернативный request-id</td><td>Считать log неполным; подтвердить событие другим сигналом</td></tr><tr><td>В trace есть ошибка, а общий error rate не изменился</td><td>Единичный запрос не отражает population</td><td>Сверить окно, labels, маршрут и число запросов</td><td>Отделить локальный разбор от массового регресса</td></tr></tbody></table></div>\n<h2>Протокол расследования по шагам</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Запишите маршрут, метод, статус, размер ответа, временной интервал, длительность, trace ID и границу, на которой получена запись.</li><li><strong>Проверьте идентификаторы.</strong> Убедитесь, что trace-id не смешан с request-id, span-id имеет ожидаемый формат, а повторная попытка получает новую операцию, если это предусмотрено инструментом.</li><li><strong>Постройте граф.</strong> Отметьте root, parent, missing parent, duplicate span-id, разные trace-id и пустые service.name. Не восстанавливайте невидимые связи догадкой.</li><li><strong>Разложите интервал.</strong> Сопоставьте start/end, дочерние операции, ожидание очереди, соединение, retry и ответ зависимости. Зафиксируйте clock skew и асинхронные границы.</li><li><strong>Сверьте log.</strong> Ищите span-id или request-id, статус и локальное состояние. Не принимайте свободный текст сообщения за структурированное доказательство: в syslog сообщение может быть усечено или отброшено при ограничениях доставки.</li><li><strong>Проверьте масштаб.</strong> Сопоставьте один trace с метриками p50/p95, error rate, объёмом запросов и теми же labels. Trace отвечает за пример, metric — за распространённость.</li><li><strong>Сформулируйте узкий вывод.</strong> Например: «задержка наблюдалась на gateway между началом запроса и чтением ответа; внутренний источник не разделён». Такой вывод направляет следующую проверку и не выдаёт корреляцию за причину.</li><li><strong>Измените ровно одну границу.</strong> Добавьте instrumentation, исправьте propagation или измените timeout только после различающей проверки. Повторите тот же сценарий и сравните исходный сигнал с соседними метриками.</li></ol>\n<h2>Что должно попасть в следующий incident note</h2>\n<p>Хорошая запись расследования позволяет другому инженеру повторить путь без устного объяснения. Укажите исходный симптом, ссылку на trace, полноту данных, граф связей, проверенные интервалы, альтернативные гипотезы и то, какой факт каждую из них различает.</p>\n<p>Полезна форма «наблюдение → интерпретация → граница». Например: «root span gateway длится 802 мс; внутри есть 91 мс API и нет span пула; причина оставшихся 711 мс не установлена; следующий шаг — включить измерение pool wait и повторить нагрузочный сценарий». В такой записи ясно, где заканчиваются данные.</p>\n<p>Если после изменения gateway длительность упала с 802 до 120 миллисекунд на повторяемом сценарии, это усиливает гипотезу о выбранной границе. Но для утверждения о причине нужны контрольные прогоны, одинаковая нагрузка, стабильный sampling и отсутствие параллельного изменения зависимости. Одного удачного trace недостаточно.</p>\n<h2>Ограничения применимости</h2>\n<p>Метод рассчитан на систему, где команда может получить хотя бы часть trace, локальных событий и агрегированных метрик. Он слабее при head- или tail-sampling, потере collector, коротком хранении, рассинхроне часов, динамическом fan-out и асинхронных очередях. Для worker понадобятся связи producer, сообщения и consumer; parent HTTP span сам по себе не описывает всю фоновую работу.</p>\n<p>Одинаковый trace-id не доказывает бизнес-причину, безопасность данных или контрфактическое утверждение «без этого вызова всё было бы быстро». Он также не доказывает, что downstream применил изменение. Для платежа, заказа или другого побочного эффекта нужен отдельный read-state или идемпотентный контракт.</p>\n<p>Не помещайте токены, cookie, тело запроса и персональные параметры в trace или log только ради удобного поиска. Trace Context предупреждает об информационных и privacy-рисках, а идентификатор должен сужать поиск, не раскрывать содержимое запроса. Настройка retention, доступа и маскирования зависит от вашей системы и политики данных.</p>\n<h2>Критерий готовности</h2>\n<p>Разбор готов, когда по одной записи другой инженер может восстановить наблюдаемый путь и повторить проверки. В note видны граница симптома, полный или явно неполный граф, временные интервалы, статус каждого сигнала, источник вывода и следующий шаг. Для отсутствующего span сохранён отрицательный результат, а не искусственно восстановленная цепочка.</p>\n<p>Изменение готово, когда повторяемый сценарий улучшает исходный сигнал, не ухудшает error rate и latency соседних маршрутов, а propagation и sampling проверены на всех нужных границах. Если причинный вывод всё ещё зависит от невидимого события, его нужно так и записать: данных недостаточно для уверенного назначения виновника.</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>, поля <code>trace-id</code>, <code>parent-id</code>, <code>trace-flags</code>, правила propagation и оговорки о sampling.</li><li><a href=\"https://opentelemetry.io/docs/concepts/signals/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Signals</a> — официальное различие traces, metrics и logs: путь запроса, измерение во время работы и запись события.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc5424.html\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 5424: The Syslog Protocol</a> — структура сообщения и ограничения доставки; документ допускает усечение или отбрасывание слишком длинного сообщения и не устанавливает причинность событий приложения.</li></ul>"
}