- PVSM.RU - https://www.pvsm.ru -

Производительность .NET: приемы настоящего джедая

Привет! Нужно ли вам представлять настоящего джедая .NET, performance-гуру, многократного Microsoft MVP, постоянного спикера конференции DotNext [1] Сашу Гольдштейна? Наверно, не стоит.

Производительность .NET: приемы настоящего джедая - 1

В нашей беседе Саша делится профессиональными советами для разработчиков .NET и .NET Core. Рассказывает о том, на что обращать внимание при профилировании и отладке приложений и какими инструментами пользоваться.

— Саша, про производительность в .NET есть много статей и советов. Начиная с того, что нежелательно увлекаться повсеместным выбрасыванием исключений и использованием StringBuilder вместо конкатенации, заканчивая низкоуровневыми оптимизациями. Но .NET постоянно развивается, появляются новые возможности и новые проблемы. Для современного .NET 4.7 можешь дать какие-либо советы прикладного характера по оптимизации производительности кода?

Саша Гольдштейн: Многие уже знакомы с «очевидными» советами о конкатенации строк, исключениях или boxing/unboxing, но все же существуют некоторые заблуждения относительно производительности при появлении новых или более высокоуровневых API. Меня беспокоит чрезмерное использование LINQ во многих кодовых базах. Несмотря на то, что с момента появления LINQ прошло много лет и есть много данных, показывающих, что большинство запросов LINQ можно сделать в 10 раз быстрее, используя обычные циклы, люди до сих пор частенько прибегают к использованию LINQ в коде, чувствительном к производительности. Уточню: я не имею ничего против LINQ в целом, но такое решение не будет работать хорошо, если ваш цикл работает 1 миллион раз.

Еще одна вещь, которую нам не удалось вложить в головы всем, — опасность чрезмерного выделения памяти. Сборщик мусора в .NET привнес некоторые улучшения, но он по-прежнему не помогает, если вы выделяете слишком много, особенно — на объекты, которые в конечном итоге умирают в старых поколениях. Трудно научить всех внимательнее относиться к проблеме выделения памяти, хотя с некоторыми инструментами это становится легче. Например, Heap Allocation Analyzer [2] на Roslyn или профилировщики, такие как dotMemory [3].

Последнее замечание, которое я хотел бы сделать, заключается в том, что мы возвращаемся к временам, когда минимальные объемы памяти и время запуска становятся критическими для серверных приложений (а не только для рабочих станций) из-за контейнерных технологий. Если мы планируем упаковать 300 экземпляров микросервиса, работающего в Docker на одном физическом хосте, то нам нужно быть очень осторожными с управлением памятью, избегать ненужных зависимостей и избавляться от ненужной работы. Когда вы привыкли к 16-ядерным 32-гигабайтным серверам в качестве стандартной среды выполнения, вас отрезвит попытка сжать ваш сервис в «--memory 256m --cpus 0.25». Кстати, мы сталкиваемся с теми же проблемами в отношении «бессерверных [4]» технологий, но блок развертывания настолько мал, что обычно легче жить с ограничениями ресурсов.

— Сегодня .NET не только для Windows. Появились Mono и .NET Core. Скажи, насколько хороши эти инструменты с точки зрения производительности, особенно в сравнении с нативными приложениями под Linux?

Саша Гольдштейн: Mono существует достаточно долго, но, честно говоря, я не использовал его в боевых проектах. Я знаю, что у него зрелый и достойный runtime, но Mono не получил того признания, на которое я рассчитывал. В любом случае, я не могу давать рекомендации относительно его эффективности.

На сегодняшний день я могу рассказать подробнее о .NET Core. В Linux .NET Core использует более или менее похожий стек и кодовую базу, что и в Windows. Компилятор выполняет те же оптимизации, что и в Windows, есть способ избежать JIT-компиляцию, выполнив компиляцию AOT (которая называется Crossgen в .NET Core), есть сборщик мусора с некоторыми особенностями и так далее. Я бы сказал, что единственная часть .NET Core, которая может вызывать вопросы, — PAL (Platform Adaptation Layer), потому что это действительно единственное место, которое имеет совершенно отличную кодовую базу от Windows-версии. На самом деле в PAL существовали проблемы производительности, в основном вокруг неправильного использования API-интерфейса Linux или использования Linux API с непредвиденным поведением по производительности, в сравнении с аналогичным API в Windows. Эти морщины, естественно, будут сглажены с течением времени, поскольку все больше разработчиков используют .NET Core в продакшне и сталкиваются с этими проблемами.

Интересна разработка, которая потребует времени для созревания, — CoreRT (.NET Native для Windows), которая в конечном итоге стремится создать управляемое приложение, не имеет внешних зависимостей и вообще не генерирует JIT. В дополнение к упрощению изоляции (без совместного использования, без установки, без управления зависимостями) CoreRT сократит время запуска и уменьшит объем памяти, «стряхнув» ненужный код. Это может подвинуть .NET еще ближе к native-приложениям в Linux.

— Один из главных советов, который указывают практически во всех статьях о повышении производительности .NET, — это использование профилировщика. На DotNext 2017 Moscow ты будешь рассказывать про отладку и профилирование .NET Core приложений под Linux. Как обстоят дела у Visual Studio с отладкой и профилированием приложений под .NET Core?

Саша Гольдштейн: Visual Studio в настоящее время не предлагает ничего для профилирования приложений .NET Core в Linux. Вы не можете использовать инструментарий Windows для профилирования приложений .NET Core — вам необходимо выполнить профилирование в Linux, и единственный способ фактического анализа результатов в Windows (если вы склонны к этому) — использовать PerfView, чуть более сложный инструмент по сравнению Visual Studio Profiler.

Аналогично, Visual Studio не может открывать дампы ядра приложений .NET Core в Linux. Богатые данные отладчика, которыми вы пользуетесь при открытии файла дампа Windows в Visual Studio, не существуют для дампов ядра; функции анализа памяти, представленные в Visual Studio 2013, не работают для дампов ядра. На самом деле, нет инструмента под Windows, который может открыть дампы приложений из Linux. Для этого вам нужно использовать инструменты Linux, которые в настоящее время предлагают гораздо более широкие возможности.

Как вы уже заметили, мне это не совсем нравится. Не могу сказать, что я очень удивлен. Несмотря на все те усилия, которые MS вкладывает в развитие платформы, и тот факт, что мы уже находимся на версии 2.0, у нас все еще нет хороших инструментов для профилирования и дебаггинга. И спустя три года после его анонса под Linux тут нет достойного профилировщика и отладчика, которым мог бы пользоваться средний разработчик. Это серьезное ограничение, лично меня это иногда выводит из себя. Я, честно, не рекомендую своим клиентам переключаться на .NET Core на Linux, так как знаю, что они будут биться об стену, когда попытаются отладить или профилировать свои production-приложения.

— В мире Linux-разработки пользуются популярностью perf и ftrace, которые предоставляют неплохие возможности для анализа производительности приложений. Помогут ли они при отладке под .NET Core? Есть какие-либо отличия при использовании perf или ftrace для нативных приложений Linux и для приложений под .NET Core?

Саша Гольдштейн: Да. Официально рабочий процесс для профилирования приложений .NET Core в Linux основан на perf. Microsoft предоставляет Bash-скрипт под названием «perfcollect», который запускает perf, собирает данные о производительности, объединяет их в один файл и предлагает вам открыть его в Windows с помощью PerfView. Давайте на мгновение проигнорируем смехотворность этой истории и поговорим о том, как работает этот процесс.

Perf — многоцелевой инструмент со множеством различных режимов работы. В частности, он может использоваться как профилировщик. Подключаться к различным системным событиям, собирать трассировки стека при возникновении этих событий, а затем меппить адреса из этого трассировочного стека на имена методов. Он также обладает некоторыми возможностями визуализации, но их часто заменяют, например при помощи Flame Graphs. Теперь perf является не просто профилировщиком CPU: вы можете присоединить его к процессам, к событиям промахов кэш-памяти, к событиям записи или чтения диска, вы можете присоединить его к переключениям контекста планировщиком и другим тысячам дополнительных статических и динамических событий. Ближайшее, что у нас есть в Windows, — это инструмент ETW (например, PerfView, Windows Performance Recorder и т. д.), но perf может предложить динамическое инструментальное средство (присоединение к произвольной функции в ядре или в библиотеке пользовательского пространства), чего ETW сделать не сможет.

При использовании perf для приложений .NET Core есть одна проблема, которую вам нужно преодолеть. Она связана с преобразованием адресов. Когда perf захватывает трассировку стека, ему необходимо иметь возможность маппить возвращаемые адреса в стеке на имена методов.

Поскольку .NET Core использует JIT-компиляцию, нет статического местоположения, по которому можно сделать этот маппинг (debuginfo, symbols, неважно, как это называть). Способ, с помощью которого работает perf в этом случае, заключается в том, что он ожидает, что целевое приложение будет писать простой текстовый файл с именем /tmp/perf-$PID.map, который содержит сопоставления адресов методов с их именами.

Действительно, .NET Core поддерживает это соглашение — если вы установите переменную среды «COMPlus_PerfMapEnabled» равной 1, JIT будет писать в этот текстовый файл каждый раз, когда метод скомпилируется, и perf сможет использовать эту информацию для успешного меппинга адресов. Немного жаль, что вам нужно сделать это заранее — если вы не задали переменную окружения и хотите профилировать уже запущенный процесс, вам не повезло — но также работает и Node.js, к примеру, поэтому я считаю, что это более или менее приемлемо.

История имеет еще один поворот, на этот раз с AOT-скомпилированными сборками. Если вы используете Crossgen для AOT-компиляции (.NET Core использует его в некоторых своих сборках в пакете .NET Core, который вы получаете из диспетчера пакетов вашего дистрибутива), вам нужен еще один способ получить отладочную информацию для этих сборок.

Инструмент Crossgen сам может генерировать эту информацию, если вы укажете это в Crossgen-скомпилированной сборке. Пока все хорошо, правда? Не совсем. Во-первых, Crossgen не ставится с .NET Core, поэтому вам нужно либо собрать CoreCLR из исходников, чтобы получить его, либо использовать дурацкий трюк NuGet-восстановления. Во-вторых, Crossgen выдает отладочную информацию в формате, который несовместим с тем, что ожидает perf, поэтому вам нужно форматировать и объединять свой вывод с основным map-файлом peft. И в-третьих, perf в настоящее время не поддерживает просмотр perf-maps-файлов для разделов памяти, которые записываются на диске, как это делает Crossgen. Поэтому даже если бы вы смогли получить хорошую perf-map для этих сборок, perf проигнорирует их. К счастью, есть и другие инструменты, которые все же будут работать в этом случае.

Кстати, есть много других инструментов, которые «зажигаются» и работают с процессами .NET Core. В моем докладе на DotNext я буду использовать perf вместе с инструментами, основанными на BPF, от BCC. Мы обсуждали BPF и BCC несколько месяцев назад на конференции JPoint [5]о профилировании JVM с BPF.

— Хотелось бы задать несколько вопросов о LTTng. В официальной документации говорится, что в LTTng производительность поставлена во главу угла. Сохраняется ли это правило при трассировке .NET Core приложений? Есть ли какие-либо ограничения LTTng при использовании tracepoint или kprobes?

Саша Гольдштейн: Давайте расставим точки над «и». LTTng — это мощный фреймворк для трассировки на Linux, который имеет два режима работы. У него есть модуль ядра, который может подключаться к tracepoint, являющимся статически определенными местоположениями трассировки, разбросанными по ядру: события планировщика, обращения к диску, выполнение процессов и т.д. Кроме того, LTTng имеет userspace-library, которая может использоваться для отслеживания событий в пользовательском приложении, и именно это использует .NET Core на Linux. В обоих случаях LTTng оптимизирован для высокой частоты срабатывания событий за счет буферов с общей памятью, компактного двоичного формата и быстрой записи на диск.

Как вы, возможно, знаете, .NET на Windows имеет множество ETW-событий, которые могут использоваться для профилирования производительности и понимания поведения системы. К ним относятся события GC, сборка, компиляция JIT, создание объектов и многие другие. В Linux ETW (Event Tracing for Windows), конечно, недоступна, поэтому Microsoft предпочли использовать LTTng. Вы получаете те же самые события, но они создаются через LTTng, а не через ETW — и с несколькими оговорками.

Во-первых, вы должны установить переменную окружения (COMPlus_EnableEventLog = 1). Если этого не сделать, никаких событий LTTng вообще не создаст. Во-вторых, LTTng не поддерживает трассировки стека для событий в пользовательском пространстве. Это означает, что вы можете перехватывать GC-события, но у вас нет стека вызовов, в котором они вызывались; вы можете перехватить события загрузки сборки, но вы не знаете, какой код загрузил эту сборку. Это очень болезненные моменты, которые ограничивают возможности использования этих событий в реальных сценариях устранения неполадок.

— Отладчик LLDB под Linux очень похож на WinDbg под Windows. Для него даже доступно расширение SOS, которое позволяет отлаживать управляемый код. Насколько он применим уже сейчас для отладки .NET Code приложений?

Саша Гольдштейн: LLDB — очень мощный отладчик, и Microsoft предоставляет библиотеку libsosplugin.so, которая является версией SOS.dll для Linux. Он предоставляет почти тот же набор команд с той же семантикой, что хорошо, если вы знакомы с SOS (хотя вам все равно придется изучать собственные команды LLDB, которые существенно отличаются от WinDbg). Но это не тема данного разговора, не так ли? Для LLDB с libsosplugin вам придется столкнуться со следующими препятствиями:

  • Плагины LLDB тесно связаны с конкретной версией LLDB. Поскольку libsosplugin.so поставляется с CLR, он построен для версии LLDB, используемой Microsoft в процессе их сборки, которая — на момент написания — была LLDB 3.6. Это довольно старая сборка LLDB, в которой есть множество известных ошибок, и на самом деле ее практически невозможно установить на множество современных дистрибутивов, не собирая из исходного кода.
  • LLDB до 4.0 не понимает дампы ядра, генерируемые по требованию, поэтому вы можете либо открывать дампы ядра, созданные в результате сбоя, либо присоединяться к запущенному процессу.
  • Когда вы открываете дамп ядра или присоединяетесь к работающему процессу, вы должны научить плагин SOS сопоставлять идентификаторы потоков ОС с идентификаторами управляемых потоков .NET. Если у вас 400 потоков, это действительно раздражает (у меня есть скрипт для этого).

— Разработка качественного многопоточного приложения — сложное занятие. Но еще более сложное — поиск узких мест и ошибок в таком приложении. Чем могут пользоваться разработчики для дебага и профилирования мультипоточных приложений под .NET?

Саша Гольдштейн: Об этом можно говорить сколь угодно долго, поэтому я буду краток. Существует несколько важных методов, которые можно автоматизировать с использованием современных инструментов:

  • Понимание того, какой код вызывает частые проблемы (блокировки, но не только). Это можно сделать, анализируя события переключения контекста.
  • Понимание распределения рабочей нагрузки между различными потоками. Это часто делается визуально, с использованием таких инструментов, как Visual Studio Concurrency Visualizer или функция временной шкалы в dotTrace.
  • Понимание различных видов oversubscription — удушение процессора (например, из-за контейнеров), проблемы приоритетов или просто отсутствие достаточного количества ядер для всех ваших потоков. Это можно сделать, анализируя автоматические события переключения задач и создавая гистограммы времен, в которые поток обрабатывается на CPU или ожидает своей очереди на обслуживание при условии отсутствия внутренних причин, препятствующих этому.

— Какие доклады ты подготовил для DotNext 2017?

Саша Гольдштейн: В Москве я расскажу [6] о профилировании и отладке приложений .NET Core в Linux. Это результат многих месяцев исследований. Расскажу о некоторых инструментах и методах, которые упоминал выше, наряду с живыми демонстрациями общих проблем производительности в приложениях .NET Core. В качестве специального бонуса я покажу, как использовать некоторые из этих инструментов для профилирования приложения .NET Core, работающего в Docker-контейнере. Все мои демо доступны на GitHub [7], поэтому вы сможете экспериментировать с ними после конференции.

А для тех, кто хочет полного погружения, 11 ноября в Москве состоится 8-часовой практический тренинг «Production Performance and Troubleshooting of .NET Applications [8]», посвященный инструментам и подходам мониторинга и решения проблем с производительностью на проде.


Если вы так же любите нутрянку .NET, как и мы, то вас могут заинтересовать выступления других экспертов на грядущей конференции DotNext 2017 Moscow [1], где выступят более 30 спикеров с докладами о настоящем и будущем платформы .NET, об оптимизации производительности и многопоточности, о внутреннем устройстве платформы .NET и CLR, о профилировании и отладке .NET-кода.

Автор: ARG89

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/proizvoditel-nost/264893

Ссылки в тексте:

[1] конференции DotNext: https://dotnext-moscow.ru/

[2] Heap Allocation Analyzer: https://www.nuget.org/packages/ClrHeapAllocationAnalyzer/

[3] dotMemory: https://www.jetbrains.com/dotmemory/

[4] бессерверных: https://en.wikipedia.org/wiki/Serverless_computing

[5] JPoint : https://jpoint.ru/

[6] расскажу: https://dotnext-moscow.ru/2017/msk/talks/2dfoltply8kscqeoa6444g/

[7] GitHub: https://github.com/goldshtn

[8] Production Performance and Troubleshooting of .NET Applications: https://dotnext-moscow.ru/2017/msk/trainings/m4aetwwkioiwk4yaaouaq/

[9] Источник: https://habrahabr.ru/post/339230/