Масштабирование игры! до тысяч одновременных запросов
Опубликовано: 2022-03-11Веб-разработчики Scala часто не учитывают последствия одновременного доступа тысяч пользователей к нашим приложениям. Возможно, это потому, что мы любим быстро создавать прототипы; возможно, это потому, что тестировать такие сценарии просто сложно .
Тем не менее, я собираюсь доказать, что игнорирование масштабируемости не так плохо, как кажется, если вы используете надлежащий набор инструментов и следуете передовым методам разработки.
Ложинья и игра! Фреймворк
Некоторое время назад я начал проект под названием Lojinha (что в переводе с португальского означает «небольшой магазин»), моя попытка создать сайт-аукцион. (Кстати, этот проект с открытым исходным кодом). Мои мотивы были следующие:
- Я действительно хотел продать некоторые старые вещи, которые я больше не использую.
- Мне не нравятся традиционные аукционные сайты, особенно те, что есть здесь, в Бразилии.
- Я хотел «поиграть» с Play! Фреймворк 2 (каламбур).
Итак, очевидно, как упоминалось выше, я решил использовать Play! Фреймворк. У меня нет точного подсчета времени, которое потребовалось на сборку, но, конечно же, прошло совсем немного времени, прежде чем мой сайт был запущен и работал с простой системой, развернутой по адресу http://lojinha.jcranky.com. На самом деле я потратил как минимум половину времени разработки на дизайн, который использует Twitter Bootstrap (помните: я не дизайнер…).
Вышеприведенный абзац должен прояснить хотя бы одну вещь: я не слишком беспокоился о производительности, если вообще беспокоился при создании Lojinha.
И это именно то, что я хочу сказать: есть сила в использовании правильных инструментов — инструментов, которые удерживают вас на правильном пути, инструментов, которые побуждают вас следовать передовым методам разработки по самой своей конструкции.
В данном случае этими инструментами являются Play! Фреймворк и язык Scala, а Akka появлялся в гостях.
Позвольте мне показать вам, что я имею в виду.
Неизменяемость и кэширование
Общепризнано, что сведение к минимуму изменчивости является хорошей практикой. Вкратце, изменчивость затрудняет анализ вашего кода, особенно когда вы пытаетесь внедрить какой-либо параллелизм или параллелизм.
Игра! Фреймворк Scala заставляет вас использовать неизменность большую часть времени, как и сам язык Scala. Например, результат, сгенерированный контроллером, неизменен. Иногда вы можете считать эту неизменность «назойливой» или «раздражающей», но эти «хорошие практики» являются «хорошими» по какой-то причине.
В данном случае неизменяемость контроллера была крайне важна, когда я, наконец, решил провести некоторые тесты производительности: я обнаружил узкое место и, чтобы исправить это, просто закэшировал этот неизменяемый ответ.
Под кэшированием я подразумеваю сохранение объекта ответа и предоставление идентичного экземпляра всем новым клиентам. Это избавляет сервер от необходимости заново пересчитывать результат. Было бы невозможно обслуживать один и тот же ответ нескольким клиентам, если бы этот результат был изменяемым.
Минус: в течение короткого периода времени (срок действия кеша) клиенты могут получать устаревшую информацию. Это проблема только в тех случаях, когда вам абсолютно необходимо, чтобы клиент имел доступ к самым последним данным, не допуская задержек.
Для справки, вот код Scala для загрузки стартовой страницы со списком товаров, без кеширования:
def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }
Теперь добавляем кеш:
def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }
Довольно просто, не так ли? Здесь «индекс» — это ключ, который будет использоваться в системе кэширования, а 5 — время истечения срока действия в секундах.
Чтобы проверить эффект от этого изменения, я провел несколько тестов JMeter (включенных в репозиторий GitHub) локально. Перед добавлением кеша я добился пропускной способности примерно 180 запросов в секунду. После кэширования пропускная способность увеличилась до 800 запросов в секунду. Это улучшение более чем в 4 раза для менее чем двух строк кода.

Потребление памяти
Еще одна область, в которой правильные инструменты Scala могут иметь большое значение, — потребление памяти. Вот, снова Играй! подталкивает вас в правильном (масштабируемом) направлении. В мире Java для «нормального» веб-приложения, написанного с помощью API сервлета (т. е. почти для любой среды Java или Scala), очень заманчиво добавить много мусора в пользовательский сеанс, потому что API предлагает простой в использовании интерфейс. методы вызова, которые позволяют вам это сделать:
session.setAttribute("attrName", attrValue);
Поскольку добавить информацию в сеанс пользователя очень просто, ею часто злоупотребляют. Как следствие, риск использования слишком большого объема памяти, возможно, без уважительной причины, столь же высок.
С игрой! framework, это не вариант — у фреймворка просто нет места для сеанса на стороне сервера. Игра! Сессия пользователя framework хранится в файле cookie браузера, и вам приходится жить с ним. Это означает, что пространство сеанса ограничено по размеру и типу: вы можете хранить только строки. Если вам нужно хранить объекты, вам придется использовать механизм кэширования, который мы обсуждали ранее. Например, вы можете захотеть сохранить адрес электронной почты или имя пользователя текущего пользователя в сеансе, но вам придется использовать кеш, если вам нужно сохранить весь объект пользователя из вашей модели домена.
Опять же, поначалу это может показаться болезненным, но на самом деле Play! удерживает вас на правильном пути, заставляя вас тщательно учитывать использование памяти, что создает код первого прохода, который практически готов к кластеру, особенно с учетом того, что нет сеанса на стороне сервера, который должен был бы распространяться по всему кластеру, что делает жизнь бесконечно легче.
Асинхронная поддержка
Далее в этой игре! обзор фреймворка, мы рассмотрим, как Play! также сияет в асинхронной (хронической) поддержке. И помимо своих собственных функций, Play! позволяет встроить Akka, мощный инструмент для асинхронной обработки.
Хотя Lojinha еще не использует все преимущества Akka, ее простая интеграция с Play! стало очень легко:
- Запланируйте асинхронную службу электронной почты.
- Обрабатывайте предложения для различных продуктов одновременно.
Вкратце, Akka — это реализация акторной модели, ставшей известной благодаря Erlang. Если вы не знакомы с акторной моделью Akka, просто представьте ее как небольшую единицу, которая общается только посредством сообщений.
Чтобы отправить электронное письмо асинхронно, я сначала создаю правильное сообщение и актера. Тогда все, что мне нужно сделать, это что-то вроде:
EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)
Логика отправки электронной почты реализована внутри актора, и сообщение сообщает актору, какое электронное письмо мы хотели бы отправить. Это делается по схеме «выстрелил и забыл», что означает, что строка выше отправляет запрос, а затем продолжает выполнять все, что у нас есть после этого (т. е. не блокируется).
Для получения дополнительной информации о собственном Async Play!, взгляните на официальную документацию.
Заключение
Подводя итог: я быстро разработал небольшое приложение Lojinha, способное очень хорошо масштабироваться. Когда я сталкивался с проблемами или обнаруживал узкие места, исправления были быстрыми и легкими, во многом благодаря инструментам, которые я использовал (Play!, Scala, Akka и т. д.), которые заставляли меня следовать лучшим практикам с точки зрения эффективности и масштабируемость. Не заботясь о производительности, я смог масштабироваться до тысяч одновременных запросов.
При разработке вашего следующего приложения внимательно рассмотрите свои инструменты.