DarkRiDDeR18 мин

Критический путь: как не оптимизировать сервис до границы

ПроизводительностьНадёжность

У сервиса можно уменьшить локальный шаг и не сдвинуть end-to-end latency. Цена ошибки — неделя оптимизации, после которой пользовательский путь остаётся прежним, а команда получает красивую, но бесполезную цифру. В этой статье не ищем «самый медленный сервис»: составляем критический путь одной именованной synthetic trace и останавливаемся до решения об оптимизации.

Точка входа — fixed literal comparable-critical-path-v1. В нём 1 000 условных единиц от входа до ответа: очередь занимает 520, вызов БД — 150, внешний каталог — 200. Единицы намеренно не миллисекунды и не production metric. Они нужны только чтобы проверить дисциплину: сначала одинаковая граница, затем наблюдение, затем hand-off.

Почему локальная скорость не равна скорости пути

Critical path — не сумма всех span и не рейтинг компонентов. Это цепь на одном пути от root span до ответа, где каждая задержка имеет родителя, начало, конец и роль. Если у задержки нет связи с root, она может быть соседним событием. Если у ожидания нет отдельного имени, его нельзя честно переложить на БД или внешний вызов.

Synthetic waterfall: что можно наблюдать в одном фиксированном пути
СегментРольИнтервалДлительностьДопустимый вывод
fixed-admission-queuequeue-wait40–560520длиннейший названный наблюдаемый сегмент
fixed-db-calldatabase-execution570–720150исполнение БД в этой synthetic trace
fixed-catalog-callexternal-dependency730–930200внешний вызов в этой synthetic trace
fixed-gatewayend-to-end0–1 0001 000граница пути, не причина задержки
Waterfall одной синтетической trace: очередь, БД и внешний вызов внутри end-to-end границы
Рисунок 1. Не масштаб производительности, а порядок проверки: увидеть очередь отдельно от исполнения.

Процедура без преждевременной оптимизации

  1. Назвать одну synthetic trace и root span. Не смешивать несколько логических запросов в «средний путь».
  2. Проверить дерево: у каждого дочернего span существует parent, у интервала есть начало и конец, root покрывает путь.
  3. Разделить ожидание, исполнение БД и внешний вызов. Неизвестная задержка — это остановка, а не поле для догадки.
  4. Зафиксировать нагрузочную границу: cohort, число логических запросов, concurrency и форма входа.
  5. Ранжировать только названные сегменты; передать review как наблюдение. Не писать «ускорили», пока нет сопоставимого контроля.

Буквально исполнимый synthetic пример

Экспорт не читает trace, не вызывает сеть и не измеряет сервис. Он возвращает только review по встроенному литералу. Результат намеренно говорит observation-ready, а не «готово к rollout».

import { createFixedPerformanceReviewInput, reviewFixedPerformanceInput } from './upgrade-2026-05.mjs';

const review = reviewFixedPerformanceInput(
  createFixedPerformanceReviewInput('comparable-critical-path-v1'),
);

console.log({
  accepted: review.accepted,
  endToEndUnits: review.endToEndUnits,
  longestObserved: review.rankedObservedSegments[0],
  handoff: review.handoff,
});

Как читать результат

Первое место у fixed-admission-queue означает ровно одно: в этой заданной записи ожидание длиннее двух других именованных сегментов. Это не доказательство того, что очередь является системным bottleneck при другой нагрузке, и не инструкция переписать gateway. Следующий шаг — сформировать узкий performance review с границей и контрпримером.

Что не включать в critical path

Не включайте в путь красивую панель, усреднённый показатель или span из соседнего trace-id. Они могут дать гипотезу, но не меняют порядок интервалов в fixed-trace-01. Особенно опасен «средний database time»: он может принадлежать другой операции, тогда как данный root уже ждёт admission queue. Водопад полезен ровно тем, что удерживает разговор возле одного логического запроса.

Не складывайте дочерние длительности, если они пересекаются. В synthetic input интервалы записаны так, что очередь, БД и каталог идут последовательно; поэтому их удобно читать как составные части пути. В иной модели параллельные вызовы требуют другой проверки: кандидат на critical path выбирается по временной зависимости, а не по арифметической сумме. Фикстура не делает этого выбора и не притворяется универсальным трассировщиком.

Минимальная карточка hand-off

Поля, без которых наблюдение нельзя передать дальше
ПолеПочему требуетсяЗначение в упражнении
trace identityзащищает от склейки разных запросовfixed-trace-01
root boundaryпоказывает, что именно означает end-to-end0–1 000 units
named waitне позволяет назвать ожидание исполнениемfixed-admission-queue
load boundaryзапрещает эффект без контроляfixed-load-a / 12 / 3 / shape-a
claimотделяет факт от решенияeffect claim: not-made

Эта карточка сознательно не содержит владельца «виновного сервиса» и ожидаемой выгоды. Сначала она делает исходные данные проверяемыми. Если рецензент не видит parent, классификации или границы нагрузки, правильный ответ — вернуть запись на уточнение. Это не задержка работы; это защита от работы по неверной поверхности.

Разбор fixed waterfall по порядку

Root fixed-gateway начинается в 0 и заканчивается в 1 000. Это рамка, а не расходный элемент. Затем выделен fixed-admission-queue: 40–560. Внутри упражнения он означает ожидание до начала последующей работы. Такой span не обязан существовать во всякой реализации; именно поэтому отсутствие отдельного имени закрывает review. После него расположены fixed-db-call 570–720 и fixed-catalog-call 730–930. Промежутки 560–570 и 720–730 не объясняются и не превращаются в скрытую причину: они остаются промежутками литерала.

Из этой последовательности нельзя получить число «ускорения БД, которое улучшит путь на 15 процентов». Для него нет варианта input с изменённой БД и тем же контролем. Но можно заметить другое: уменьшение исполнения БД в изоляции имеет верхнюю видимую границу 150 условных единиц в данном пути, тогда как ожидание уже занимает 520. Это не прогноз, а способ не выбирать первую работу только потому, что она находится в знакомом сервисе.

В реальном обсуждении команда нередко начинает с самой доступной части: SQL, кеша или кода gateway. В synthetic review доступность изменения не заменяет evidence. Сначала автор выписывает самый длинный наблюдаемый сегмент и все условия, которые позволяют его так назвать. Затем владелец потенциального изменения может принести новый, отдельно проверяемый fixed input. Только после этого можно обсуждать, какую гипотезу он изолирует.

Границы критического пути

Критический путь не обязан быть полным объяснением latency. Он отвечает на более узкий вопрос: какие именованные интервалы лежат в одном пути и какой из них длиннее в данном наблюдении. Если операция происходит параллельно внешнему вызову, её полная длительность может не добавляться к root. Если клиент повторил запрос вне известного root, это уже другой логический путь. Если очередь существует, но не имеет span, её нельзя восстановить по пустоте между отметками.

Поэтому таблица waterfall — не журнал «куда ушло всё время». Она — контракт на терминологию. Слово queue-wait используется только там, где литерал сам это указывает. Слово database-execution не расширяется до «БД виновата». Слово external-dependency означает границу вызова, а не качество внешнего поставщика. Такой словарь заметно сокращает спор: рецензенты сначала спорят о недостающем поле, а не о характере системы.

Вопросы до любой задачи на ускорение

Вопрос
ПроверкаЕсли ответ «нет»Корректное действие
Путь один и связан?root и все parent существуютнельзя назвать critical pathвернуть incomplete trace
Ожидание отдельно?есть queue-wait, а не opaque delayневозможно отделить очередь от работызапросить классификацию
Нагрузка та же?cohort, requests, concurrency, shape совпалинет контрольной границыне сравнивать интервалы
Эффект не придуман?claim = not-madeвывод сильнее evidenceснять claim и оставить observation

Этот набор вопросов кажется медленным лишь до первой неверной оптимизации. Он не запрещает исследовать БД, внешний каталог или admission policy. Он запрещает выдавать исследование за доказанный end-to-end эффект. В пакете P99 положительным результатом считается не меньший root, а аккуратно оформленный hand-off для следующего review.

Практический результат waterfall не в том, что он называет один «самый важный» блок. Он делает цену следующего шага наблюдаемой: пока нет контрольной границы, любой рефакторинг остаётся отдельной гипотезой с неизвестным влиянием на путь. Поэтому в карточке полезно оставить место для вопроса, который ещё не решён, вместо того чтобы закрыть его словом «оптимизация». В нашем случае вопрос звучит: как сформировать следующий fixed input, не потеряв queue-wait и границу нагрузки?

До этого момента никакая команда не обязана соглашаться с причиной задержки. Она обязана согласиться только с тем, какие поля были проверены и какие поля ещё отсутствуют.

Ограничения и следующий шаг

Synthetic trace не содержит распределения, хвостов, реального таймера, вариации входа или причинности. W3C Trace Context описывает перенос идентификаторов, а не полноту данных; OpenTelemetry задаёт роли, а не экономический эффект. Поэтому корректный hand-off: «в fixed input queue-wait — самый длинный названный сегмент; эффект изменения не заявлен». Следующая статья проверит, почему похожий waterfall может оказаться несопоставимым.

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

  • W3C Trace Context — версия: Recommendation, 23.11.2021, immutable publication. Фиксирует формат traceparent и смысл trace-id/parent-id для связности распределённой trace. Граница: Опираемся только на идентификацию и связь; не выводим производительность из стандарта.
  • OpenTelemetry Specification Trace API — версия: v1.29.0, commit c6520a7, 11.01.2024. Задаёт различение CLIENT, SERVER, PRODUCER и CONSUMER span как описательных ролей. Граница: Роль span не доказывает причину задержки и не является метрикой.
  • RFC 9110: HTTP Semantics — версия: RFC 9110, June 2022. Определяет HTTP как stateless application-level protocol и отделяет протокольную семантику от измерения времени. Граница: Не используем RFC для обещаний latency, capacity или реального поведения системы.