Эта статья является логическим продолжением обновления проекта nopCommerce — бесплатной CMS с открытым исходным кодом для создания интернет-магазинов. В прошлый раз мы рассказали о нашем опыте миграции проекта с ASP.NET MVC на ASP.NET Core 2.2. Теперь мы рассмотрим процесс миграции на .NET Core 3.1. Учитывая, что официальная поддержка .Net Core 3.1 будет длиться до декабря 2022 года, сейчас тема миграции очень актуальна. Поэтому, если вы хотите получить все преимущества обновленного фреймворка, идти в ногу с технологическими новинками и соответствовать набирающим популярность общемировым трендам, то самое время заняться миграцией.
Какие задачи предстояло решить в процессе перехода на .NET Core 3.1
Для быстро развивающегося проекта в области электронной коммерции, крайне важно уделять большое внимание производительности системы и ее безопасности. Уже в первых review .NET Core 3.0 анонсировалось, что новая версия фреймворка будет в разы производительней, появится многоуровневая компиляция и как следствие уменьшение времени запуска, встроенная высокопроизводительная и не требовательная к памяти поддержка JSON. Маршрутизация конечной точки, которая появилась в версии .NET Core 2.2 была улучшена. Основное преимущество в том, что теперь маршрут определяется до запуска промежуточного программного обеспечения. На момент выхода релиза эти ожидания подтвердились на практике. Далее вы узнаете, что именно повлияло на производительность системы и как эволюционировал .NET Core с момента выхода версии 2.2.
Что нового дает переход на .NET Core 3.1
Давайте подробнее остановимся на тех нововведениях которые мы можем использовать с новой версией фреймворка. Подробную инструкцию по переходу от .NET Core 2.2 к .NET Core 3.1 вы можете найти на официальном сайте Microsoft. В этой статье мы рассмотрим преимущества, которые мы получаем благодаря использованию того или иного нововведения.
Generic Host
В .NET Core 2.1 Generic Host является неким дополнением к Web Host, это позволяет использовать такие инструменты, как внедрение зависимостей (DI) и протоколирование абстракций. В .NET Core 3.х был сделан акцент на большую совместимость с Generic Host, теперь вы можете использовать обновленный Generic Host Builder вместо Web Host Builder. Это дает возможность создавать любые приложения, начиная от консольных приложений и WPF и заканчивая веб-приложениями на одной базовой хостинговой парадигме с одинаковыми общими абстракциями.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseStartup<Startup>();
});
}
}
Вы все еще можете продолжать использовать WebHostBuilder, но необходимо понимать что некоторые типы устарели в ASP.NET Core 3.1, и могут быть заменены в следующей версии.
global.json
Сама идея и возможность использования этой фичи уже существовала в версии .NET Core 2.0, но ее возможности были неполными. Можно было только указать версию SDK, которую использует ваше приложение. Более гибкое управление версией пакета SDK стало доступным только в .NET Core 3.0 с появлением таких политик как allowPrerelease и rollForward. Ниже представлен наш вариант комбинации политики наката версий .NET Core SDK.
{
"sdk": {
"version": "3.1.201",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
Полную версию кода приложения вы можете посмотреть в нашем репозитории на GitHub.
Теперь вы можете указать версию, начиная с которой можно без проблем производить сборку приложения, без необходимости каждый раз вносить правки в global.json после выхода очередного патча. Тем самым определяя тот функциональный диапазон, который нужен исходя из ваших требований. Также это даст вам гарантию того, что пользователи вашего приложения будут запускать его именно на тех сборках SDK которые вы определили, а не последней версии, которая установлена на сервере.
ASP.NET Core Module V2
До .NET Core 2.2 IIS по умолчанию размещал приложение .NET Core, выполняя экземпляр Kestrel (встроенный веб-сервер .NET Core) и перенаправляя запросы из IIS в Kestrel. В основном IIS действовал как прокси. Это работает, но медленно, так как выполняется двойной переход от IIS к Kestrel для обработки запроса. Этот метод хостинга получил название «OutOfProcess».
В .NET Core 2.2 была представлена новая модель хостинга под названием «InProcess». Вместо того чтобы IIS пересылал запросы в Kestrel, он обслуживает запросы внутри IIS. Это намного быстрее при обработке запросов, потому что не нужно пересылать запрос в Kestrel. Однако это была необязательная функция, и не использовалась по умолчанию.
В .NET Core 3.1 внутрипроцессная модель размещения уже настроена по умолчанию, что значительно повышает пропускную способность запросов ASP.NET Core в IIS. При тестировании мы фиксировали двукратное увеличение производительности системы при большом количестве запросов.
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<Copyright>Copyright (c) Nop Solutions, Ltd</Copyright>
<Company>Nop Solutions, Ltd</Company>
<Authors>Nop Solutions, Ltd</Authors>
<Version>4.4.0.0</Version>
<Description>Nop.Web is also an MVC web application project, a presentation layer for public store and admin area.</Description>
<PackageLicenseUrl>https://www.nopcommerce.com/license</PackageLicenseUrl>
<PackageProjectUrl>https://www.nopcommerce.com/</PackageProjectUrl>
<RepositoryUrl>https://github.com/nopSolutions/nopCommerce</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<!--Set this parameter to true to get the dlls copied from the NuGet cache to the output of your project-->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<!--When true, compiles and emits the Razor assembly as part of publishing the project-->
<RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>
Хотя справедливости ради стоит отметить, что при запуске приложения на Linux все равно будет использоваться модель внепроцессного размещения веб-сервера.
И еще, если вы размещаете на своем сервере несколько приложений с внутрипроцессной моделью, вы должны позаботиться о том, чтобы каждое такое приложение имело свой собственный пул.
Маршрутизация Endpoint Routing
В .NET Core 2.1 маршрутизация выполнялась в Middleware (промежуточном программном обеспечении ASP.NET Core MVC) в конце конвейера обработки HTTP-запросов. Это означает, что информация о маршруте, например, какое действие контроллера будет выполнено, была недоступна промежуточному ПО, которое обработало запрос до промежуточного ПО MVC в конвейере запросов. Поэтому начиная с .NET Core 2.2 была введена новая система маршрутизации, основанная на конечных точках (Endpoints), призванная решить вышеупомянутые проблемы.
Теперь в .NET Core 3.1 система маршрутизации Endpoint Routing построена иначе, этап маршрутизации отделен от вызова конечной точки. По сути мы имеем два промежуточных middleware:
- EndpointRoutingMiddleware — тут определяется, какая конечная точка будет вызвана для каждого пути URL запроса, по сути выполняя роль маршрутизации
- EndpointMiddleware — вызывает конечную точку
Приложение определяет конечную точку, которая будет отправлена на раннем этапе, в конвейере промежуточного программного обеспечения, чтобы следующее middleware могло использовать эту информацию для предоставления функций, недоступных в текущей конфигурации конвейера.
/// <summary>
/// Configure Endpoints routing
/// </summary>
/// <param name="application">Builder for configuring an application's request pipeline</param>
public static void UseNopEndpoints(this IApplicationBuilder application)
{
//Add the EndpointRoutingMiddleware
application.UseRouting();
//Execute the endpoint selected by the routing middleware
application.UseEndpoints(endpoints =>
{
//register all routes
EngineContext.Current.Resolve<IRoutePublisher>().RegisterRoutes(endpoints);
});
}
Это дает определенную гибкость, поскольку теперь функциональность сопоставления и разрешения маршрутов отделена от функциональности диспетчеризации конечных точек.
Подробнее ознакомиться с кодом вы можете в нашем репозитории на GitHub.
С# 8.0 синтаксический сахар
Кроме обновления самого .NET Core так же была выпущена и новая версия C# 8.0. Обновлений очень много. Одни из них достаточно глобальны, другие затрагивают косметические улучшения, давая разработчикам так называемый “синтаксический сахар”.
Подробное описание вы можете найти в документации.
Замеры производительности в приложении
После того как мы обновили наше приложение nopCommerce, нам необходимо было проверить на сколько именно в нашем случае увеличилась производительность, т.к. для пользователей это один из самых важных критериев выбора eCommerce платформы.
Тесты мы проводили под управлением Windows 10 (10.0.19041.388), где IIS 10 (10.0.19041.1) выступал в качестве прокси-сервера для веб-сервера Kestrel на той же машине. Для моделирования нагрузки мы использовали Apache JMeter, который позволяет имитировать очень серьезную нагрузку со множеством параллельных запросов. Для теста мы специально подготовили среднестатистическую базу данных интернет магазина среднего бизнеса, где количество продуктов составляло около 50 тысяч наименований, количество категорий продуктов — 516 с произвольной вложенностью, количество зарегистрированных пользователей — порядка 50 тысяч, и количество заказов — около 80 тысяч с рандомным включением от 1 до 5 продуктов. Все это под управлением MS SQL Server 2017 (14.0.2014.14).
JMeter выполнял несколько нагрузочных тестов, генерируя каждый раз сводный отчет об измерении каждого из запросов. Усредненные результаты после нескольких прогонов представлены ниже.
Время прохождения теста стало быстрее примерно на 20% (меньше лучше)
Среднее время отклика (Average) быстрее примерно на 13.7% (меньше лучше)
Количество запросов на единицу времени, т.е. пропускная способность (throughput) увеличилось на 12.7% (больше лучше)
Также нас интересовало использование памяти, затраченной приложением после запуска, и после краткосрочного нагрузочного теста, который проводился несколькими сериями подряд. Нашей целью было узнать, насколько эффективно освобождается память в процессе работы приложения. И как повлияло обновление модуля ASP.NET Core с новой моделью размещения на стартовое потребление памяти.
Результаты представлены на диаграмме — модель внутрипроцессного размещения показывает более чем в 1,5 раза лучшую эффективность после прохождения первого цикла нагрузки. Далее наблюдается более эффективное использование ресурсов, и к концу третьего цикла нагрузочных тестов общее потребление памяти оказывается более чем в 2 раза эффективнее по сравнению с внепроцессным размещением. Это очень хороший результат.
Для сравнения мы приведем таблицу с результатами испытаний, в том числе и для AspNetCoreModule, который мы использовали с .NET Core 2.2. Новый модуль несомненно выигрывает у своего предшественника.
Стартовое потребление памяти практически не изменилось, показания изменяются в пределах погрешности измерений.
Итоги
Процесс миграции платформы .NET Core по-прежнему остается трудоемким и требующим времени мероприятием. И если делать обновления вовремя и последовательно, это позволит избежать огромного количества ошибок, а главное экономить время. Многие аспекты .NET Core были улучшены, бонусом вы получаете улучшенную производительность. В нашем конкретном случае мы фиксировали увеличение производительности в среднем на 13%. Это привело к тому, что запросы к приложению от наших клиентов выполняются быстрее, что позволяет отправлять больше данных за меньшее время, делая работу с приложением более комфортной. Поэтому это довольно существенная прибавка, учитывая, что вам фактически не требуется проводить performance refactoring вашего приложения: можно просто обновить фреймворк и получить общее повышение эффективности платформы.
Также немаловажным является тот факт, что, как и всегда, в .NET Core много внимания уделяется вопросам безопасности, к слову, с момента релиза .NET Core 3.1.0 уже успел выйти ряд обновлений (последняя версия на текущий момент 3.5.1) в том числе и обновлений безопасности — security patch.
Очень важным моментом является и то, что .NET Core 3.1 является очередной версией LTS после 2.1, что гарантирует нам и нашим клиентам поддержку и получение самых последних исправлений, в том числе и исправлений безопасности. Поэтому для нас важно двигаться вперед вместе с выходом LTS версий .NET Core. Следующим глобальным выпуском станет .NET 5, и перенос приложения на .NET Core 3.1 — это лучший способ подготовиться к этому.
В дальнейшем мы планируем и дальше обновлять наше приложение nopCommerce, добавляя все новые и новые возможности которые предоставляет платформа .NET Core. Одна из них это переход на использование System.Text.Json вместо Newtonsoft.Json. Это более производительный, безопасный и стандартизованный подход к обработке JSON объектов. Также в планах внедрить и использовать как можно больше фичей, которые предоставляет C# 8.0.
Узнать больше о нашем проекте вы можете на сайте nopcommerce.com или посетив наш репозиторий на GitHub.