Представим ситуацию: вы молодой и перспективный специалист инжиниринга производительности, который устроился в крупную IT-компанию. Ваша задача — выполнить оптимизацию определенного продукта, который должен заработать быстрее. Вы почитали об этом проекте, поняли, в чем основная суть, и даже запустили нагрузочные тесты, показывающие потенциал роста. Какой будет ваш первый шаг для поиска узких мест? Конечно, сперва стоит заняться профилированием приложения, чтобы обнаружить код, исполнение которого занимает больше всего времени. Но какой профилировщик использовать для первого осмотра? Давайте попробуем в этом разобраться.
Меня зовут Александр Слепнев, я работаю в команде инжиниринга производительности Positive Technologies. Мы занимаемся анализом продуктов компании для ускорения их работы: ищем узкие места в коде, прорабатываем способы устранения проблем, реализуем патчи и интегрируем готовые решения в проект. В этой статье расскажу о том, как мы начинаем работу над проектами и какие инструменты используем для получения первичной информации.
Wall-clock time
Время работы любого приложения можно разбить на две части:
on-cpu
— фактическое выполнение процессором кода программы;off-cpu
— ожидание различных событий, таких как завершение операций ввода-вывода, доступ к внешним ресурсам, сетевые задержки и так далее.
Вместе эти части представляют собой wall-clock time
, то есть общее время, затраченное на выполнение программы. Визуально wall-clock
выглядит как чередование сегментов on-cpu
и off-cpu
.
Как это связано с выбором профилировщика? Дело в том, что при первичном анализе важно смотреть на всю картину целиком для формирования правильных выводов относительно производительности. Поэтому использовать стоит тот инструмент, который покажет как on-cpu-
, так и off-cpu
-части.
Отличным инструментом для профилирования является perf, который позволяет довольно точно оценить алгоритм работы приложения без серьезного влияния на производительность. Но у него есть свои особенности:
в сборку приложения необходимо добавить флаг
-fno-omit-frame-pointer
, иначе снимки стека будут содержать мало полезной информации. Чем больше зависимостей и компонентов будет собрано с этим флагом, тем точнееperf
сформирует данные;perf
не покажетoff-cpu
-часть, которая интересна при первом осмотре.
Поэтому рассмотрим другой инструмент, позволяющий получить wall-clock
-профиль — pt-pmp
.
Poor man's profiler
pt-pmp
— это часть Percona Toolkit, вдохновленная poor man's profiler. Идея заключается в следующем: профилировщик при помощи дебаггера (по умолчанию — gdb)
подключается к программе и собирает семпл стека (stack trace
) для всех потоков (в том числе находящихся в состоянии off-cpu
). Эти операции повторяются несколько раз с определенным интервалом, а затем полученные снимки агрегируются, формируя wall-clock
-профиль тестируемой программы.
На основе полученного профиля можно построить флеймграф — это специальное представление stack trace профилируемого приложения для определения горячих путей в коде (Percona описала алгоритм получения флеймграфов на базе pt-pmp
в своей статье).
В качестве демонстрации возможностей pt-pmp
воспользуемся open-source-приложением Stockfish. Это шахматный движок для определения оптимальных ходов. Команды для сборки профиля и постройки графа:
Загрузка
pt-pmp
иFlameGraph
.
git clone https://github.com/brendangregg/FlameGraph
git clone https://github.com/percona/percona-toolkit.git
Запуск бенчмарка
stockfish
в многопоточном режиме с длительностью теста в 40 секунд. Описание флагов и другие режимы можно посмотреть на GitHub.
./stockfish bench 64 4 40000 current movetime >"stockfish.stdout" 2>"stockfish.stderr" &
Запуск
pt-pmp
, флагiteration
указывает на количество собираемых семплов.
sudo ./percona-toolkit/bin/pt-pmp -p $(pidof stockfish) --iterations=100 > pmp.stacktrace 2>pmp.stderr
-
Обработка полученного профиля, она необходима для корректной работы
flamegraph.pl
:удаление даты, которую профилировщик вставляет на первую линию файла;
перестановка местами колонок с количеством потоков и backtrace;
замена запятых на точки с запятой, которые используются для разделения имен функций и методов.
tail -n+2 pmp.stacktrace | awk '{print $2, $1}' | sed -e 's/,/;/g' > pmp.profile
Создание флеймграфа.
./FlameGraph/flamegraph.pl --title "stockfish PMP flamegraph" pmp.profile > flamegraph_pmp.svg
Получился следующий результат.
Граф действительно отображает wall-clock
-профиль: два столбца слева иллюстрируют on-cpu
-часть, самый правый столбец с методом Stockfish::Thread::wait_for_search_finished
представляет собой off-cpu
-часть. Тем не менее pt-pmp
обладает следующими недостатками:
Невысокая точность измерений. По сравнению с
perf
,pt-pmp
собирает гораздо меньше семплов, и это может быть критично, когда происходит работа над оптимизацией в 1–2%: проблемные методы могут просто скрываться от глаз или при сравнении двух флеймграфов до и после оптимизации количество семплов, которые относятся к определенному методу, может не измениться, хотя ожидается обратное.Использование
gdb
влияет на производительность тестируемого приложения, так как при снятии семпла работа модуля приостанавливается на некоторое время, и количество таких остановок прямо пропорционально количеству итераций. Если сравнивать результаты бенчмаркаstockfish
в простом запуске и при использованииpt-pmp
, то видно, что производительность упала практически на 100%.
Nodes searched |
Nodes/second |
Разница в процентах |
|
Обычный запуск |
108379841 |
2709428 |
97,5% |
Запуск с |
50614339 |
1012266 |
98% |
pt-pmp
может обрезать название метода при агрегировании снимков стека в рамках функции aggregate_stacktrace: в этом примере большинство методов отмечены какint
, что дает мало информации.
Однако существуют приемы, позволяющие полностью или частично эти проблемы исправить.
Делаем бедного человека богаче
Начнем с использования gdb
. Этот отладчик, который используется по умолчанию в рамках pt-pmp
, необходим для того, чтобы собрать stack trace, но он довольно тяжелый и ощутимо влияет на производительность. Поэтому логичным будет использование более легковесного инструмента. Начиная с версии percona-toolkit 3.5.8 (сейчас она находится в разработке, нужные правки находятся в ветке 3.x) pt-pmp позволяет выбрать инструмент для сборки снимков стека. Доступны следующие варианты:
gdb
;eu-stack
— легковесный инструмент из пакета elfutils, с помощью которого можно получить снимок стека каждого потока программы;pt-eustack-resolver
— обертка надeu-stack
, которая дополнительно форматирует снимки стека аналогично выводуgdb thread apply all bt
.
Для наших целей подойдет обычный eu-stack
. Для его использования нужно передать дополнительный параметр dumper
при запуске pt-pmp
:
sudo ./percona-toolkit/bin/pt-pmp --dumper=eu -p $(pidof stockfish) --iterations=100 > pmp.stacktrace 2>pmp.stderr
При таком подходе флеймграф выглядит так.
Результат получился похожим на gdb-версию, однако производительность stockfhish
заметно улучшилась: бенчмарк стал выдавать достаточно близкие к обычному запуску результаты.
Nodes searched |
Nodes/second |
Разница в процентах |
|
Обычный запуск |
108379841 |
2709428 |
0,82% |
Запуск с pt-pmp (eu-stack) |
107494533 |
2687296 |
0,82% |
Также заметно ускорился процесс работы pt-pmp
: версия с gdb
для 100 итераций отрабатывала примерно за 30 секунд, в случае с eu-stack
время сбора приблизилось к 6 секундам.
Чтобы метод aggregate_stacktrace
выводил полные имена функций, нужно отредактировать код pt-pmp
следующим образом:
diff --git a/bin/pt-pmp b/bin/pt-pmp
index 0aacb473..7575a576 100755
--- a/bin/pt-pmp
+++ b/bin/pt-pmp
@@ -604,8 +604,15 @@ aggregate_stacktrace() {
if ( ${maxlen:-0} == 0 || c < ${maxlen:-0} ) {
if ( \$2 ~ /0x/ ) {
if (dumper ~ /^eu/) {
- targ = \$3;
- tfile= \$NF;
+ if ( NF > 2 )
+ \$1 = \$2 = \"\";
+ OFS=\"\";
+ targ = \$0
+ }
+ else {
+ targ = \$3;
+ tfile= \$NF;
+ }
}else {
if ( \$4 ~/void|const/ ) {
Еще раз формируем флеймграф обновленным pt-pmp
.
Результат выглядит гораздо информативнее, чем изначальная версия (теперь видны полные названия методов), on-cpu-
и off-cpu-
части присутствуют.
Как повысить точность измерений
Очевидным выглядит вариант увеличения количества итераций. Но что, если нужно повысить величину снимаемых семплов на несколько порядков, не увеличивая длительность замера? В таком случае лучше использовать другой профилировщик, который уже упоминался в данной статье, — perf
.
Для сравнения прогоним тот же бенчмарк, но флеймграф соберем через perf
.
Запуск
perf record
для получения снимков стека длительностью в 6 секунд (бенчмарк уже должен быть запущен).
sudo perf record -F 99 -g -p "$(pidof stockfish)" -- sleep 6
Обработка полученных данных для совместимости с
flamegraph.pl.
sudo perf script | ./FlameGraph/stackcollapse-perf.pl > perf.profile
Формирование флеймграфа.
./FlameGraph/flamegraph.pl --title "stockfish perf flamegraph" perf.profile > flamegraph_perf.svg
Для Stockfish::Thread::idle_loop
за 6 секунд perf
собрал примерно 34 000 000 000 семплов, что гораздо больше, чем у pt-pmp
. Такая точность позволяет увидеть функции, которые pt-pmp
может просто не показать: например, perf
-флеймграф отображает более глубокую вложенность метода Stockfish::Search::Worker::search
(это можно понять по «гребню волны»).
Производительность бенчмарка изменилась примерно на 2% при использовании perf
, что довольно близко к показателям при pt-pmp
.
Nodes searched |
Nodes/second |
Разница в процентах |
|
Обычный запуск |
108379841 |
2709428 |
1,96% |
Запуск с perf |
106258084 |
2656385 |
1,96% |
Итоговый алгоритм профилирования
Какой же инструмент выбрать для начального и последующих осмотров? Начать c pt-pmp
, а потом переключиться на perf
? Важно помнить, что, когда появляется такой вопрос, всегда есть вариант не выбирать вовсе. Действительно, pt-pmp
позволяет определить места, в которых приложение простаивает, а perf
полезен в локальных оптимизациях, где необходима повышенная точность измерений. К тому же с помощью perf
можно проводить Top-Down-анализ, оценивая различные метрики работы процессора. Так почему бы не объединить эти два инструмента?
На практике мы используем именно такой подход: сперва запускается один профилировщик, а затем второй. Важно отметить, что они запускаются последовательно, чтобы не оказывать серьезного влияния на производительность модуля.
Алгоритм имеет следующий вид:
сборка тестового приложения с флагом
-fno-omit-frame-pointer
и его запуск в фоновом режиме;запуск
perf
для получения точного профиля с ожиданием завершения работы;запуск
pt-pmp
для полученияwall-clock
-профиля;ожидание завершения тестовой программы и формирование флеймграфов на основе полученных профилей.
В результате мы получаем достаточную информацию о работе программы, которую в дальнейшем анализируем и используем для поиска проблемных мест.
Заключение
При помощи описанного алгоритма удалось найти множество оптимизаций, которые улучшили производительность продуктов Positive Technologies. Эти инструменты довольно просты в использовании и не оказывают серьезного влияния на работу приложения, что позволяет интегрировать их в нагрузочные тесты для сбора информации о программе. Однако появляется логичный вопрос: что дальше? Ведь использование двух инструментов профилирования выглядит избыточным, хотелось бы иметь какую-то одну утилиту, позволяющую получить точный wall-clock-профиль. Пока что каких-то готовых альтернатив нет. Довольно много работ ведется в направлении разворачивания стека без использования frame pointers, например, Polar Signals в своей статье описали этот механизм на базе DWARF с использованием eBPF, но тут присутствует все та же проблема с off-cpu-частью, которую инструмент не показывает. Поэтому нам остается следить за новостями в сфере инструментов профилирования и пользоваться Poor Man's Profile, который по-прежнему актуален. Пишите в комментариях, какие инструменты для анализа вы применяете в своих проектах.
Один из продуктов, производительность которого мы постоянно улучшаем — MaxPatrol SIEM. Узнать больше о новых фичах продукта можно 27 июня в 14:00 на онлайн-запуске MaxPatrol SIEM 8.2. Регистрируйтесь и задавайте свои вопросы. Авторов самых лучших мы обязательно наградим.
Александр Слепнев
Специалист отдела инжиниринга производительности, Positive Technologies
leitosama
Хотелось бы увидеть какие-то приближенные к реальности примеры находок. Допустим вот Вы запустили инструмент и что было дальше? Как увидели проблему, как нашли итоговый кусочек кода, который надо оптимизировать.
Считаю что статья заслуживает второй практической части.