У сервиса можно уменьшить локальный шаг и не сдвинуть 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, она может быть соседним событием. Если у ожидания нет отдельного имени, его нельзя честно переложить на БД или внешний вызов.
| Сегмент | Роль | Интервал | Длительность | Допустимый вывод |
|---|---|---|---|---|
| fixed-admission-queue | queue-wait | 40–560 | 520 | длиннейший названный наблюдаемый сегмент |
| fixed-db-call | database-execution | 570–720 | 150 | исполнение БД в этой synthetic trace |
| fixed-catalog-call | external-dependency | 730–930 | 200 | внешний вызов в этой synthetic trace |
| fixed-gateway | end-to-end | 0–1 000 | 1 000 | граница пути, не причина задержки |
Процедура без преждевременной оптимизации
- Назвать одну synthetic trace и root span. Не смешивать несколько логических запросов в «средний путь».
- Проверить дерево: у каждого дочернего span существует parent, у интервала есть начало и конец, root покрывает путь.
- Разделить ожидание, исполнение БД и внешний вызов. Неизвестная задержка — это остановка, а не поле для догадки.
- Зафиксировать нагрузочную границу: cohort, число логических запросов, concurrency и форма входа.
- Ранжировать только названные сегменты; передать 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-end | 0–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 или реального поведения системы.