Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Введение
Однажды перед нами была поставлена задача: портировать набор приложений на C# с Windows/.NET Framework на Linux/.NET Core. Я полагаю, что для Microsoft мы были клиентом с рабочими нагрузками, которые было бы интересно поддерживать с помощью .NET Core. В то время я не понимал, насколько сильным было их стремление работать с нами. Наш подход к Open Source был решающим фактором.
Насколько сложной она может быть? Ну… в этом посте мы расскажем вам о трудностях, с которыми нам пришлось столкнуться, чтобы запускать, контролировать и отлаживать наши приложения.
Попробуйте
Как только мы получили билд всех сборок .NET Core (подробнее об этом в одной из следующих статей блога), пришло время запустить несколько приложений. Первые проблемы, с которыми мы столкнулись, были связаны с отсутствием функций между .NET Framework и .NET Core. Например, нам нужна поддержка криптографии 3DES и AES с режимом шифрования CFB, но она (все еще) отсутствует в .NET Core для Linux. Благодаря статусу Open Source .NET Core мы смогли добавить ее в CoreFx. Однако, поскольку мы не реализовали его на MacOS/Windows, как того требовала Microsoft, чтобы наше изменение было принято в качестве Pull Request, нам пришлось сохранить нашу форкнутую ветку.
Второй класс проблем рантайма, которые нам пришлось решать, был связан с различиями между Windows и Linux, а также с «контейнеризацией» рантайма. Рассмотрим два примера, связанных со сборщиком мусора .NET. Во-первых, наши контейнеры использовали Linux cgroups для управления квотами, включая память и количество ядер процессора, используемых приложениями. Однако при запуске CLR GC подсчитывал общее количество ядер CPU, чтобы вычислить количество куч для выделения, а не то, которое определено на уровне cgroup: В итоге мы получали мгновенное автоматическое уничтожение Out Of Memory. На этот раз наше исправление было сделано и внесено в репозиторий CLR.
Второй пример связан с оптимизацией GC: Во время фоновых коллекций 2-го поколения потоки CLR, работающие под ними, аффинируются к каждому отдельному ядру CPU, чтобы избежать блокировок. Нам посчастливилось принять Маони Стивенс (ведущего разработчика GC) в нашем парижском офисе в начале 2018 года, чтобы поделиться нашими странными паттернами распределения, которые влияют на GC. Во время своего пребывания она была достаточно любезна, чтобы помочь нам исследовать поведение на наших серверах: Когда был запущен SysInternals ProcessExplorer, сборка мусора занимала больше времени, чем обычно. Маони обнаружила, что ProcessExplorer имеет аффинитизированный поток с высоким приоритетом, конфликтующий с потоками GC. Во время расследования, связанного с более длительным временем отклика в Linux по сравнению с Windows. Мы поняли, что потоки GC не были аффинитизированы, как это было в случае с Windows, и проблема была исправлена Яном Ворличеком.
Вот наш урок: иногда исправления вливаются в официальный релиз, а иногда нет. Если ваши рабочие нагрузки доводят .NET до предела, вам, вероятно, придется создавать и управлять собственным форком Core и делать его доступным для своих развертываний.
Контролируйте его
Наши приборные панели Grafana, измеряющие состояние приложений .NET Framework, были основаны на метриках, вычисленных на основе счетчиков производительности Windows. Даже без перехода на Linux, .NET Core больше не предоставляет счетчики производительности, поэтому нам пришлось полностью перестроить нашу систему сбора метрик!
Основываясь на отзывах Microsoft, мы решили прослушивать события CLR, передаваемые через ETW в Windows и LTTng в Linux. Помимо того, что эти события работают в обеих операционных системах, они также предоставляют точную информацию о сдерживании потоков, исключениях и сборках мусора, недоступную при использовании счетчиков производительности. Пожалуйста, обратитесь к серии статей нашего блога для получения более подробной информации и многократно используемых примеров кода для интеграции этих событий в ваши собственные системы.
Наша первая реализация сбора метрик в Linux была основана на LTTng, и мы представили наш путь во время Tracing Summit в 2017 году. Microsoft уже создала TraceEvent, сборку, позволяющую коду .NET анализировать события CLR как для Windows, так и для Linux. К сожалению для нас, Linux-часть могла только загружать файлы трассировки, но нам нужна была живая сессия, как в Windows, где можно слушать события, испускаемые запущенными приложениями. Поскольку этот код является Open Source, Грегори смог добавить функцию живой сессии в TraceEvent.
В .NET Core 3.0 Microsoft предоставила способ обмена событиями, общий для Linux и Windows, под названием EventPipes. Итак… мы перенесли нашу реализацию коллекции с LTTng на EventPipe (смотрите серию наших блогов и сессию конференции DotNext для получения более подробной информации и примера многократно используемого кода). С новой реализацией EventPipe в CLR возникли проблемы с производительностью, не замеченные Microsoft. Причина проста: Некоторые из наших приложений запускают сотни потоков для обработки тысяч запросов в секунду и выделяют память как сумасшедшие. В таком контексте CLR нужно многое сделать, а значит, нужно генерировать и передавать много событий через LTTng или EventPipes.
(Количество обработанных событий CLR в минуту; даже при ограниченном количестве ключевых слов gc/thread/exception, которые мы используем, выдается еще больше)
В первоначальной реализации отсутствовала некоторая фильтрация, генерировалось слишком много событий или создавалась затратная полезная нагрузка событий, хотя события не испускались. Основываясь на наших отзывах, команда Microsoft Diagnostic была очень отзывчива и быстро устранила проблему.
Microsoft не «просто» перешла на Open Source, команды работают в глубокой интеграции с моделью выпуска/запроса на GitHub. Поэтому не стесняйтесь и, если вы обнаружили проблему, создайте задачу с подробным описанием, а еще лучше — предоставьте запрос на исправление. От этого выиграет все сообщество!
Запускайте
С помощью этих показателей мы начали исследовать некоторые различия в производительности (в основном время отклика) между Windows и контейнерным Linux.
(Чем ниже, тем лучше)
Мы увидели огромную разницу в производительности на Linux: Как время отклика (x2), так и масштабируемость (увеличение таймаута с ростом QPS). Наша команда потратила много времени на улучшение ситуации, вплоть до того, что можно было отправлять приложения в продакшн.
В новой среде с контейнерами мы столкнулись с теми же признаками шумных соседей, что и в Process Explorer. Если ядра процессора не выделены для контейнера (как это было у нас в начале), такой сценарий происходит очень часто. Поэтому мы обновили систему планирования, чтобы выделить ядра ЦП для контейнеров.
В совершенно другой области мы обнаружили, что то, как .NET Core обрабатывает сетевой ввод-вывод, повлияло на наше основное приложение. Чтобы немного пояснить, это приложение должно обрабатывать большое количество запросов и зависит от времени отклика. Во время обработки запроса текущему потоку может потребоваться отправить HTTP-запрос, прежде чем продолжить обработку. Поскольку это делается асинхронно, поток становится доступным для обработки большего количества входящих запросов, что хорошо для пропускной способности. Однако это означает, что когда внутренний HTTP-запрос вернется, все доступные потоки могут обрабатывать новые входящие запросы, и потребуется время для завершения старого. Чистый эффект — увеличение медианного времени отклика, а это не то, чего мы хотим!
Реализация .NET Core полагается на .NET ThreadPool, который разделяет свои потоки со всей магией async/await и обработкой входящих запросов (реализация .NET Framework использует совершенно другую реализацию, основанную на портах завершения ввода/вывода в Windows). Чтобы решить эту проблему, Кевин реализовал собственный пул потоков для обработки сетевого ввода-вывода, и мы продолжаем его оптимизировать. Когда вы работаете над такой глубокой областью кода, разделяемой столькими различными рабочими нагрузками, вы понимаете, что невозможно найти «серебряную пулю».
Отладка
Что бы вы сделали, если бы что-то пошло не так в приложении? В Windows с помощью Visual Studio мы можем удаленно отлаживать неавторизованное приложение, чтобы установить точку останова, просмотреть поля и свойства или даже получить высокоуровневое представление о том, что делают потоки с помощью представления ParallelStacks. В худшем случае SysInternals procdump позволяет нам сделать снимок приложения и проанализировать его на машине разработчика с помощью WinDBG или Visual Studio.
Что касается удаленной отладки приложения Linux, Microsoft предоставляет решение на основе SSH для подключения к запущенному приложению. Однако, по соображениям безопасности, в наших контейнерах нельзя запускать SSH-сервер. Решением было реализовать протокол связи с VsDbg для Linux поверх WebSockets.
Приложение Remote Debugger sidecar на рисунке проксирует команды из Visual Studio в VsDbg, который действует как отладчик Linux .NET с приложением. Нет необходимости в рискованном SSH, только аккуратный HTTP.
Ну… этого оказалось недостаточно. Архитектура хостинга (Marathon и Mesos в нашем случае) обеспечивает бесперебойную работу приложений в контейнерах, отправляя запросы на конечные точки проверки работоспособности. Если приложение отвечает, что все в порядке, значит, контейнер в безопасности. Если приложение не отвечает, как ожидалось (включая повторные попытки), то Marathon/Mesos убивает приложение и очищает контейнер. Теперь подумайте, что произойдет, если вы установите точку останова в приложении и несколько минут будете копаться в содержимом структур данных в панелях Visual Studio Watch/Quick Watch. За сценой отладчик должен заморозить все потоки приложения, включая те из пула потоков, которые отвечают за проверку работоспособности. Как вы уже, наверное, догадались, сеанс отладки ничем хорошим не закончится.
Именно поэтому на предыдущем рисунке показана стрелка между Marathon и удаленным отладчиком, который действует как прокси для проверки работоспособности приложения. Когда начинается сеанс отладки (т.е. когда код WebSockets выполняет протокол), удаленный отладчик знает, что ему следует ответить OK вместо вызова конечной точки приложения, которая может никогда не ответить.
Когда удаленной отладки недостаточно, как сделать моментальный снимок памяти приложения? Например, если проверка работоспособности не отвечает после серии повторных попыток, удаленный отладчик вызывает инструмент createdump, установленный вместе со средой выполнения .NET Core, для создания файла дампа. Опять же, поскольку создание дампа памяти приложения размером 40+ ГБ может занять несколько минут, был введен тот же механизм прокси проверки работоспособности.
Как только файл дампа будет создан, удаленный отладчик позволит Marathon уничтожить приложение. Но подождите! Этого недостаточно, потому что в этом случае контейнер будет очищен, а дисковое хранилище исчезнет. Это не проблема, после создания дампа программой createdump, файл отправляется в приложение «Dump Navigator» (по одному на центр обработки данных). Это приложение предоставляет простой пользовательский интерфейс HTML для получения высокоуровневых деталей состояния приложения, таких как стеки потоков или содержимое управляемой кучи.
В Windows мы создали собственный набор команд расширения, которые позволяют нам исследовать память, нехватку пула потоков, конкуренцию потоков или случаи утечки таймера в дампе памяти Windows с помощью WinDBG, как показано на этой сессии конференции NDC Oslo. Обратите внимание, что их также можно использовать с LLDB в Linux. Эти команды используют библиотеку ClrMD Microsoft, которая дает вам доступ к реальному процессу или дампу памяти в C#. Благодаря поддержке Linux, которая была добавлена в эту библиотеку разработчиками Microsoft, код было легко повторно использовать в нашем приложении Dump Navigator. Я однозначно рекомендую обратить внимание на API, предоставляемый ClrMD, для автоматизации и создания собственных инструментов.
Заключение
Несмотря на то, что некоторые из наших основных приложений перешли на .NET Core, работающий на контейнерном Linux с большим набором инструментов мониторинга/отладки, путь еще не закончен. Сейчас мы тестируем предварительную версию .NET Core 5.0 (как мы это делали для 3.0). Если это не так, мы выясним причины и найдем решения для интеграции в код. То же самое касается инструментов: я начал добавлять наши команды расширения в инструмент Microsoft dotnet-dump CLI, используемый для анализа дампов Windows и Linux.
По крайней мере, мы можем сказать, что помогли не только себе, но и Microsoft понять, как далеко может зайти .NET Core, и даже всему сообществу .NET Windows и Linux. Вот где Open Source сияет!