Материал переведен. Ссылка на оригинальную статью
Bing работает с одним из крупнейших в мире, наиболее сложных, высокопроизводительных и надежных приложений .NET. В этой заметке рассказывается о процессе перехода на .NET 5, а также о значительном увеличении производительности, которого мы смогли добиться.
Это приложение находится в центре стека архитектуры Bing и отвечает за координацию работы тысяч других компонентов, которые обеспечивают результаты всех запросов. Оно также лежит в основе многих других сервисов кроме Bing.
Коллектив, которому принадлежит этот компонент, называется XAP ("Zap"). Я являюсь членом этой команды с 2008 года, вскоре после моего прихода в Microsoft (когда Bing был Live Search!). В 2010 году мы осуществили миграцию большей части нашего стека с C++ на .NET Framework.
Работа по миграции XAP на .NET Core началась в июле 2018 года. Мы консультировались с командами, которые уже осуществили этот переход, такими как UX-уровень Bing, а также с самой командой разработчиков .NET.
Коллектив XAP давно поддерживает тесные рабочие отношения с командой разработчиков .NET, особенно с той ее частью, которая занимается Common Language Runtime (CLR). После анонса .NET Core мы уже знали, что захотим перейти на новую версию. Ожидаемые преимущества в производительности, открытый исходный код и более быстрые сроки разработки стали дополнительными стимулами.
Эта миграция стала безоговорочным успехом для нашей команды, налицо были улучшения в производительности и адаптивности.
XAP: Краткое описание
Для того чтобы обеспечить некоторый контекст, позвольте мне передать высокоуровневую картину архитектуры XAP.
Три года назад Mukul Sabharwal подробно описал в блоге, как его команда осуществила миграцию фронтенда Bing на .NET Core. Под этим UX-слоем располагается высокопроизводительный механизм выполнения рабочих процессов (workflow), который управляет координацией и взаимодействием между различными бэкендами данных, обеспечивающими работу Bing. Этот промежуточный слой также используется во многих сервисах Microsoft, не относящихся к Bing.
Следует заметить, что команда разработчиков фронтенда Bing уже перешла на предварительные версии .NET 6. Они начали работу с .NET 6 Preview 1 и теперь находятся на Preview 4. В этой версии были отмечены значительные улучшения, в основном связанные с новыми функциями crossgen2.
Команда XAP имеет две основные сферы ответственности:
Управление центральной частью рабочего сайта для Bing (и других приложений, связанных с поиском, таких как Sharepoint, Cortana, Windows Search и т.д.).
Предоставление среды выполнения, обработчика для рабочего процесса, инструментов и SDK (Software development kit) для наших внутренних партнеров, чтобы они могли сами создавать и работать с различными сценариями на этой платформе.
В Bing, нашей крупнейшей рабочей среде, XAP управляет тысячами компьютеров в глобальном комплексе центров обработки данных. На каждом устройстве XAP загружает более 900 000 экземпляров рабочих процессов и 5,3 миллиона экземпляров плагинов - и все это в процессе с кучей размером 50 ГБ, которая загружает 2500 уникальных сборок и JITs (Just in time) обрабатывает более 2,1 миллиона методов.
По своей сути XAP ApplicationHost - это хост для независимо разработанных плагинов (функций), объединенных в направленный ациклический граф, называемый рабочим процессом (workflow). Рабочие процессы состоят из плагинов и других рабочих процессов. Задача XAP - выполнить этот рабочий процесс как можно эффективнее и быстрее.
Типичный Bing-запрос будет выполнять около 12 000 узлов (включая более 2000 сетевых вызовов) за несколько сотен миллисекунд. Один компьютер обычно обрабатывает более 30 000 узлов в секунду в пиковый момент. Для обеспечения эффективности выполнения, а также для поддержания безопасности системы, мы накладываем многочисленные ограничения на авторов плагинов, такие как:
Ограниченная область применения API. Никаких API, изменяющих процесс, никаких потоков, никакого ввода-вывода, никакой синхронизации. Все эти возможности управляются через обработчиков рабочего процесса и специализированные плагины (например, Xap.HTTP).
Неизменяемое состояние. Плагины не могут ссылаться на изменяемые статические данные. Их результаты делаются неизменяемыми, чтобы избежать синхронизации в их зависимостях.
Строгий мониторинг. Плагины имеют жесткие требования к времени ожидания, и после истечения таймаута мы начнем запускать их зависимости. Плагины, которые часто выходят из строя, автоматически отключаются.
Обработчик runtime также хорошо оптимизирован для эффективного выполнения этих плагинов. Мы максимально избегаем синхронизации потоков, минимизируем выделение памяти, объединяем крупные объекты в пулы, чтобы избежать повторного выделения Large Object Heap, и генерируем код для более успешного выполнения динамически загружаемых плагинов.
Результаты производительности
Перед внедрением мы запускали различные сборки на нескольких тестовых компьютерах и тщательно анализировали результаты. Получив достаточную уверенность в правильности своих решений на данном этапе, мы перешли к мелкомасштабным экспериментам. Начав с десяти произвольно выбранных компьютеров, работающих на .NET 5, мы постепенно расширяли эксперимент, пока весь центр обработки данных не стал использовать .NET 5.
Результаты внедрения
Мы начали внедрять .NET 5 preview 6 в одном центре обработки данных 8 июля 2020 года, почти через 2 года после начала проекта. (С тех пор мы обновили .NET 5 до версии RTM).
Мы задержали развертывание на других центрах обработки данных на несколько недель, чтобы обеспечить достаточное время для мониторинга стабильности и производительности.
Латентность
Латентность (время ожидания) - один из основных показателей, которые мы отслеживаем.
Миграция одного центра обработки данных особенно заметна:
Здесь приведены высокоуровневые данные по различным средам. Эти цифры приблизительны и взяты из ежедневных средних значений за несколько месяцев. Они измеряют % улучшения в двух процентилях.
Между этими центрами обработки данных существует большой разброс, который можно объяснить различиями в трафике и конфигурации компьютеров.
Перегрузка времени ожидания
Общее время, которое ApplicationHost добавляет к выполнению запросов, уменьшилось на 11%:
CPU
Общее использование ЦП снизилось примерно на 27%, и разница особенно очевидна, когда мы смотрим на суммарное процессорное время, занимаемое плагинами, не относящимися к I/O, в запросе BingFirstPageResults:
Сбор мусора (GC)
Измерить точное влияние GC в разных сборках с помощью счетчиков довольно сложно. Помимо того, что значительно изменилась имплементация GC, также мы применили новую конфигурацию в .NET Core.
Счетчик % Time в GC увеличился:
Он увеличился в среднем с 0,3% до 0,8%, что много в относительном выражении, но незначительно в абсолютном.
Одним из объяснений этого является изменение, которое мы сделали после долгих измерений, проконсультировавшись с командой CLR, чтобы уменьшить размер бюджета Gen0. Это привело к тому, что GC стали выполняться быстрее и чаще. Что напрямую способствовало снижению общей задержки запроса P99, но ценой большего времени, проведенного в GC. В этой области ведутся активные исследования, чтобы выяснить, можем ли мы и в дальнейшем улучшать ситуацию, к тому же есть некоторые перспективные наработки в других конфигурациях.
Исключения
Средняя частота исключений значительно снизилась:
В среднем она снизилась с 7,8/сек до 3,5/сек, что на 55% лучше.
Конфликт при блокировке
Управляемая блокировка показала значительное снижение пиковых значений, но при этом немного повысила базовые уровни:
В среднем он снизился с 645 до 410 контентов/сек, что на 36% лучше. Это существенное улучшение связано с тем, что .NET Core изменил алгоритм блокировок, переходя в состояние ожидания быстрее, чем .NET Framework (который работал некоторое время). Таким образом, значительная часть контента в .NET Core могла вообще не учитываться в .NET Framework.
Время запуска
Сокращение времени запуска важно, поскольку это означает, что во время развертывания (несколько раз в день) каждый центр обработки данных простаивает меньше, а это приводит к снижению пиковых задержек и повышению доступности при пиковой нагрузке.
Время запуска значительно сократилось:
Время запуска в основном определяется временем прогрева, когда мы прогоняем через систему непользовательские запросы, прежде чем принять реальный трафик. Время прогрева определяется временем работы процессора. Снижение на 28% сильно коррелирует со снижением общего использования ЦП плагином.
ThreadPool
Одним из способов измерения эффективности является то, как долго плагины, готовые к выполнению, остаются в очереди ожидания свободного процессора. Эта метрика также определяется использованием ЦП, поэтому она не является чистым показателем эффективности пула потоков.
Средняя задержка при постановке в очередь P95 снизилась примерно на 31%. Средняя задержка при постановке в очередь P99 снизилась примерно на 26%.
Метод миграции
Показатели производительности - это прекрасно, но как насчет работы, которая была проделана для их достижения? Стоит подчеркнуть цели нашей миграции:
-
Гибкость в выборе, загружать ли компьютер под .NET Framework или .NET Core. Это позволит нам:
Динамически переключаться туда и обратно на основе инфраструктурных концепций, таких как функция компьютера, единица масштабирования, среда или раздел данных.
Выполнять откат в процессе разработки даже через несколько месяцев после миграции.
Вносить изменения в наш репозиторий, не вмешиваясь в работу других. Благодаря тому, что код работал в обоих режимах runtimes, никому не нужно было делать ничего особенного - мы тестировали оба режима.
Единая кодовая база
Единая копия бинарных файлов
Избежали необходимости с самого начала переводить всех наших партнеров на целевой netcore/netstandard. Это было бы невозможно.
Руководствуясь этими принципами, мы применили гибридный подход к созданию и эксплуатации платформы XAP:
Продолжали создавать платформу под .NET Framework 4.7.2.
Использовали инструмент ApiPort для проверки совместимости на уровне API. Это гарантировало, что все вызовы библиотек .NET, которые мы делали в нашей платформе, существовали и в .NET Core.
Разработали пользовательское хост-приложение на базе CoreCLR, которое динамически загружало и выполняло двоичные файлы, созданные на основе Framework.
Такой подход помог упростить аспекты тестирования и развертывания этого проекта и позволил всем нашим партнерам продолжить разработку своих сценариев в прежнем виде, не перенося все сразу на новую платформу.
Со временем мы переведем все наши двоичные файлы на сборку непосредственно для netstandard2.0 и .NET 5 или .NET 6 и больше не будем использовать гибридный подход.
Сложности
Как только мы начали эту работу, то поняли, что проблемы, с которыми придется столкнуться, будут многочисленными и отличаться по типу и масштабу от тех, что возникали у других команд.
Бинарная проблема
Довольно много сборок, которые использует XAP, необходимы для обеспечения запуска службы или обработки всего одного запроса. Без устранения всех зависимостей невозможно будет начать даже тестирование. Таких зависимостей десятки. Это проблема типа "все или ничего".
C++/CLI
Когда мы начали процесс миграции, .NET Core был в версии 2.0, которая не поддерживала C++/CLI. Эти DLL даже не загружались. Bing использует ряд общих компонентов инфраструктуры, которые передаются в управляемый код через интерфейсы C++/CLI.
Без этих сборок процесс не мог даже загрузиться, не говоря уже об обработке запросов.
С помощью и в сотрудничестве с командами по всей компании мы перевели их на использование интерфейсов P/Invoke.
Даже с учетом поддержки в последней версии .NET Core проектов на C++/CLI, эти сборки пришлось бы переделывать. Так как мы уже перешли на новые интерфейсы, в этом нет необходимости.
Несовместимый код
XAP runtime состоит в основном из процесса хостинга под названием ApplicationHost, в то время как предоставляемые партнерам библиотеки выполняются на нашей платформе.
Мы полагались на многие управляемые библиотеки, небольшие части которых использовали API, отсутствующие в .NET Core.
Примеры этого включают:
Кэширование в памяти, использующее библиотеку .NET MemoryCache
Библиотека хэш-вычислений
.NET remoting (WCF)
Функциональность HTTP
Загрузка пользовательских сборок
Каждый случай требовал отдельного решения. Иногда в .NET существовали альтернативные API, на которые можно было перейти. В-остальном, нам нужно было произвести апгрейд библиотек. Часто это приводило к длинной цепочке модернизированных зависимостей.
Сотни плагинов наших партнеров также использовали различные API, которых не было в .NET Core. Количество нерабочих API было относительно небольшим - возможно, около дюжины. Но количество плагинов и команд партнеров, которые владели этими плагинами, было огромным. Мы работаем с более чем 800 разработчиками в мире, и для команды XAP было непросто общаться со всеми ними.
Поэтому мы разработали автоматизированные инструменты для сканирования всех плагинов с помощью инструмента ApiPort команды .NET. Они были включены в процессы автоматического развертывания, через которые должны пройти все авторы плагинов, сначала в виде предупреждения, а затем в виде блокирующей ошибки. Была составлена документация о наиболее распространенных несовместимостях и рекомендуемых изменениях для приведения их в соответствие. Плагины, от которых отказались их разработчики, были навсегда отключены.
Ошибки .NET и функциональные изменения
Команда XAP столкнулась с множеством проблем из-за некоторых областей узкоспециализированной функциональности, которую мы создали. Иногда мы опирались на неутвержденные предположения, и тогда внутренние функциональные изменения в .NET вызывали поведенческие отклонения, которые нам нужно было устранить. В других случаях наши экстремальные уровни масштабирования и уникальная архитектура выявляли ошибки, которые другие тесты еще не обнаружили.
Загрузчик
XAP полагается на некоторые очень специфические функциональные возможности при использовании сборок наших партнеров для обеспечения их параллельной загрузки. Это фундаментальная часть нашей модели изоляции. Некоторые тонкие различия в загрузчике .NET потребовали от нас детального расследования, и в итоге мы обратились к разработчикам CLR, которые обнаружили как минимум две ошибки (dotnet/runtime #12072 и dotnet/runtime #11895), одна из них - критическое нарушение переполнения стека. Нам также пришлось внести некоторые изменения в наш код.
Кроме того, благодаря нашей диагностике и расследованию они исправили ошибку выполнения сборки, которая оказывала на нас влияние.
Наша уникальная архитектура и масштаб позволили выявить эти незаметные ошибки намного раньше, чем их обнаружили бы другие.
ETW и TraceEvent
Еще одна серьезная проблема была связана с библиотекой Microsoft.Diagnostics.Tracing.TraceEvent. Когда ApplicationHost только создавался и мы выбрали ETW в качестве механизма протоколирования, технологии ETW еще не было в .NET Framework, а версии с открытым исходным кодом находились на ранней стадии. Мы форкнули код для EventSource и TraceEvent в наши собственные версии, и они постепенно менялись в течение последующих десяти лет. По разным причинам нам было трудно и дорого переходить на официальные версии, прежде чем они стали более доступными.
Наконец, мы перешли на официальный пакет, обнаружив по пути пару небольших ошибок.
Еще одна пара интересных ошибок, с которыми мы столкнулись и которые были исправлены в .NET 5, была связана с поставщиком событий CLR. Когда наш код регистратора перелистывает файлы, ему необходимо отписаться от событий, которые он прослушивает, включая события CLR. Когда регистратор повторно подписывался на события для новых файлов, он больше не получал события CLR. Даже внешние инструменты, такие как PerfView, на которые мы полагаемся для отладки и профилирования производительности, больше не могли получать эти события CLR. Эта ошибка была сложной в основном потому, что ее было очень трудно воспроизвести, а наша модель использования, вероятно, была немного необычной.
Счетчики производительности
XAP очень сфокусирован на метриках. Мы собираем 6 миллиардов событий/мин по 500 миллионам временных рядов. Большинство из них - наши собственные, но мы полагаемся на многие системные счетчики и счетчики уровня .NET. В .NET Core 2.x эта инфраструктура отсутствовала, и поэтому мы оказались слепы во многих областях.
Начиная с версии .NET Core 3.x, некоторые счетчики производительности стали доступны. Мы попросили добавить еще несколько. См. здесь, здесь и здесь. Многие из них были добавлены в .NET 5.
Ошибка обработки исключений
Хотя это не совсем ошибка, связанная с миграцией, она была связана со многими другими проблемами при исследовании производительности, и мы решали ее в тандеме с другими исследованиями производительности CLR.
В течение довольно долгого времени XAP замечал длительные паузы в необычных местах при выполнении запросов - там, где для этого не существовало очевидных причин. С помощью команды CLR мы собрали трассировки и обнаружили, что пауза потока блокировалась механизмом обработки исключений, выполняющим дисковый ввод-вывод в некоторых очень специфических граничных случаях. Это была известная ошибка, но любопытно, что все релевантные стеки были от одного плагина, который выдавал исключения (и перехватывал их) почти при каждом запросе. С помощью событий ETW мы увидели, что это был самый большой источник исключений в процессе, и попросили владельца кода сделать проверку перед вызовом API, вызывающего исключения (это было тривиальным исправлением).
Как только команда партнеров была задействована для решения данной проблемы, загадочные паузы в потоках были устранены, и самый значительный источник наших запросов с большой задержкой был ликвидирован. Команда CLR с тех пор исправила ошибку. Этот график дает представление о влиянии этого исправления, показывая изменение процента запросов с очень высокой задержкой:
Изменения в GC
Большое изменение, которое помогло нам, касалось способа декоммитирования памяти. Это изменение переместило процесс декоммита из паузы GC для серверного GC и внесло ряд других изменений с целью улучшения производительности.
Стек HTTP
Одна из самых сложных областей перехода была в клиентском стеке HTTP. В целом по миру наш HTTP-плагин вызывается партнерами более 7,5 миллионов раз в секунду. .NET Framework и .NET Core имеют очень разные стеки HTTP-клиентов. Учитывая, что одной из важных целей было создание единой кодовой базы и возможность перехода между Framework и Core с помощью перезапуска процесса, предстояло решить очень важную проблему.
В .NET Framework мы полагались на функциональность ServicePointManager
и WebRequestHandler
, которых нет в .NET Core. Сначала было рекомендовано перейти на WinHttpHandler
, но в этом классе отсутствуют многие функции, на которые мы рассчитывали. Переход на него обернулся проблемами с производительностью, поскольку некоторые бэкенд-команды в Bing реализовали балансировку нагрузки, которую невозможно было достичь с помощью WinHttpHandler
. В итоге оказалось, что SocketsHttpHandler
доступен только для .NET Core.
Вкратце нашу ситуацию можно отразить в следующей таблице:
Мы не хотели переходить на WinHttpHandler
и оказаться при этом в не самой лучшей ситуации. В итоге был создан интерфейс HTTP, который динамически загружал WebRequestHandler
, либо SocketsHttpHandler
, в зависимости от используемой среды выполнения. Оба эти класса обладают схожей функциональностью, но не имеют общего интерфейса, поэтому был разработан свой собственный.
Кроме того, в .NET Framework некоторые параметры TCP устанавливаются в рамках всего процесса через ServicePointManager, а в .NET Core они устанавливаются на объекте SocketsHttpHandler
. Это означало использование большего количества условного кода в приложении.
Масштабирование инженерных усилий
Из-за бинарной природы многих частей этого масштабного проекта было легко полностью заблокировать его, особенно на ранних стадиях. Например, пока ожидалось перемещение библиотек C++/CLI в P/Invoke, приходилось работать только над теми вещами, которые, как мы знали, должны быть изменены. Не было возможности реально запустить ApplicationHost и протестировать его или посмотреть, что еще не работает. Таких проблем было много. Конечно, одновременно велась и другая работа, например, создание автоматизированных систем для анализа кода наших партнеров.
Заключение
В целом, в сценарии XAP .NET 5 демонстрирует значительное улучшение производительности по сравнению с .NET Framework . Несмотря на то, что предстоит еще много работы, общая картина ясна: .NET 5 феноменально превосходит .NET Framework по большинству параметров.
Мы надеемся, что уроки, извлеченные из миграционного опыта XAP, помогут другим командам в Microsoft и отрасли в целом при рассмотрении вопроса о миграции на .NET 5 и далее.
Все коды на изображениях для копирования доступны здесь.
В преддверии старта курса C# Developer. Professional приглашаем всех желающих на бесплатный демоурок в рамках которого мы рассмотрим полезные нововведения в C# 9.0 и .NET 5.0, разберем на примерах как они работают, обсудим насколько они улучшают код, а также их применимость в production проектах.
Материал переведен. Ссылка на оригинальную статью