В этом уроке разберем, что такое флеймграф, как его построить из обычного perf, как читать каждый пиксель, где новички спотыкаются и что изменилось к 2026 году. Покажу классический путь через perf и набор скриптов Брендана Грегга, современный путь через eBPF, а в конце - куда все это выросло в проде (continuous profiling). Это must-have инструмент, когда надо найти узкое место в большом приложении и понять профиль linux, где десятки тысяч строк кода и непонятно, кто виноват.
Что такое flame graph linux и зачем он нужен
Flame graph (флеймграф, "пламенный график") придумал Брендан Грегг - инженер, имя которого ты будешь встречать в каждой статье про производительность Linux. Это способ нарисовать профиль так, чтобы горячий код было видно мгновенно.
Идея простая. Профайлер много раз в секунду делает снимок (сэмпл): где сейчас выполняется программа и какой у нее стек вызовов - кто кого позвал. Например: main -> handle_request -> parse_json -> memcpy. Таких снимков набираются тысячи. Флеймграф их группирует и рисует прямоугольники (их называют "фреймы"):
- Ось X (ширина) - это НЕ время по порядку слева направо, а доля сэмплов. Чем шире прямоугольник функции, тем чаще она встречалась в стеках, то есть тем больше CPU она ела. Соседние фреймы на одном уровне отсортированы по алфавиту, а не по времени - это важно понимать, иначе будешь искать смысл там, где его нет.
- Ось Y (высота) - глубина стека. Внизу - main или точка входа, выше - кто кого вызвал. Прямоугольник стоит на том, кто его вызвал. Высота сама по себе ничего не говорит про скорость, это просто глубина вложенности.
- Цвет - по умолчанию случайный "теплый" (отсюда и пламя), смысловой нагрузки не несет. Но цвет можно включить осмысленный: --color=java/js/perl подсветит user/kernel/JIT-фреймы разными оттенками, --color=io пригодится для off-CPU.

Как читать флеймграф: разбор по осям и взгляду
Давай разберем чтение на пальцах, потому что это половина успеха - построить картинку легко, а вот не запутаться в ней сложнее. Возьми любой флеймграф и читай его так.
Верхняя кромка - это и есть процессор прямо сейчас. Самый верхний фрейм над каждой точкой по оси X - это та функция, которая в этот момент реально исполнялась на CPU. Все, что под ней, - это ее предки, цепочка "кто кого позвал", уходящая вниз до точки входа. Поэтому правило "смотри наверх" буквально означает: смотри на ту самую кромку, по ней размазано все процессорное время. Если ты мысленно проведешь горизонтальную линию по верхушкам всех плато и сложишь их ширины - получишь 100% профиля.
Ширина - это доля, а не абсолют. Фрейм шириной в половину графика означает: эта функция (вместе со всеми, кого она вызвала ниже по стеку, то есть со всеми своими потомками выше нее на картинке) присутствовала примерно в половине сэмплов. Ключевое слово - "вместе с потомками": широкий фрейм внизу не значит, что он сам жжет CPU, он лишь суммирует все, что происходит над ним. Чтобы понять, где именно горит, спускайся взглядом по широкому столбу вверх, пока ширина не "рассыплется" на узкие пички или пока не упрешься в широкое плато на верхушке - вот оно, место, где CPU тратится непосредственно.
Высота не про скорость. Высокая узкая башня - это просто глубокая цепочка вызовов или рекурсия. Она может занимать доли процента времени и не быть проблемой вообще. Не дай высоте себя обмануть: единственное, что коррелирует с расходом CPU, - это ширина, и только у верхних фреймов.
Алфавитная раскладка по X - это фича, а не баг. Соседние фреймы на одном уровне отсортированы по имени функции слева направо. Зачем? Чтобы одинаковые поддеревья всегда оказывались рядом и сливались в одно широкое плато, а не были раскиданы по графику. Побочный эффект: положение фрейма слева или справа не значит "раньше" или "позже" по времени. Если тебе нужен именно порядок во времени (что за чем исполнялось), это другой инструмент - flame chart (с осью X = время) или perf timechart, не путай их.
Алгоритм осмотра за 30 секунд. Открыл картинку - не вчитывайся в имена сразу. Сначала глазами найди 2-3 самых широких столба от низа до верха. Поднимись по каждому до верхней кромки, прочитай имя функции на верхушке широкого плато - это твои кандидаты в горячие точки, обычно их 1-3 штуки и они дают 60-90% времени. Все остальное (лес узких пичков) игнорируй на первом проходе: даже если ускорить узкий фрейм в два раза, общий выигрыш будет в пределах его доли, то есть копейки. Это и есть смысл флеймграфа - он за секунды показывает, куда стоит вкладывать силы, а куда нет.
Поиск как лупа. Не доверяй только глазам: используй Ctrl+F (поиск в SVG) по имени функции или библиотеки. Он подсветит фиолетовым все вхождения, даже размазанные по разным веткам, и покажет суммарный процент справа вверху (надпись Matched). Так ты увидишь, например, что malloc сам по себе нигде не широкий, но в сумме по всему графику ест 12% - такое "размазанное" потребление глаз пропускает, а поиск ловит.
Как построить perf flamegraph: три шага
Классический путь Brendan Gregg flamegraph - это связка perf и набора скриптов FlameGraph. Скрипты - это несколько perl-файлов, ставятся клонированием с гитхаба, компилировать ничего не надо.
Код: Выделить всё
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph
Код: Выделить всё
sudo perf record -F 99 -a -g -- sleep 30
- -F 99 - частота сэмплирования, 99 Гц (99 снимков в секунду на каждый CPU). Почему не 100? Чтобы не попасть в резонанс с таймерами и периодическими задачами, которые часто тикают ровно на круглых числах (100, 1000 Гц), и не словить систематическое искажение - так называемый lockstep sampling. По той же причине берут нечетные 99, 199, 997 Гц, а не 100/200/1000.
- -a - снимать со всех CPU (вся система). Для одного процесса вместо -a используй -p PID, для одной команды от старта - perf record ... -- ./myapp.
- -g - вот это ключевое: записывать стеки вызовов (call graph). Без -g флеймграфа не будет, останутся только верхушки без предков. -g - это сокращение для --call-graph fp (разворот по фрейм-поинтеру).
- -- sleep 30 - perf будет писать ровно пока живет команда sleep, то есть 30 секунд. Удобный трюк задать длительность, не нажимая Ctrl+C руками.
Код: Выделить всё
perf script | ./stackcollapse-perf.pl > out.folded
./flamegraph.pl out.folded > profile.svg
Важная тонкость: запускай perf script на той же машине и желательно с теми же бинарями, где делал perf record, иначе perf не сможет разрезолвить адреса в имена функций и ты увидишь голые шестнадцатеричные адреса вместо названий.
Вот как выглядит промежуточный свернутый (folded) формат - то, что отдает stackcollapse:
Код: Выделить всё
main;handle_request;parse_json;memcpy 5412
main;handle_request;db_query;recv 1830
main;event_loop 540
Почему именно folded в середине. Этот текстовый формат - универсальный "мостик". Любой профайлер (perf, DTrace, SystemTap, async-profiler для Java, py-spy для Python, eBPF-инструменты) умеет отдавать стеки, а в репозитории FlameGraph под каждый есть свой свертыватель: stackcollapse-perf.pl для perf, stackcollapse.pl для DTrace, stackcollapse-stap.pl для SystemTap, stackcollapse-java-exceptions.pl и так далее. Все они приводят данные к одному виду "стек ЧИСЛО", а дальше flamegraph.pl уже не важно, откуда стеки пришли. Поэтому, освоив один формат, ты строишь флеймграфы для чего угодно.
Актуально на 2026. Современный perf (ядра 6.x) умеет отдавать folded-формат сам, без perl-скрипта: perf script -F +pid --no-inline для контроля полей, а связка для одной трубы выглядит так же. Более того, в perf встроили команду perf script flamegraph, которая сразу рисует интерактивный HTML-флеймграф на базе d3-flame-graph (нужен шаблон d3, perf предложит скачать его с CDN при первом запуске). Но stackcollapse-perf.pl по-прежнему стандарт - он чистит мусорные кадры и оффсеты лучше, а SVG-вывод flamegraph.pl легче встроить в отчет. Для Rust/C++ удобнее обертки: cargo flamegraph (crate flamegraph) или samply - последний не требует perl и открывает результат прямо в Firefox Profiler (это полноценный flame chart с таймлайном плюс обычный флеймграф). Учти нюанс: если бинарь слинкован новым линкером lld (он стал дефолтным в Rust 1.90) или mold, для корректных стеков нужен флаг --no-rosegment, иначе perf не размотает стек.
Fail быстро: фрейм-поинтеры против DWARF
Самая частая причина "сломанного" флеймграфа - не размотался стек, потому что в бинаре нет фрейм-поинтеров (компилятор выкинул регистр RBP оптимизацией -O2). Есть два пути разворота:
- --call-graph fp (он же -g) - быстрый и почти бесплатный, но требует, чтобы код был собран с -fno-omit-frame-pointer. Актуально на 2026: Ubuntu 24.04 и Fedora пересобрали системные пакеты с фрейм-поинтерами по умолчанию именно ради профилирования, так что на свежих дистрибутивах fp чаще просто работает.
- --call-graph dwarf - разматывает стек по DWARF/.eh_frame даже без фрейм-поинтеров. Работает с любым релизным бинарем, но дороже: perf копирует кусок стека на каждый сэмпл (по умолчанию 8 КБ, регулируется --call-graph dwarf,16384), perf.data распухает, и при глубокой рекурсии стек может обрезаться. Это плата за удобство.
- --call-graph lbr - аппаратный разворот через Last Branch Record на Intel/AMD, очень дешевый, но глубина ограничена железом (десятки кадров).
Символы - отдельная боль. Даже если стек размотался, без таблицы символов вместо имен будут голые адреса 0x7f.... Что нужно:
- Для системных библиотек - поставить пакеты с debug-символами (имена -dbg, -dbgsym или -debuginfo в зависимости от дистрибутива) либо настроить debuginfod - сервис, который подтягивает символы по сети на лету (переменная DEBUGINFOD_URLS, на современных дистрибутивах уже преднастроена).
- Для своего бинаря - не запускать stripped-версию, держать символы при себе.
- Для JIT-языков символы генерятся в рантайме. Java: запусти JVM с -XX:+PreserveFramePointer (иначе стек обрывается на JIT-кадрах) и подключи perf-map-agent, который кладет таблицу /tmp/perf-PID.map - perf ее читает автоматически. Node.js: флаг --perf-basic-prof делает то же самое. Альтернатива для Java - async-profiler, он сам строит флеймграф без perf и без правки фрейм-поинтеров.
То, что мы построили выше, - это on-CPU флеймграф. Он показывает, где процессор реально считал. Но огромный класс тормозов - это не "считаем много", а "ждем". Ждем диск, сеть, блокировку (lock), ответ базы, планировщик. На on-CPU профиле этого ожидания не видно вообще - процесс в это время не на CPU, профайлер его не сэмплит. Симптом: приложение "висит", отвечает медленно, а top показывает простаивающий CPU. on-CPU граф тут будет почти пустой и бесполезный.
Тут на помощь приходит off-CPU флеймграф. Он показывает обратную картину: где поток заблокировался и сколько времени проспал вне CPU. Снять его обычным perf record неудобно (нужно ловить каждый sched-switch), поэтому используют eBPF. eBPF - это технология, которая позволяет безопасно цеплять мини-программы к событиям ядра почти без накладных расходов. Для off-CPU есть готовый инструмент из пакета bcc - offcputime (в Debian/Ubuntu ставится как bpfcc-tools, бинарь зовется offcputime-bpfcc).
Код: Выделить всё
sudo offcputime-bpfcc -df -p $(pgrep -n myapp) 30 > off.folded
./flamegraph.pl --color=io --countname=us --title="Off-CPU" off.folded > offcpu.svg
Важная разница в смысле ширины. У on-CPU графа ширина = доля сэмплов = доля процессорного времени. У off-CPU графа ширина = суммарное время сна в микросекундах. Это меняет интерпретацию: широкое плато на off-CPU может быть нормой (например, поток-воркер большую часть времени честно спит на epoll_wait в ожидании запросов - это здоровое ожидание, а не проблема). Поэтому off-CPU читают с пониманием контекста: ищи не просто широкое, а широкое ожидание на горячем пути запроса, которое бьет по latency. Блокировка внутри обработки запроса - проблема; сон idle-воркера - нет.
Полезный сосед - offwaketime-bpfcc: он показывает не только где заснули, но и кто разбудил поток (wakeup-стек), что помогает распутать цепочки "кто кого ждет" на блокировках. А если наложить on-CPU и off-CPU на один граф, получится hot/cold флеймграф - полная картина жизни потока: и где он жег CPU (hot), и где спал (cold), в сумме они дают все 100% wall-clock времени. Это самый честный, но и самый перегруженный вид: новичку проще держать два отдельных графа, а hot/cold доставать, когда надо доказать, что "потерянное" время именно в ожидании, а не в счете.
Про версии ядра. eBPF-разворот стеков появился в Linux 4.8, offcputime работает на 4.6+ (полноценно с eBPF - с 4.8). На современных дистрибутивах с systemd (Ubuntu 22.04/24.04, свежие Debian, RHEL/Fedora) все из коробки. На отечественных Astra Linux и RED OS ядра достаточно новые, bcc-инструменты ставятся из репозиториев. Для разовых замеров вместо bcc удобнее bpftrace - однострочники короче, а для on-CPU есть штатный bcc-инструмент profile (profile-bpfcc), который начиная с Linux 4.9 сэмплит и считает стеки прямо в ядре, не создавая огромный perf.data. Это и есть тот самый эффективный BPF-профайлер: меньше накладных расходов, сразу folded на выходе.
Зоопарк флеймграфов: какой когда брать
Брендан Грегг развел целое семейство визуализаций на одной идее. Держи в голове карту, чтобы выбирать инструмент под задачу, а не строить on-CPU граф на любую проблему.
- On-CPU (CPU flame graph). Базовый. Ширина = доля процессорного времени. Берешь, когда CPU загружен под завязку и надо понять, на что он тратится. Источник - perf или profile-bpfcc.
- Off-CPU. Ширина = время вне CPU (ожидание). Берешь, когда latency большая, а CPU простаивает. Источник - offcputime-bpfcc.
- Hot/cold. On-CPU и off-CPU на одном графике, в сумме весь wall-clock. Берешь, когда надо показать полную картину "где время вообще".
- Icicle graph (ледяной/сосулька). Тот же флеймграф, но перевернутый вверх ногами: корень (точка входа) сверху, а листья-горячие функции свисают вниз, как сосульки. Строится флагом --inverted у flamegraph.pl. Зачем переворачивать? Так удобнее, когда тебя интересует не "кто жжет", а "кто вызывает горячий код" - агрегация идет от листьев, и одинаковые горячие функции, вызванные из разных мест, собираются в широкие верхние (теперь нижние) плато. Многие continuous-profiling UI рисуют именно icicle по умолчанию, потому что так привычнее читать сверху вниз.
- Differential (дифференциальный). Сравнение двух профилей цветом: красный - стало хуже, синий - стало лучше. Незаменим для поиска регрессий. Подробно ниже.
- Memory flame graph. Ширина = не сэмплы, а байты выделенной/живой памяти, цвет обычно зеленый. Строится из трейсинга malloc/free, brk, mmap или page faults (например, через bcc или ловлю аллокаций). Берешь, когда расследуешь утечку или раздувшийся RSS: широкое плато покажет стек, по которому льется память.
- I/O flame graph. Ширина = байты или количество операций ввода-вывода по стекам. Полезен, чтобы увидеть, какой код инициирует диск или сеть.
Дифф-флеймграфы и continuous profiling (2026)
Два приема, которые отличают новичка от практика.
Дифференциальные флеймграфы. Сняли folded "до" и "после" релиза - и сравнили скрипт-ом из того же репозитория:
Код: Выделить всё
./stackcollapse-perf.pl out.stacks1 > before.folded
./stackcollapse-perf.pl out.stacks2 > after.folded
./difffolded.pl before.folded after.folded | ./flamegraph.pl > diff.svg
Несколько тонкостей, на которых спотыкаются:
- Ширина берется из второго (after) профиля. То есть форма графика - это состояние "после", а цвет накладывается как дельта. Поэтому если функция в "после" исчезла совсем, ей негде нарисоваться и ты не увидишь, что она пропала. Чтобы поймать и такие пропажи, есть зеркальный режим: difffolded с флагом --negate плюс flamegraph - он рисует по форме "до" и подсвечивает то, что ушло. На практике строят оба и смотрят вместе.
- Нормализация -n. Если "до" и "после" снимали разное время или под разной нагрузкой, абсолютные счетчики не сравнить честно. Флаг -n у difffolded приводит первый профиль к масштабу второго, чтобы дельта отражала структуру, а не просто разную длительность замера.
- Срезать адреса -x. Если между сборками поехали адреса и в стеках остались куски hex, флаг -x их вычистит, иначе одинаковые по смыслу кадры не совпадут и diff будет шумным.
- Grafana Pyroscope. Версия 2.0 вышла в апреле 2026, переписана под масштаб: единый путь записи профилей и stateless-обработка запросов, что снизило стоимость хранения и ускорило выборки. Интегрируется с остальной экосистемой Grafana, понимает OTLP.
- Parca от Polar Signals. Полностью open-source, сильная eBPF-сторона, чистая интеграция в стиле Prometheus (метки, scrape-модель).
- OpenTelemetry eBPF profiler. Системный профайлер, который цепляет eBPF-пробы и снимает стеки со всех процессов хоста (порядка 97 Гц), складывая профили в формат OTLP. SIG по профилированию в OpenTelemetry стандартизирует профиль как четвертый сигнал телеметрии (рядом с трейсами, метриками и логами), так что профили потекут в общий OTel Collector наравне с остальным.
Куда смотреть дальше. Если зацепило: страница brendangregg.com/flamegraphs.html - первоисточник со всеми вариантами; репозиторий brendangregg/FlameGraph - скрипты и примеры; для языков - async-profiler (JVM), py-spy (Python), samply и cargo flamegraph (Rust/native); для прода - подними Pyroscope или Parca на тестовом сервисе и поживи с непрерывным профилем неделю, это меняет привычки сильнее любой статьи.
Типичные грабли и заблуждения
- "Ширина по X - это время выполнения слева направо." Нет. По X соседние фреймы отсортированы по алфавиту, а ширина - это доля сэмплов. Флеймграф не таймлайн, порядок слева направо ничего не значит. Нужен таймлайн - смотри flame chart (другой инструмент) или perf timechart.
- "Цвет что-то означает." По умолчанию нет, теплые цвета случайны и нужны только чтобы соседние фреймы визуально различались. Смысл у цвета появляется лишь в спец-режимах: --color=java/js (тип кода), --color=io, дифференциальный (красный/синий), memory (зеленый). Не ищи смысл в случайной палитре обычного графа.
- Пустой или плоский график, все стеки глубиной 1. Почти всегда забыли -g у perf record, либо нет фрейм-поинтеров. Собери с -fno-omit-frame-pointer или используй perf record --call-graph dwarf.
- Вместо имен функций - адреса вида 0x7f... Нет символов. Поставь debug-символы (пакеты -dbg/-debuginfo или debuginfod), не запускай stripped-бинарь, резолвь на той же машине. Для JIT-языков (Java, Node) нужен perf-map-agent или -XX:+PreserveFramePointer, иначе стек обрывается.
- "Узкая высокая башня = проблема." Не обязательно. Высота - это глубина рекурсии или вложенности вызовов, она не про CPU. Смотри на ширину верхушек, а не на высоту башни.
- Ищут тормоза ожидания на on-CPU графе. Если приложение висит, но CPU простаивает - on-CPU граф пустой. Бери off-CPU (offcputime).
- Слишком короткий замер. 1-2 секунды на 99 Гц - это сотня-другая сэмплов, статистика шумная, узкие фреймы случайны. Снимай хотя бы 15-30 секунд под стабильной нагрузкой.
- Принимают широкое плато на off-CPU за проблему. Idle-воркер, который честно спит на epoll_wait или futex в ожидании работы, даст широченное плато - и это норма, а не баг. На off-CPU графе смотри на ожидание внутри горячего пути запроса, а не на здоровый сон простаивающих потоков.
Повтори прямо сейчас, минут на десять:
- Клонируй FlameGraph: git clone https://github.com/brendangregg/FlameGraph и зайди в каталог.
- Запусти любую нагрузку, например в соседнем терминале: yes > /dev/null (одно ядро в полку) или реальную сборку проекта.
- Сними профиль: sudo perf record -F 99 -a -g -- sleep 15
- Построй SVG одной трубой: perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > profile.svg
- Открой profile.svg в браузере. Найди самое широкое плато наверху, кликни по нему - оно развернется на весь экран. Это твое узкое место. Попробуй Ctrl+F и поиск по имени функции - увидишь ее суммарную долю.
- Переверни тот же профиль в icicle: ./flamegraph.pl --inverted out.folded > icicle.svg и сравни, как читается сверху вниз.
- Бонус: сними второй профиль под другой нагрузкой в out2.folded и сделай difffolded.pl out.folded out2.folded | flamegraph.pl > diff.svg - посмотри, что покраснело.
- Что означает ширина прямоугольника на флеймграфе и почему это НЕ время по порядку слева направо?
- Почему смотреть надо именно на верхнюю кромку графика, а широкий фрейм внизу сам по себе не значит, что он жжет CPU?
- Какой флаг perf record отвечает за запись стеков вызовов и что будет, если его забыть?
- Чем off-CPU флеймграф принципиально отличается от on-CPU, что означает у него ширина и каким инструментом его снимают?
- Что такое icicle graph и дифференциальный флеймграф, и в каких ситуациях каждый из них берут?
- Ты видишь вместо имен функций шестнадцатеричные адреса. В чем причина и как починить? Назови хотя бы две.
Флеймграф - это профиль, превращенный в картинку, где ширина = доля сэмплов (времени), а высота = глубина стека. Ось X - алфавитная раскладка стеков, а не время; цвет по умолчанию случаен. Читаешь по верхней кромке: самые широкие плато на верхушках - горячие точки, и их обычно всего 1-3. Цепочка постройки проста: perf record -F 99 -a -g -> perf script -> stackcollapse-perf.pl -> flamegraph.pl -> SVG, а посередине универсальный folded-формат, который можно фильтровать и дифить. Жжешь CPU - бери on-CPU граф; висишь в ожидании - бери off-CPU через eBPF (offcputime), там ширина = время сна; нужна полная картина - hot/cold; удобнее читать сверху - icicle (--inverted); ищешь регрессию - дифф-флеймграф (красный хуже, синий лучше). Не размотался стек - проверь -g, фрейм-поинтеры или --call-graph dwarf; вместо имен адреса - поставь debug-символы или debuginfod, для JIT нужен perf-map-agent / -XX:+PreserveFramePointer. В 2026 это уже не только разовая картинка, а непрерывное профилирование в проде (Pyroscope 2.0, Parca, OTel eBPF profiler), где флеймграф рисуется за любой прошлый интервал с почти нулевым оверхедом. Флеймграф остается самым наглядным способом найти узкое место в большом приложении - от десктопа до прода.