Comments 20
perf в режиме сбора dwarf стеков (--call-graph dwarf) не требует пересборки кода. Дополнительно можно поэкспериментировать с размером собираемого стэка и включить компрессию (--compression-level).
Почему не использовать для этих целей Vtune который работает на перфе и предоставляет различные методологии профилирования?
Его сложнее внедрить на масштабе всего кластера. ПБЧ требует всего лишь GDB, который и так доступен в каждом нашем контейнере.
Закрытый исходный код. Когда мы писали ПБЧ, VTune точно требовал лицензию для коммерческого использования.
Нам неизвестно, как с помощью VTune получать разбивку по экспериментам, как с ПБЧ.
были ли попытки перейти с ПБЧ (gdb) на профайлинг при помощи bpf?
bpf как и perf откручивает стеки через frame pointers. Так что, по крайней мере в этой части достоинств не будет.
Пока не пробовали, но в будущем надеемся проверить.
Интересно, за счёт чего GDB разворачивает стек лучше, чем --call-graph dwarf
— казалось бы, у последнего есть и стек (который дампится в perf.data как есть — возможно, нужно лимит увеличить?), и символьная информация (во всяком случае, если мы его запускаем на той же машине и не подменяли файлов с момента запуска бинаря). Возможно, дело в самом анвайндере и стоит попробовать какую-нибудь ещё реализацию, например, из hotspot.
gdb откручивает на полном "живом" стеке онлайн. У перфа "оффлайн" раскрутка делается на этапе репорта, а во время сбора данных сохраняется только верхушка стековой памяти некоторого размера. Этого сохраненного куска не хватает для полной открутки если длинная цепочка вызовов или много данных хранится на стэке.
Если всё объясняется этим, то действительно увеличения --stack-size
должно быть достаточно — всё лучше, чем заставлять поток дожидаться, пока GDB весь стек раскрутит.
Я бы тут был осторожным. Допустим, сэмплируем 64 ядра, 100 раз в секунду. Если сохранять 256 КБ стековой памяти, то это 64*100*256*1024/2**30 = ~1.5 ГБ/сек. Нетривиальное количество памяти для копирования - несколько процентов общей memory bandwidth, и last level cache выметет...
100 раз в секунду
Так речь же скорее про раз в 100 секунд
Если уж через perf собирать, то имеет смысл чаще, кмк. Возможно, получится выбрать sampling rate пониже, чтобы самортизировать нагрузку, да. Но в целом мысль такая, что режим открутки dwarf в perf очень расточителен с точки зрения количества памяти, к которой он прикасается и копирует. Было бы здорово, если бы perf умел прямо в обработчике прерывания откручивать через dwarf, но Линус уже сказал как-то в своей манере, что dwarf раскрутки в ядре не будет - слишком сложный код (turing-complete, вроде как). Ждем, когда Intel CET shadow stack станет более доступной и поддерживаемой.
Еще один недостаток такого профайлера, это что стеков ядра увидеть не получится - я так понимаю, GDB обязательно дождется выхода из системного вызова.
У такого дифференциального flame graph есть неудобство - поскольку форма строится по первому флеймграфу, то если функция / путь вызова есть только во втором, ее не увидишь. Приходится менять местами первый и второй, чтобы убедиться, что ничего не пропустил. Есть еще рабочий вариант использовать в качестве ширины среднее между профилями, то тоже не идеально.
По поводу проблемы, что если одна и та же функция есть во флеймграфе много раз, то ее сложно увидеть - посмотрите на pprof, и в частности на фичу из этого пулл реквеста. С ней жить должно стать повеселее.
Можно попробовать -fnoomit-frame-pointer -momit-leaf-frame-pointer. Может помочь компилятору таки использовать %rbp в самых горячих листовых функциях. И раскрутка в большинстве случаев работать будет - когда %rbp не используется.
В каждом контейнере нашего кластера запущен демон, который просыпается раз в несколько минут в случайные моменты времени и присоединяется к профилируемому процессу с помощью GDB.
Кстати, я тут экспериментировал для одного своего проекта и мне удалось сделать так, что GDB подключается к процессу в контейнере, считывает символы там же, но при этом сам установлен и запущен на хосте. Т.е. технически в вашем случае можно было бы запускать один экземпляр демона.
Если интересно, могу рассказать подробнее.
gdbserver?
Это вариант, но не то, что я сделал: в этом случае все равно придется установить gdbserver в контейнере.
Я сделал с помощью namespaces. GDB запускается в PID namespace целевого процесса и в новом mount namespace, аналогично nsenter -t <pid> -p unshare --mount-proc gdb ... -p 1
. Нужно еще добавить -iex set sysroot /proc/1/root -iex set auto-load safe-path /proc/1/root/<solib paths> -iex set solib-search-path /proc/1/root/<solib paths>
. Еще важно: если в mount namespace GDB есть такой же путь к бинарнику, как в mount namespace целевого процесса (условно /usr/bin/python3
), то его нужно скрыть (с помощью bind mount, например), чтобы GDB читал символы из /proc/1/exe
.
Профайлер Бедного Человека: первое знакомство и (приятные) последствия