Что делает путь горячим: симптом против причины
Широкий фрейм во flame graph показывает, где накапливается время, но не почему. Один и тот же leaf может скрывать пять разных узких мест — каждое требует своей правки.
Профиль готов. Один фрейм во flame graph широкий. Двое инженеров хотят сменить шаблонизатор. Третий спрашивает: «Широкий от чего? CPU-работа, аллокации, lock contention или syscall?» Только один из четырёх вариантов имеет «сменить шаблонизатор» как правильный fix.
Что такое hot path
Через десять минут ты будешь знать, как отличить пять очень разных проблем, одинаково выглядящих как «широкий фрейм» — и почему неверный диагноз тратит больше времени, чем отказ от оптимизации вовсе.
Hot path — это цепочка вызовов, в которой программа проводит большую часть времени. Профиль показывает её как стек широких фреймов, поднимающихся от leaf’а к top-level entry. Leaf называет функцию; вопрос — почему эта функция дорогая.
Современное железо превращает ту же «1 секунду CPU» в очень разные проблемы в зависимости от того, что CPU реально делал: исполнял инструкции, ждал памяти, ждал лока, ждал возврата syscall. Диагноз решает, какое семейство правок применимо.
Применение неверной правки к верному hotspot’у — вторая по частоте трата в performance-работе, после оптимизации не того hotspot’а вообще (разобрана в unit’е profile-first).
Метафора приёмной
Приёмная у врача переполнена. Это говорит, что в приёмной много людей — не говорит, почему. Пациенты ждут врача, лабораторию, бумаги, парковку? У каждого варианта своя правка: больше врачей, более быстрая лаборатория, меньше форм, больше мест для парковки.
Широкий фрейм во flame graph — то же самое: приёмная полная; спроси, чего люди ждут внутри.
| Широкий фрейм показывает | Что это реально означает | Куда смотреть дальше |
|---|---|---|
| Высокий self-time в пользовательской функции | Функция делает настоящую CPU-работу | Изучи алгоритм или data layout |
| Широкие GC-фреймы рядом с leaf’ом | Caller много аллоцирует | Переключись на allocation profile |
| Широкий в wall-clock, узкий в CPU | Функция ждёт — лок или syscall | Сними off-CPU или mutex profile |
| Фрейм интерпретатора там, где должен быть JIT | JIT деоптимизировался — откат к baseline | Стабилизируй формы объектов / типы |
Bea и Sven: один фрейм, два прочтения
Антон находит processOrder на 35% CPU и хочет переписать цикл. Дима смотрит внимательнее: большая часть этих 35% — в runtime.scanobject (GC), вызываемом изнутри цикла. Цикл много аллоцирует. Правка — sync.Pool, не новый алгоритм.
Flame graph показал симптом. Причина — на уровень глубже.
Сценарий: regex на каждый запрос
Search-эндпоинт показывает regex.test как широкий leaf. Двое инженеров хотят сменить regex-движок. Третий смотрит на parent: regex компилируется на каждом запросе, потому что паттерн строится из шаблонной строки. Правка — компилировать один раз на старте. Leaf указал на правильную область; баг был в паттерне caller’а, не в самом leaf’е.
▸Почему это работает
Leaf — это лампочка на dashboard’е: «здесь что-то не так». Правка может быть внутри функции (переписать алгоритм), в caller’е (не вызывать так часто), в callee (реальная цена уровнем ниже) или в окружающем контексте (меньше аллоцировать, реже брать лок, меньше syscall’ов). Сеньоры читают весь район, не только leaf.
Flame graph показывает широкий leaf-фрейм. Что спросить ПЕРВЫМ?
Почему «широкий фрейм = bottleneck» — неполное прочтение flame graph?
Расставь шаги сеньорной атаки на hot path по порядку:
- 1 Открой профиль и найди самый широкий leaf-фрейм по self-time
- 2 Прочти parent chain — leaf вызывается из одного пути или из многих?
- 3 Классифицируй работу: CPU-инструкции, аллокации, cache miss, lock wait, syscall или JIT deopt
- 4 Сформулируй одну гипотезу правки, соответствующей классификации
- 5 Применить ТОЛЬКО это изменение изолированно
- 6 Сними новый профиль под той же нагрузкой и сравни с baseline
- 7 Убедись, что и локальный hotspot сжался, И headline-метрика улучшилась
Вставь пропуск: широкий фрейм во flame graph называет _______; причина может сидеть уровнем выше (в caller'е), уровнем ниже (в callee) или в том, что функция реально делает.
- 01В одном абзаце: почему назвать горячую функцию недостаточно — что ещё нужно прочесть из профиля, прежде чем её чинить?
- 02Приведи два конкретных примера, когда правка находится в caller'е, а не в широком leaf'е самом по себе.
Hot path — это цепочка вызовов, где программа проводит большую часть времени. Широкий leaf во flame graph называет функцию, но причина может быть в caller’е (слишком частые вызовы), в callee (реальная цена уровнем ниже) или в том, что функция делает (CPU-работа vs аллокации vs ожидание). Диагностический вопрос — какая из пяти форм этот hotspot — должен предшествовать выбору правки. Следующие уроки разбирают каждую из пяти форм и соответствующие семейства правок. Теперь, когда встретишь широкий фрейм, твой первый шаг — вопрос, а не переписывание.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
- Пять форм hotspot''''а: CPU, аллокации, кэш, лок, syscallmiddle
- Основы GC: за что рантайм берёт налогjunior
- N+1: одна логическая операция, много round-trip''''овjunior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Что на самом деле стоит bundle: download, parse, compile, executejunior
- Цикл performance: дисциплина, а не проектjunior
встречается в162
- Путь запроса: семь остановок от сокета до ответаjunior
- Accept и парсинг: от очереди ядра до типизированного запросаmiddle
- Маршрутизация и middleware: что выполняется и в каком порядкеmiddle
- Обработчик и ответ: от бизнес-логики до байтов на проводеmiddle
- Стриминг и backpressure: когда клиент читает медленнее, чем вы пишетеsenior
- Таймауты и хвостовая задержка: бюджеты, дедлайны и ловушка fan-outsenior
- Middleware и DI: два паттерна, формирующие любой backendjunior
- Пишем middleware: сигнатуры, next() и три модели фреймворковmiddle
- Инверсия управления: как зависимости добираются до классаmiddle
- Скоупы и время жизни DI: singleton, request, transientmiddle
- DI как шов для тестов: фейки, моки и граница, которая важнаsenior
- DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоитsenior
- Блокирующий vs неблокирующий I/O: два способа ждатьjunior
- Event loop: один поток, упорядоченные фазыmiddle
- Что блокирует цикл: CPU-работа и синхронные вызовыmiddle
- Вынос CPU-работы: worker threads и пул libuvmiddle
- Backpressure и ограниченная конкурентностьsenior
- Пропускная способность под нагрузкой: хвостовая задержка и насыщениеsenior
- Зачем пул: цена создания соединенияjunior
- Размер пула: почему больше не значит быстрееmiddle
- Взятие и таймауты: очередь ожидания — настоящий дроссель задержкиmiddle
- Стратегии retry: backoff, jitter и thundering herdmiddle
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Задачи, микрозадачи и scheduler.yield()middle
- Точность таймеров, троттлинг и фоновая работаmiddle
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- LCP: четыре фазы, одна доминирующая стоимостьmiddle
- INP: input delay, processing, presentationmiddle
- Lab vs field: почему они расходятся и как использовать каждыйmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое индекс и как он ускоряет запросыjunior
- Leading-column rule: почему порядок столбцов в composite-индексе важенmiddle
- Partial, expression и covering-индексыmiddle
- Типы индексов: GIN, GiST, BRIN, Hash, Bloom и HOT-обновленияmiddle
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- Упражнение по проектированию индексов: стратегия полнотекстового поискаsenior
- EXPLAIN и планы выполнения: что решает планировщик и почемуjunior
- Типы сканирования: Seq, Index, Bitmap, Index-Onlymiddle
- Алгоритмы соединения и каскад ошибок оценки строкmiddle
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Расширенная статистика: исправление ошибок оценки для коррелированных колонокsenior
- Кеш планов, настройка константных стоимостей и внутренности планировщикаsenior
- Производственные режимы отказа и стабильность плановsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Поколенческий GC и Scavengermiddle
- Major GC: mark, sweep, compactsenior
- Write barriers: цена инкрементального и поколенческого GCsenior
- Биты в проводеjunior
- Математика задержкиmiddle
- Bufferbloat и перегрузкаsenior
- Граница физического уровняsenior
- Номера последовательности и состояние соединенияmiddle
- Управление потоком и перегрузкойmiddle
- BBR, производственная наблюдаемость и за пределами TCPsenior
- CDN: контент по соседствуjunior
- Anycast и GeoDNS: маршрутизация к ближайшему edgemiddle
- Многоуровневый кеш и Cache-Controlmiddle
- Заголовок Vary и cache keysmiddle
- Stale-while-revalidate и cache stampedesenior
- Edge workers и edge-side compositionsenior
- CDN: операции и observabilitysenior
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- WebSocket vs SSE vs long-polling: выбор правильного транспортаmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Алгоритмы балансировки: от round-robin до power-of-two-choicesmiddle
- L4 vs L7 балансировка и сохранение IP клиентаmiddle
- Health checks, connection draining и slow startmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- QUIC-потоки и head-of-line blockingjunior
- Объединённое рукопожатие и 1-RTTmiddle
- Connection ID и миграция сетиmiddle
- Обнаружение потерь и управление перегрузкойmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- Развёртывание и стоимость CPUsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- Двенадцать слоёв: один URL, семь действующих лицjunior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Критический путь рендеринга и Core Web Vitalsmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Метрики и cardinality: cost-модель time-series databasemiddle
- Логи и объём: cost-модель структурного логированияmiddle
- Трейсы и сэмплирование: cost-модель distributed tracingmiddle
- Join-ключи и exemplar''''ы: как три сигнала становятся компонуемымиmiddle
- Observability 2.0: широкие события и сдвиг стоимостиsenior
- Режимы сбоя и инженерная практика: cardinality budget''''ы, PII и сэмплированиеsenior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- Log levels и маршрутизация алертовmiddle
- Стратегии sampling и стоимость логовmiddle
- PII-редакция и log injectionsenior
- Propagation trace-контекста в логахsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Авто-инструментирование и ручные спаны: правило 80/20 в OTelmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Стратегии сэмплирования: head, tail и parent-basedmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- RED и USE: два чек-листа, одна дисциплина триажаjunior
- Инструментация RED в Prometheus: счётчики, гистограммы и дисциплина cardinalitymiddle
- USE на Linux: CPU, память, диск, сеть и PSImiddle
- Golden signals, структура дашборда и auto-RED в service meshmiddle
- Cardinality как драйвер затрат: label, PII, exemplars и семплированиеmiddle
- Native histograms, SLO и паттерны production-сбоевmiddle
- Выбор SLI и SLO-целей: отношения, не ощущенияmiddle
- Multi-window multi-burn-rate-алертинг: почему AND лучше ORmiddle
- Error budget policy, latency SLO и составные journeysmiddle
- Iceberg SLI, математика составного SLO и SLA vs SLOsenior
- Flame graph: читаем картинку, которая показывает, куда ушло времяjunior
- Sampling vs instrumentation profiling: почему 99 Гц побеждает в productionmiddle
- Типы профилей: CPU, память, off-CPU, mutex — какой когда братьmiddle
- Continuous profiling: always-on flame graphs с eBPF и корреляцией trace-idmiddle
- Как flame graph строится из сэмплов и как использовать его в productionmiddle
- Linux perf, внутренности eBPF, PGO и ограничения sampling''''аsenior
- Profiling в production: безопасность, war stories, OTel profiles и дизайн инфраструктурыsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Экономия на observability: удерживаем затраты в пределах 5% inframiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.