Как стать автором
Обновить
Dodo Engineering
О том, как разработчики строят IT в Dodo

История о том, как мы монолит с .NET Framework на .NET 6 и Kubernetes переводили

Время на прочтение10 мин
Количество просмотров9.8K

В 2011 году 2 разработчика начали создавать свою информационную систему, чтобы через неё принимать заказы в Додо Пицце. 2 года назад мы  рассказывали про раннюю архитектуру Dodo IS здесь и здесь. За это время монолит нашей системы пережил немало изменений, самое значительное произошло в этом году — мы перевели его весь на .NET 6 и переехали в Kubernetes. Переход оказался непростой задачей и длился в общей сложности год.

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

Монолит, что с тобой не так?

Сам по себе монолит — это вполне нормальный паттерн проектирования, и некоторые компании прекрасно с ним живут. Но в нашем случае он превратился в серьёзную архитектурную проблему, которая мешала командам разработки приносить бизнесу пользу.

Начинался проект на .NET Framework 4.0,  стал огромным — в какой-то момент в нём было 2 миллиона строк кода. Распиливать монолит мы начали в 2016 году, но процесс шёл медленно. Сейчас в нём 600 тысяч строк кода на С#, 200 проектов, из них 16 запускаемых. В начале 2021 года монолит всё ещё был на .NET Framework и запускался на Windows Server, и это вызывало кучу проблем.

Неудобная разработка

Для разработки монолита нужен был Windows. Но не все разработчики хотели работать на Windows — в основном они используют Mac, есть и те, кто работает на Linux. Для Mac приходилось использовать Parallels Desktop, а это дополнительные траты и неудобство.

Вдобавок, чтобы начать разработку монолита на свежем компьютере, приходилось тратить почти целый день на настройку окружения: устанавливать кучу разных SDK, Visual Studio определённой версии, IIS Express и много чего ещё. Завести монолит локально превращалось в целую историю. Добавляла проблем и собственная система конфигурации с генерацией XML-файлов (об этом будет дальше).

Долгая и дорогая сборка

Для сборки был Cake-скрипт на 500 строк, который быстро работал только на 16-32 ядерных Windows-серверах. При этом каждая сборка монолита занимала 15 минут, т.к. надо было готовить собственные специальные артефакты. Все шаги сборки шли последовательно, всё это было ещё и на TeamCity, для которого образы билд-агентов приходилось готовить самим.

Трудно тестировать

Интеграционные тесты монолита «по историческим причинам» были написаны на .NET Core и находились в отдельном репозитории. Чтобы их запустить, требовалось полноценное развёрнутое окружение. А запуск интеграционных тестов — это 6 билдов в TeamCity. В общем, процесс трудно поддерживать, тесты трудно отлаживать, трудно новые писать, ёще и делать это синхронно в двух репозиториях.

Медленные деплои

Мы используем собственную систему деплоев из-за особенностей кода и развёртывания. Вернее, из-за того, что однажды мы совершили грубейшую и страшнейшую ошибку.

В 2014 году открывалась Додо Пицца в Румынии. И вместо того, чтобы в код и в БД монолита ввести понятие «страна» и обслуживать Россию и Румынию на одних серверах, мы просто сделали копию Dodo IS для новой страны. В результате получили две параллельно работающие Dodo IS. И потом сделали так ещё 14 раз.

Поэтому сейчас у нас монолит расшардирован 16 раз — по количеству стран. И мы запускаем не 16 приложений, а 256 (на самом деле больше, т.к. нужны реплики). Все новые сервисы делаем работоспособными сразу же с несколькими странами (мы это называем country-agnostic).

Не совершайте похожие ошибки. Есть отличный документ от AWS о том, как делать multi-tenant системы.

До 2017 года деплой делался на bat-скриптах и копировании сайтов через Samba. Процесс ручной, долгий и не бесшовный — не было выведения из upstream-балансировщиков. Мы решили его автоматизировать.

Для начала решили узнать, как делают другие. Но статей про сайты на .NET Framework и похожей спецификой особо не было — многие выкладывали просто через WebDeploy или у них была другая нагрузка. В итоге за основу нового деплоя мы взяли процесс, описанный в статьях сервиса StackOverflow, потому что у нас был похожий стек. Всё, что нам нужно было сделать, — это повторить их сборку и деплой, добавив только усложнение с конфигурацией и странами.

Как устроена наша система конфигов

Раньше система конфигов была такая: в каждом из запускаемых проектов был свой файл конфигурации и к нему шла пара десятков файлов трансформации. Разработчик выбирал в Visual Studio профиль и по нему средствами MSBuild при сборке происходили трансформации.

Проблема такой системы в том, что мы, во-первых, не могли получить итоговые конфиги, кроме как билдить каждый раз весь проект. Во-вторых, было очень много дублирования, т.к. в appSettings напихали кучу всяких констант и даже настроек сервиса.

Хотелось делать 1 билд и получать все целевые конфигурации (их в 2016 было уже более 50), чтобы отделить билд от деплоя, а также убрать дублирование. Там и вспомнили про XSLT, написали небольшую тулзу, убрали файлы трансформации из проектов. Всё это заняло около 2-х месяцев работы одного инженера.

Деплой происходит так: заходим на сервер через PowerShell Remoting, настраиваем там IIS (например, создаём сайты), выводим IIS из балансировщика, обновляем сайты, вводим IIS обратно в балансировщик.

Так у нас получился собственный «недоKubernetes»

  • Сам процесс деплоя был автоматический, но настройка и обслуживание виртуалок были ручными: например, чтобы ввести новый балансировщик или сервер, требовалась масса времени.

  • Сделать на этой системе autoscaling, auto healing или ещё какие-то базовые фичи Kubernetes было очень трудно. Да и зачем, если уже есть Kubernetes — он у нас появился в 2018 году для .NET Core и всех остальных сервисов.

  • Production-окружение находилось на 11 серверах. Сервера были pets, а не cattle — мы знали их по именам и они выполняли только одну роль.

  • Виртуальная машина на Windows Server стоит в облаке как минимум в 2 раза дороже, чем на Linux. Причина стоимости понятна, ведь Windows Server — огромный комбайн с разными возможностями, Active Directory, Storage Spaces и т.д. Однако на наших веб-серверах работает только, собственно, веб-сервер и ничего больше, то есть мы платим ни за что. Никакой другой инфраструктуры на Windows Server у нас нет.

  • Команда SRE тратила время на поддержку двух инфраструктур: на Windows Server VM и на Kubernetes. Например, чтобы обновить подсистему логирования, её пришлось делать два раза: на K8s, что заняло в сумме неделю, и на Windows Server, что заняло два месяца. 

3 попытки всё исправить

Kubernetes for Windows

Работало, но трудно было представить инвестиции, которые придётся сделать, чтобы K8s for Windows довести до паритета с K8s for Linux в нашей среде — логи, метрики, сборка, и т.д. К тому же K8s for Windows не окончательно готов: например, в Azure Kubernetes Service в 2020 году ещё не было Windows Server 2022, а образы Windows Server 2019 весили по 10 гигабайт.

Mono

Не работало из коробки, но можно было завести. Поскольку качество кода нашего монолита оставляло желать лучшего, на Mono встретились веселые баги: например, в некоторых собранных сайтах не хватало сборок. Похоже, что на Windows они забирались из GAC, а Mono так не умел.

Полный распил монолита

Рассматривали и такой вариант, ведь если полностью распилить монолит, проблем с ним не будет. Но сроки полного распила мы оцениваем в 2-3 года, причём с выделением на это ещё одной-двух целевых команд, потому что один сервис с полным реинжинирингом распиливается за 9-12 месяцев, а их 16. Таким образом, распил выглядит скорее долгосрочной стратегией, краткосрочно от него не получить пользы.

Видит СТО, мы старались…
Видит СТО, мы старались…

Более того, чтобы распилить монолит, в нём надо разрабатывать. А если в монолите медленная скорость разработки, то и распиливать его долго. Такой вот замкнутый круг.


В итоге мы посчитали, что если перейдём на .NET 6 и Kubernetes, то сэкономим около 10% стоимости нашей инфраструктуры в месяц только на серверах, не считая opportunity cost ускоренной разработки и деплоя. Сократим расходы за счёт лицензий на Windows и за счёт повышения утилизации серверов, например, autoscaling.

Перевод монолита на .NET 6

Проект стартовал в мае 2021 года. К этому моменту 4 из 16 сервисов уже были на .NET Core 2.1-3.1. Большая часть библиотек тоже была на .net standard 2.0. Соответственно, предстояло обновить 12 сервисов разного размера — в каких-то было по паре контроллеров, а в нескольких было по 300 Razor Views и контроллеров.

Список сервисов в монолите для понимания масштаба
  1. AlertServer.Web — сервис уведомлений пиццерий, в процессе проекта был выпилен из монолита.

  2. Admin.Web — b2b админка.

  3. DeveloperDashboard.Web — внутренняя админка для разработчиков.

  4. Api — API для интеграций.

  5. ExportService — API для интеграций с 1C.

  6. Auth.Web — сервис аутентификации и авторизации.

  7. OfficeManager.Web — интерфейс менеджера офиса пиццерий.

  8. CallCenter.Api — API колл-центра.

  9. CallCenter.Web — интерфейс колл-центра.

  10. PrivateSite — личный кабинет сотрудника.

  11. RestaurantCashier.Web — касса ресторана.

  12. CashHardware.Web — сервис кассового оборудования, отвечает за печать фискальных чеков.

  13. ShiftManager.Web — интерфейс менеджера смены.

  14. ClientSite.LegacyFacade — внутреннее API монолита.

  15. Communications — сервис отправки СМС и имейлов.

  16. Bus.Jobs — консьюмеры монолита.

  17. Scheduler — сервис джоб, запускаемых по расписанию.

Хронология перехода

Это я, пытаюсь в одно лицо перевести монолит.
Это я, пытаюсь в одно лицо перевести монолит.

Начали с того, что установили .NET 5 на Windows виртуальные машины, агенты, настроили инфраструктуру стенда, почистили репозиторий и проекты. Переключились на другие задачи, сходили в отпуск.

За лето перевели AlertServer.Web. В процессе обнаружили, что в солюшене очень большая связаность, из-за которой сложно изолированно что-то обновлять, packages.config затрудняет обновление и рестор пакетов, почти невозможно пользоваться райдером.

Дело в том, что проекты на packages.config не поддерживают транзитивные ссылки на пакеты, выкачивают все свои пакеты в ./packages директорию и райдер (и VS) нещадно тормозят при любой попытке что-то сделать. При переводе csproj на PackageReference пакеты обновляются мгновенно, и ресторятся в кеш, а оттуда подтягиваются ссылками в project.assets.json.

В итоге вместе с основным проектом пришлось перевести половину репозитория и библиотек на .net standard 2.0 и PackageReference. Дальше дело пошло быстрее, т.к. основная часть перевода — это обновление пакетов.

С конца августа и до ноября переводили Auth,  LegacyFacade & Consumers, отвлекались на тесты, зелёный пайплайн, перенос приватных NuGet пакетов на GitHub Packages (с myget.org).

В ноябре к нам присоединились 2 новых человека, бэкендеров на проекте стало трое. Стартанули и закончили перевод сразу двух сервисов — CallCenter.Api и Api, перенесли автотесты на .NET 5, принялись за CallCenter.Web.

Декабрь — январь: взялись за Shiftmanager.Web на мега-системе с проксированием на основе YARP, закончили с ним и с CallCenter.Web.

К концу февраля обновили весь солюшен на .NET 6.0. Тогда же подключили ещё 4 разных команды к переводу оставшихся четырёх сервисов: Admin.Web, CashHardware.Web, OfficeManager.Web, RestaurantCashier.Web. В этот момент проектом занимались уже 15 человек. При этом ребята, которые присоединились к переводу на старте, выступали уже в качестве консультантов для «новичков».

В марте занимались в основном мелкими «доделками» и приступили к переезду в Kubernetes:

  • подготовили сборку, тестирование на Linux & GitHub Actions;

  • сделали приложения кросс-платформенными;

  • убрали System.Drawing;

  • везде пофиксили пути (как минимум сепараторы с \ на /), кракозябры(html encode).

В мае начали деплоить монолит  в Kubernetes, перевели первые тестовые стенды и canary-продакшены с низкой нагрузкой.

При нагрузочном тестировании обнаружили море косяков, в основном в sync over async коде. Как фиксили:

Обнаружили проблему с graceful shutdown: если его не сделать, поды будут убиваться во время того, пока на них идут запросы, и клиенты будут видеть ошибки. Для корректной работы в Kubernetes приложение должно правильно обрабатывать внешние сигналы об остановке (SIGTERM). Как настроить обработку SIGTERM описано тут.

И, наконец, в июне полностью вся Dodo IS заработала в Kubernetes!

Пример чеклиста для апгрейда сервиса с .NET Framework 4.8 на .NET 6

  • Начинаем или с .NET upgrade assistant, или с convert packages.json to nuget references (инструмент в Rider/Visual Studio).

  • Обновляем csproj файл:

    • обновляем заголовок файла на Project Sdk="Microsoft.NET.Sdk.Web";

    • обновляем версию фреймворка (TargetFramework) до NET6.0;

    • удаляем из проекта все неизвестные науке targets;

  • Удаляем все левые PropertyGroup, кроме необходимых.

  • Удаляем все itemgroup, относящиеся к включению в проект cs и других файлов.

  • Удаляем импорт csharp.targets

  • Удаляем все пакеты System.* Microsoft.*

  • Пытаемся удалить все пакеты, которые нужны только как зависимости.

  • Если есть бинарная сериализация, её нужно явно включить EnableUnsafeBinarySerialization 

  • Обновляем Startup:

    • удаляем Global.asax

    • удаляем ненужные зависимости

    • удаляем owin

    • используем .NET 6 хост

    • используем UseForwardedHeaders в стартапе.

  • Переносим конфигурацию на Microsoft.Extensions.Configuration.

  • Обновляем логирование: переход на Microsoft.Extensions.Logging.

  • Обновляем метрики: либо последняя версия prometheus-net, либо OpenTelemetry.

  • По возможности отказываемся от Autofac в пользу Microsoft.Extensions.DependencyInjection.

  • Внимательно относимся к сервисам, которые стартуют: переделываем на HostedService.

  • Обновляем Swagger на ASP.NET Core версию.

  • Обновляем SignalR на ASP.NET Core версию.

  • Обновляем MassTransit на последнюю версию.

  • Обновляем все middleware.

  • Если во вьюхах попадается HtmlString, меняем его на IHtmlContent.

  • Обновляем аутентификацию и авторизацию:

    • анализируем использование Session и SystemUser (нашу собственную абстракцию над аутентифицированным пользователем);

    • не забываем, что Session в ASP.NET Core по умолчанию синхронная. Надо вызывать .LoadAsync() перед любой работой, чтобы она стала асинхронной.

  • Обновляем контроллеры:

    • перепиливаем сигнатуры методов контроллеров, чтобы везде возвращалось ActionResult, например:

      • было
        public PartialViewResult PartialIndex()

      • стало
        public IActionResult PartialIndex()

    • проверяем сериализацию: либо оставляем Newtonsoft.JSON, либо везде с него съезжаем на system.text.json;

    • в JSON-сериализации изменились дефолты casing, учитываем это (JsonSerializerOptions.PropertyNamingPolicy = null).

  • Настраиваем роутинг, байндинг моделей.

  • Не забываем перевести тестовые проекты.

Всего проделали мы это 15 раз. Какие-то сервисы занимали одну неделю (если были на OWIN и в них было 5 контроллеров), какие-то два месяца.

Резюме и итоги

Задача по обновлению монолита — процесс долгий. Нам удалось относительно быстро это сделать за счёт того, что люди добавлялись в проект поэтапно. Мы начинали с одного разработчика в мае 2021, в ноябре добавили ещё двоих, часть сервисов переводили сами команды-владельцы. Однако в конце февраля мы решили ускориться и подключили ещё 4 команды к проекту, и внезапно всё доделали за три недели. Удалось это благодаря тому, что у нас уже была экспертиза по переводу, которую мы могли пошарить на 4 команды, работая в них приходящими экспертами.

Большая часть сложности заключалась в инфраструктурных библиотеках. Т.е. сам C# код работал плюс-минус так же, а вот инфраструктурные библиотеки и фреймворк работали абсолютно по-другому. Поэтому в самом простом случае всё обновление заключалось в том, чтобы обновить все внешние библиотеки: NLog, MassTransit и т.д., обновить Startup/Program.cs. Например, мы много раз хотели отказаться от Autofac в пользу Microsoft DI и откладывали это, потому что тогда пришлось бы потрогать буквально весь солюшен.

Мы перевели 16 сайтов на .NET6, потрогали  больше 400 Razor Views, выпилили несметное количество старого кода и библиотек, удалили больше 100 серверов, несколько доменов, кучу конфигураций из TeamCity.

Теперь у нас только одна инфраструктура для всех. Монолит можно разрабывать на всех платформах (Windows, Linux, MacOS), сборка занимает 6 минут, обновление на продакшене — 10 минут.  Для .NET 6 мы используем GitHub Actions, в котором есть hosted runners, за которые платишь посекундно и ничего не надо настраивать. В итоге экономили 10% месячных расходов.

Надеюсь, наш опыт поможет вам избежать ошибок, если придётся столкнуться с похожей задачей.

Теги:
Хабы:
+38
Комментарии19

Публикации

Информация

Сайт
dodo.dev
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия