Профайлинг .NET Core приложения в Linux

А тем временем я продолжаю разбираться в том, как дебагить .NET Core приложения в Linux. Теперь я уже более или менее представляю, как анализировать проблемы с памятью, но таких на самом деле у нас не так уж и много. Самая распространённая проблема — загрузка процессора под 100%, и тогда очень, очень хочется понять, чем же это приложение таким занимается.

На Windows я первым делом лезу в логи и трейс-файлы. Когда их не хватает, приходит очередь PerfView, который с огромной частотой сэмплирует приложение, и тогда уже можно посмотреть, сколько потоков сейчас запущено, чем они занимаются, какие самые горячие места, и т. п.

Логи и трейсы, разумеется, существуют и на Linux, но мне было интересно, можно ли на нём профайлить .NET Core примерно так же, как и на Windows. Как оказалось, инструментов почти куча, но применительно к .NET Core основными выглядят три: утилиты perf, lttng  и perfcollect. Вот на них мы сейчас и посмотрим.

Песочница (профайлинг идёт следом)

Нам нужно тестовое .NET Core приложение для профайлинга, линуксовый хост и какие-нибудь инструменты разработчика на нём. Так как Vagrant и VirtualBox ещё никто не отменял, всё это можно сделать за минуты.

Во-первых, тестовое приложение, которое тупо делает много бессмысленной математики в бесконечном цикле:

Во-вторых, вот нам Vagrantfile, который и хост с Убунтой создаст, и .NET Core SDK с  perf, lttng и perfcollect  установит:

Теперь, выполняем vagrant up и начинаем развлекаться.

Ищем «горячие» функции с perf

Это просто удивительно, как легко начать делать что-то с perf. То есть документации, примеров и зубодробительных концепций под капотом хватает, но, чтобы получить мало-мальский полезный результат, нужно совсем немного.

Так как Vagrantfile нам всё установил, вот, что мы сейчас сделаем: сбилдим и запустим тестовое приложение в фоновом режиме, а потом посмотрим, чем же оно занимается изнутри.

Запуск приложения в фоновом режиме возвращает очень полезное число — 13689. Полезное потому, что это айдишка процесса приложения, и она нам понадобится, чтобы натравить на него perf. Кроме PID мы ему дадим параметр -g, чтобы в отчётах были стеки вызовов, и команду record, чтобы начать сэмплирование.

Итак, запускаем perf, даём ему возможность пособирать статистику секунд десять или около того, и останавливаем веселье при помощи ctrl+C:

Барабанная дробь — смотрим отчёт:

perf: отчёт без имён функций

Ну, что я скажу. Красиво, конечно, но не то, чтобы как-то особенно полезно. В таблице с отчётом выведены все функции, которые хоть раз выполнялись, пока perf собирал статистику. Для каждой из них: в первом столбце — как долго выполнялись дочерние функции, а во втором — как долго выполнялась она сама.

Если бы я искал проблемы в производительности, то высматривал бы функции, у которых «дочернее» и «собственное» время заметны и примерно равны — то есть функция работала много и выполняла самы себя. Я вижу аж четыре кандидата внизу списка, но вот незадача — адреса функций есть, а имён — нет.

Но к счастью для нас в CoreCLR есть секретный выключатель, который поможет perf собрать и имена тоже. Это переменная окружения COMPlus_PerfMapEnabled, которой нужно задать единицу и передать либо напрямую процессу, либо установить всей сессии (export COMPlus..):

Да, есть ещё такой момент. Так как perf запускался через sudo , а dotnet и CoreCLR — нет, то у отчёта perf и маппинг файлов CLR скорее всего будут разные владельцы, что для perf очень и очень печально. С другой стороны, если ему сказать -f, то на владельцев файлов ему внезапно станет накласть:

perf: отчёт с именами функций

Красота? Практически все имена на месте, и сразу видно, что самые «горячие» функции — Aggregate и две лямбды для сложения и умножения, которые мы ей передавали.

Смотрим результат в FlameGraph

Отчёт-таблица — это, конечно, здорово, но если бы было можно посмотреть результат в виде графика, как в том же Google Chrome, то было бы вообще прекрасно. И это, оказывается, очень просто сделать. Нужно скачать проект FlameGraph и скормить ему результаты perf через два perl файла: stackcollapse-perf и flamegraph:

Так как моя Убунта запущена без UI, то посмотреть результирующий SVG файл с графиком было бы проблематично. С другой стороны, на машине есть установленный python2.7 и проброшенный к хостовой машине восьмидесятый порт (80->8080, Vagrantfile), так что можно запустить временный веб-сервер и посмотреть картинку через браузер на хостовой машине:

flamegraph

Ничего нового картинка, конечно, не сказала, но зато как красиво! Кстати, там всё кликабельно и мыше-двигательно, так что можно и увеличить отдельные ветки, и посмотреть детали в тултипе.

Собираем события рантайма через lttng

Есть типы дотнэтовских событий, которые было бы проблематично слушать через perf. Всякие там сборки мусора, загрузки assemblies, брошенные и отловленные ошибки. Возможно, какие-то их следы и можно было бы заметить в perf отчётах, но скорее всего те редкие события были бы похоронены под более частыми вызовами пользовательских функций.

С другой стороны, дотнэтовский рантайм умеет генерировать целый набор сообщений/событий как раз о таких вещах, и если их включить, то при помощи уже другой утилиты lttng можно будет всё это собрать. События включаются через ещё одну переменную окружения COMPlus_EnableEventLog, и ей тоже нужно значение 1.

Чтобы сделать жизнь немного интереснее, добавим-ка мы в тестовый проект кусок кода, который выбрасывает и тут же отлавливает ошибку. Снаружи ничего не будет видно, но заглушенные ошибки в продакшене, в общем-то, хороший повод спросить, а всё ли нормально там.

Итак, изменения в коде:

Билдим/запускаем:

И теперь будем слушать события. Пользоваться lttng чуть-чуть сложнее, чем perf, потому что нужно и сессию создать, и на события подписаться, и запустить всё это дело. А потом и выключить в обратном порядке. Но на самом деле всё вменяемо.

Единственная загадка: какие вообще события дотнэт рантайм поддерживает, чтобы знать, на что подписываться? Чтобы не пришлось лезть в исходники дотнэта и собирать события по-одному, можно скачать perfcollect скрипт, в котором все они перечислены в одном месте. Это всего-лишь малая часть того, что там есть:

Нам, в принципе, только и ошибки и нужны, так что можно подписаться сразу на все через маску:

lttng результаты идут в очень бинарных файлах, так что смотреть их лучше при помощи babeltrace утилиты, которая, насколько я понимаю, установилась одновременно с lttng. Имя файла с результатами мы уже знаем (строка после lttng create), так что смотрим:

Конечно, отчёт не показывает, где именно ошибка произошла, да и стека события нет, но мы ясно видим текст ошибки, её тип, где она была поймана, и этого более чем достаточно для диагностики.

Собираем всё подряд через perfcollect

В одном из дотнэтовских репозиториев есть утилитка под названием perfcollect, которая собирает практически всё то же самое, что и perf с lttng. Более того, perfcollect на самом деле всего-лишь обычный bash скрипт, который эти самые perf с lttng и вызывает. Казалось бы, зачем он тогда упал? Есть, оказывается, причины:

  1. У него есть команда install, которая устанавливает практически все зависимости, включая perf и lttng, и эта команда понимает, как работать на разных дистрибутивах Linux.
  2. Как и PerfView на Windows, все собранные данные упаковываются в один zip архив, которым удобно меняться с товарищами и рабочими машинами.
  3. Если показать ему, где лежит дотнетовская crossgen утилита, то perfcollect сможет собрать немного больше дебаггинг информации, чем perf и lttng по-отдельности.
  4. У perfcollect есть команда view, которая может показать отчёт как в perf, так и в lttng. Знать одну команду всегда проще, чем две.
  5. Отчёт perfcollect можно перенести на виндовую машину и смотреть уже в PerfView.

И на самом деле им очень просто пользоваться. Скачиваем один файл, делаем его исполняемым, просим установить зависимости, и начинаем работать.

Так как perfcollect будет пользоваться и perf, и lttng, то нужно устанавливать уже обе переменные окружения:

Профайлим:

Результат собрался в mysession.trace.zip, и его можно посмотреть всё там же, в perfview:

Отчёт perfcollect

Всё знакомо, всё понятно.

Добавляем crossgen

Если подсунуть perfcollect утилитку crossgen, то, теоретически, собранный им отчёт, будет немного полнее — он сможет подтянуть детали о нативных дотнэтовских библиотеках. К сожалению, о том, как именно подсунуть ему crossgen и где вообще того достать, нужно немного подумать.

Во-первых, простейший способ найти правильный crossgen — это опубликовать наше приложение с dotnet publish. В этом случае NuGet подтянет всевозможные дотнетовские зависимости и среди всего прочего положит crossgen себе в кэш ~/.nuget:

Во-вторых, perfcollect будет искать crossgen в той же папке, где лежит и libcoreclr.so, используемый нашим приложением. Но вот, чтобы узнать, где же этот libcoreclr лежит, нужно немного постараться.

  1. Для опубликованных приложений libcoreclr.so будет лежать в publish, рядом с собранным приложением.
  2. Для приложений вроде нашего, которые отдалживают рантайм у хостовой машины, libcoreclr.so будет лежать в папке с остальным рантаймом, и нам надо просто знать, какая его версия используется нашим приложением, и где вообще он, собственно, находится. Так как на моей убунте есть только .NET Core SDK 2.1.3, и я знаю, что он идёт в комплекте с рантаймом версии 2.0.4, то всего-то нужно найти libcoreclr, в пути к которому есть эти цифры.

И такой действительно есть:

Я бы точно не делал ничего подобного в продакшене, но во временной виртуальной Убунте не грех скопировать crossgen прямиком в рантайм:

Если мы запустим perfcollect ещё раз, то он уже не будет жаловаться на отсутствующий crossgen. С другой стороны, я не заметил никакой разницы в результатах на Убунте. Возможно, появилось бы что-нибудь новое на Винде в PerfView, но и того, что я собрал без crossgen, мне хватило.

Вместо заключения

Самая большая проблема во всём этом выяснении, как профайлить .NET Core приложения на Linux, — какие именно задавать вопросы Гуглу. Всё-таки пропасть между моим обычным околовиндовым вэбовским опытом и юниксовой экосистемой для профайлинга очень ощутима, а чтобы искать что-то неизвестное, нужно сначала знать, что именно мне неизвестно.

Но мне повезло наткнуться на цикл постов (вроде такого) от Саши Голдштейна, которые дают отличную начальную площадку, от которой можно уже копать дальше. Более того, он уже писал и о perf, и lttng c perfcollect, так что теперь я думаю: писать об инструментах, которые уже описали другие, — это плагиат, или ещё нет? Непонятно. Но если вдруг кому-то захочется продолжить копать в сторону .NET Core, дебаггинга и Linux, то определённо стоит посмотреть, что он ещё написал.

2 комментария для “Профайлинг .NET Core приложения в Linux

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *