Когда я начинал карьеру лет 15 назад, иногда делал сайты на PHP, Perl, RoR, ASP… Но из-за слишком частой смены стандартов в Web, я выгорел и "ушел" целиком в Backend + Desktop-разработку, где всё плюс-минус стабильно годами, если не десятилетиями.
Однако, в условиях растущего запроса на кроссплатформенность, все чаще задумываюсь о возвращении к Web, и так как я больше всего использую .NET, решил разобраться детальнее с Blazor, а точнее, с Interactive Server-Side Rendering.
Довольно быстро освоил как оно работает, но остался незакрытый вопрос — а что там с производительностью? Что если много людей одновременно будут жать кнопочки в системе? Какая вообще нагрузка на сервера?
Не найдя подробные разборы в открытых источниках, я за недельку по вечерам сделал игрушку для Telegram App, и в пятницу 26 июля 2024 собрал людей со своего и дружественных сообществ на исследование нагрузки и возможных инцидентов. Собранную информацию с деталями раскрываю в этой статье.
Контекст: Правила игры и архитектура решения
Для того, чтобы понимать контекст, необходимо погрузиться в разработанную игру и архитектуру решения.
Игра — «Угадайте число», многие ее знают под названием «Быки и Коровы». Суть исходной игры заключается в том, что один игрок загадывает число из уникальных цифр, а второй должен его угадать. В помощь угадывающему, на каждую его попытку, загадывающий должен называть сколько есть «Коров» (количество угаданных цифр не на своих местах) и «Быков» (количество угаданных цифр на своих местах) в названном числе.
Моя реализация этой игры отличается от оригинала тем, что число загадывает сервер, а угадывает его не один игрок, а все игроки вместе. Для простоты регистрации игра была адаптирована под запуск через Telegram Mini Apps, благодаря чему с минимальными затратами мне удалось получить готовые ID пользователей не затрачивая время и силы на создание своей системы регистрации.
Интерфейс игры представляет из себя клавиатуру из 9 цифр, 4 экранов для отображения введенного числа, 4 экранов для «лампочек» подсказки отгадывания, а также несколько экранов для статистической информации. При каждом нажатии кнопки «клавиатуры» происходит впечатывании цифры, а после ввода четырех цифр — происходит сверка угадываний и вывод результата сверки («коровы» — желтые лампочки, «быки» — зеленые).
Соревновательный момент состоит в том, что на угадывание числа отводится всего 120 секунд. Чем раньше игрок отгадает число, тем больше очков получит. Угадавший ждет завершения раунда до того, как сервер придумает новое число, а те кто не угадал до конца раунда — остаются без своих очков и начинают все с начала. Для удобства отслеживания времени, ежесекундно срабатывает таймер, отсчитывающий время назад, а при достижении нуля — запрашивает обновление статистики и сброс введенных ранее данных.
Все описанные выше действия были реализованы строго на C# и согласно технологии Server-Side Rendering должны обрабатываться на стороне сервера. А так как сравнение введенного числа с загаданным, обработка статистики, устранение конфликтов многопользовательской игры могут давать свою нагрузку на сервер — проект был разделен на две части, которые работали на разных серверах:
Часть / Сервер 1 (Front) — ASP.NET приложение с включенным Blazor Interactive Server-Side Render, которое обрабатывает только изменения интерфейса и отправляет запросы по API на второй сервер.
Часть / Сервер 2 (Back) — ASP.NET приложение с включенным Quartz для регулярного обновления загаданного числа, WebApi для обработки запросов от Blazor, а так же с PostgreSQL для сохранения статистической информации. При этом все данные об игре держатся в оперативной памяти, а для ускорения работы SELECT в БД не используется.
Первый взгляд на результаты
В игре за день приняло участие 37 разных игроков. Которые сыграли 445 раундов и сделали 7250 попыток отгадать число.
Игра была запущена в 8 утра и примерно до 11 утра количество одновременно играющих росло. При этом в мониторинге нагрузка на сервера выглядела довольно маленькой:
Сервер 1 - (Front / Blazor Server-Side Rendering)
Сервер 2 - (Back / Quartz + WebApi + PostgreSQL)
Далее почти весь день нагрузка была приблизительно на этом же уровне, за исключением двух всплесков, которые разберем далее:
Сервер 1 (Front / Blazor Server-Side Rendering)
Сервер 2 - (Back / Quartz + WebApi + PostgreSQL)
Разбор инцидента, произошедшего в обед
На обеденном перерыве мы с коллегами спокойно сидели, играли в игру, никого не трогали, как вдруг игра перестала предлагать нам новые числа на угадывание. Первое, о чем я подумал — упало бэкэнд приложение, которое обеспечивало работу вычислений.
С учетом того, что я не тратил много времени на разработку, то допускал такие падения с быстрым решением путем «Перезапустить приложение». Но каково было мое удивление, когда я вообще не смог попасть на сервер ни по RDP, ни по SSH. По итогу его пришлось перезагружать полностью, после чего бэк-приложение успешно запустилось и игра ожила.
На странице мониторинга эта ситуация на бэкенде выглядела следующим образом:
Рост нагрузки виден в двух местах:
1 — Возросшая сетевая активность до 100kBps, что на самом деле не является большим показателем, но выбивается из общей картины.
2 — Возросшая нагрузка на диски до 30 MBps, которая вызвала вопросы. Здесь можно предположить, что произошел спам со стороны фронта, но с учетом того, что попытка отгадывания отправляется с фронта только после ввода четырех цифр, а фиксируется она всего одним INSERT-ом, следовало бы ожидать более высокой сетевой нагрузки на сервер.
Но даже если гипотеза со спамом попыток верна, то значит в таблице с попытками пользователей должно быть много записей на момент начала зависания. Но в базе данных предпоследняя попытка перед зависанием была в 12:51:11, а последняя в 12:53:19 — не такая уж и серьёзная нагрузка, она была примерно такой почти весь день:
Следующим подозреваемым стал Quartz — а что если он сошел с ума и начал срабатывать не раз в 2 минуты, а чаще? Ведь при срабатывании Quartz происходит генерация нового числа, а с ним выполняется не только запись этого числа в базу, но и сбор+запись статистики за раунд + за час. Но и эта гипотеза оказалась ложной после анализа базы данных — предпоследний код был сгенерирован в 12:50:57, последний перед зависанием в 12:52:57 — строго через 2 минуты, а следующий раз был уже в 13:20:38, после перезагрузки:
Таким образом получается, что непосредственно ни игровой движок, ни сама СУБД, не являются причиной сбоя. Возможно, что-то пошло не так на уровне системы, либо инфраструктуры облачного провайдера. Ведь особенно странным является факт недоступности даже по SSH.
На этом можно было бы довольно хлопнуть в ладоши закрыв инцидент как псевдо-ложный, но вызвал вопрос график нагрузки на CPU и сеть фронта:
Здесь я увидел потенциально главный минус балл, который захотелось сразу поставить Блазору — нагрузка на CPU, но разбор ситуации заставил меня передумать.
Довольно быстро я сообразил, что в высоком потреблении процессора и сети виноват таймер обратного отсчета, который при достижении 0 секунд выполняет запрос актуальной статистики с бэка и только при ее получении возвращает отсчет на 120 секунд. Так как таймер срабатывает каждую секунду, то получается, что каждую секунду создается новый поток обработчика таймера, в котором отправляется запрос на бэк, который в свою очередь впадает в ожидание до таймаута и до этого момента поток потребляет процессорное время. А так как таймаут примерно минута, то за минуту копится порядка 60 потоков на игрока (открытую страницу с Blazor-приложением в браузере).
И здесь можно было бы опять хлопнуть в ладоши, сказав «Так вот в чем причина!», но возникает немой вопрос — «А почему таймер не перестает работать при условии, что игроки увидев нерабочую игру повыходили из игры? Почему сессии на Interactive SSR не погасились?» Ответ я найти не смог, но возможно знатоки Блазора знают как обрабатывать такие случаи, приглашаю их написать ответы в комментариях.
Разбор стресс-тестирования
По итогу дня я пришел к выводу, что графики выглядят через-чур красиво. И проведенный анализ базы данных показал, что за весь день не было такого случая, чтобы в одном раунде играло больше 4 человек. А это совсем никакой показатель для теста. Поэтому я кинул клич в нескольких сообществах и собрал 9 добровольцев, которые с 19:00 до 19:05 в игре просто тыкали кнопки в быстром ритме не пытаясь угадать число.
По итогу получилось где-то около 4500 запросов за 5 минут, или примерно 15 запросов в секунду. Конечно, это не уровень условного Яндекса, но и серверные мощности у меня не очень большие. И вот какой результат вышел на мониторинге:
Сервер 1 - (Front / Blazor Server-Side Rendering)
Сервер 2 - (Back / Quartz + WebApi + PostgreSQL)
Всплески нагрузки видны, но с учетом пиковой нагрузки на Front CPU 3.5% и 60kBps сети, кажется, можно признать Blazor жизнеспособным решением для продуктивной нагрузки. Но, конечно, всегда есть но.
Некоторые из игроков во время стресс-тестирования жаловались на одну и ту же проблему: Иногда не прожимаются кнопки. Это было видно, когда набивали их последовательно, например нажимали 1 2 3 4, а вводилось только 1 2 4 (3 как будто бы не нажималась).
Как показало общение – у всех это происходило не одновременно,. Сам я повторил проблему и позже стресс-тестирования при меньшей нагрузке на сервер. Из этого я делаю вывод – Blazor Interactive SSR (а скорее соединение SignalR) не может держать столь большое количество обращений между сервером и одним и тем же клиентом, в результате чего пропускает часть.
На сколько является это проблемой для применения — вопрос открытый для ваших систем. Что будет, если пользователь попадется нервный и будет много быстро кликать, а какая-нибудь галочка не нажмется, из-за чего нажатая следом кнопка отработает не так?
Помимо спама командами, я нашел так же еще один негативный момент. Когда ехал в электричке, экран игры вдруг стал серым и появилась анимация загрузки:
Как выяснилось, это нормальное поведение при потере соединения SignalR с сервером Blazor. С одной стороны, это правильно — если нет соединения, значит команды не отработают, и пользователь должен знать об этом. С другой стороны, а что если пользователь хочет методично почитать информацию со страницы? Ему будет мешать эта подложка загрузки? Возможно, профессионалы Blazor знают как решать подобные проблемы, так же приглашаю написать в комментарии.
Подведем итоги исследования
По итогу исследования я решил собрать вместе плюсы и минусы, чтобы другие специалисты, принимающие решение о выборе технологии на своих проектах, могли принять взвешенное решение о применимости:
Плюсы:
Легкий вход в технологию разработчика, который знает C# /.NET. Это означает, что к разработке фронт‑энда можно подключать разработчиков бэк‑энда, которые могут не разбираться в тонкостях языков JS/TS и архитектуре фреймворков типа AngularJS, Vue.js. А на тех проектах, где не требуется реализация сверх-красивого интерфейса, скорее всего можно вообще обойтись без фронт-специалистов (для небольших интеграторов это может означать хорошую потенциальную финансовую выгоду);
Решение проблемы передачи сложных объектов между браузером и сервером. В прошлом я немало провозился с передачей объектов с десятком полей между бэком на C#/Java и браузерным JS/TS, и прекрасно понимаю эту боль. То, что Blazor позволяет получить по API сразу нативный C#-класс и сразу же его отобразить в данных на веб-странице, кажется весьма весомой фичей.;
Минусы:
Нет возможности сделать микс из Interactive Server-Side Rendering и Client-Side Rendering (WebAssembly). Как показала практика игры — целый ряд действий (например, отсчет таймера, просто вычитание единицы из int переменной timeToNextRound) через рендер на сервере является крайне нездоровой историей. Но, с другой стороны, включение всего рендера на стороне клиента обременяет отправкой нескольких десятков мегабайт Runtime в браузер клиента, что тоже плохая идея (и не факт что будет нормально работать на слабых устройствах). Поэтому кажется, что не хватает какой-то промежуточной истории. Будет здорово, если Microsoft сделает некий транслятор C# в JS для методов с простыми вычислениями и изменениями примитивов. Но, пока этого нет – истории с постоянным изменением данных, которые по факту не зависят от бэк-сервера, выглядят убийственными не только для производительности фронт-сервера, но и для сети;
Непрозрачная отладка SignalR. Периодически во время разработки у меня происходил какой-то сбой и нажатые кнопки в браузере вроде отрабатывали, но вот точка останова в Visual Studio не срабатывала. Почему – непонятно. В браузерных средствах разработчика на панели «Сеть» никаких данных про отправку запросов и получение ответов тоже не было. В целом спасала повторная сборка / перезапуск проекта, но какое-то время у меня это съело, что однозначно записываю как негатив.
Потеря вызовов при срабатывании обработчиков чаще 2-3 раз в секунду. Хоть эта история возникает только в тех случаях, когда пользователь истерично жмет по кнопкам, которые имеют под собой обработчики, и в реальности, за исключением игр, такое вряд ли будет происходить, этот момент однозначно стоит держать в голове.
Лично для себя я сделал вывод, что несмотря на все озвученные минусы, у технологии есть свое место в корпоративном мире / в закрытых контурах и я однозначно буду агитировать использовать Blazor большинству команд, которые пишут на C#. А какие выводы делаете вы? Поделитесь в комментариях!