- PVSM.RU - https://www.pvsm.ru -
Я уже рассказывал [1] о программной платформе Typesafe Stack 2.0 [2]. В том посте шла речь об одном из компонентов платформы — фрэймворке Akka 2.0 [3], реализующем модель акторов на JVM. Сегодня я хочу написать о возможностях другой составляющей Typesafe Stack — фрэймворке Play 2.0 [4]. Хотя о функциональности данного компонента уже рассказывали здесь [5] и здесь [6], тема производительности решений под управлением Play 2.0 по-моему осталась не раскрытой.
Тестирование фрэймворка будет проводиться с помощью простейшего приложения разработанного на его основе. В результате выполнения тестов необходимо ответить на следующие вопросы. Какое максимально возможное количество одновременных подключений? Сколько оперативной памяти потребляют эти подключения? Сколько запросов в единицу времени может обработать тестируемое приложение?
Прежде чем перейти к описанию тестируемого приложения, следует прояснить основные архитектурные особенности фрэймворка Play 2.0. HTTP-сервер Play основан на высокопроизводительной библиотеке Netty [7]. Это не только позволяет использовать его «из коробки», исключая настройку какого-либо сервлет-контейнера, но и обеспечивает возможность асинхронной обработки клиентских запросов. В классическом синхронном варианте обработки, любой поступающий запрос, для ответа на который требуется выполнить некоторое вычисление, будет занимать поток операционной системы все время пока осуществляется данное вычисление. Play же позволяет на время вычисления вернуть поток в пул сервера и снова занять поток для ответа, когда вычисление будет готово. Технически это означает возможность одновременного подключения большего количества клиентов, чем в синхронном варианте.
Тестируемое приложение будет выполнять три основных функции:
/wait?cid={connection_id}
)/put?v={value}
)/closeall
)При разработке использовалась библиотека Akka 2.0. Приложение разработано на языке Scala, так как с моей точки зрения он более удобен для работы с Akka, по-сравнению с Java. Ниже я приведу основные части кода, чтобы только показать простоту работы с подключениями в Play 2.0 и не уходить от сути данного поста. Весь код можно получить из git-репозитория, ссылка на который приведена в конце публикации.
...
lazy val channel: PushEnumerator[String] = ch
...
def receive = {
case Message(message) =>
{
channel.push(message)
}
...
case Close =>
{
channel.push("closed")
channel.close()
self ! Quit
}
}
...
Переменная channel
— это источник данных для comet-подключения (тип — Enumerator
), который, как будет показано ниже передается комет-подлючению через адаптер Comet
(тип — Enumeratee
). Подробнее о работе с источниками-преобразователями-потребителями потоков данных в Play можно прочитать здесь [8]. Передача данных в comet-сокет осуществляется вызовом функции channel.push(message)
. Закрытие comet-сокета — вызовом channel.close()
.
В функции актора ConnectionSupervisor входят: создание comet-соединения, отправка сообщения в созданные соединения, закрытие всех соединений.
...
var connectionActors = Seq.empty[ActorRef]
def receive = {
case SetConnect(connectionId) =>
{
lazy val channel: PushEnumerator[String] = Enumerator.imperative(
onComplete = self ! Disconnect(connectionId)
)
val connectionActor = context.actorOf(Props(new ConnectionActor(channel)), connectionId)
connectionActors = connectionActors :+ connectionActor
sender ! channel
}
case BroadcastMessage(message) =>
{
connectionActors.foreach(_ ! Message(message))
}
case CloseAll =>
{
connectionActors.foreach(_ ! Close)
}
}
...
Ссылки на созданные акторы хранятся в последовательности connectionActors (тип — Seq[ActorRef]). При установлении соединения создается канал channel, который передается в новый актор ConnectionActor. Актор добавляется к списку акторов. Как рассылаются сообщения и закрываются соединения должно быть понятно из кода.
Предполагается, что в StorageActor
поступает значение, производятся какие-либо действия и значение рассылается во все comet-соединения, а также возвращается клиенту. Таким образом имитируется поведение некоторого реального приложения, когда клиент делает запрос и ожидает на него ответ.
...
var value = ""
def receive = {
case Put(v) =>
{
value = v
connectSupervisor ! BroadcastMessage(value)
sender ! value
}
}
...
...
object Application extends Controller {
...
def waitFor(connectionId: String) = Action {
implicit val timeout = Timeout(1.second)
AsyncResult {
(ActorsConfig.connectSupervisor ? (SetConnect(connectionId)) ).mapTo[Enumerator[String]].asPromise.map { chunks =>
Ok.stream(chunks &> Comet( callback = "console.log"))
}
}
}
def broadcastMessage(message: String) = Action {
ActorsConfig.connectSupervisor ! BroadcastMessage(message)
Ok
}
def putValueAsync(value: String) = Action {
implicit val timeout = Timeout(1.second)
Async {
(ActorsConfig.storageActor ? Put(value)).mapTo[String].asPromise.map { value =>
Ok(value)
}
}
}
def closeAll = Action {
ActorsConfig.connectSupervisor ! CloseAll
Ok
}
}
На методы данного контроллера отображаются адреса из поступающих HTTP-запросов (описание маршрутов находится в файле conf/routes
). Наибольший интерес здесь представляет метод waitFor
, который создает comet-сокет и связывает с ним канал типа Enumerator[String]
. Канал в сокет отправляется актором в ответ на сообщение SetConnect
. Каждое поступающее в канал сообщение передается в браузер клиента как параметр функции, указанной в объекте Comet( callback = "console.log")
. В данном случае — это функция console.log
.
Со стороны клиента comet-соединение создается с помощью скрытого элемента , например:
<iframe src='/wait?cid=1' style='display:none'/>
Тестируемое приложение было запущено на виртуальной машине под управлением Ubuntu 11.10 (32-bit) c 1 ГБ оперативной памяти и 1-ядром процессора (процессор физического компьютера - Intel Core i5 2.8GHz).
Провести тестирование стандартными средствами (JMeter, Visual Studio Load Test) не удалось, т.к. запуск даже 700 параллельных потоков озадачил тестирующую систему настолько сильно, что создать хоть сколько-нибудь существенную нагрузку оказалось невозможным. Использование специального тестирующего инструмента такого как Gatling Stress Tool [9] (архитектура которого также основана на Akka) оказалось невозможным ввиду отсутствия функции тестирования comet-подключений. При этом провести доработку также оказалось сложной задачей, т.к. документация разработчика находится в стадии создания. По этим причинам был разработан собственный инструмент для тестирования.
Сценарий состоит из трех шагов:
В процессе тестирования собираются данные о количестве установленных comet-соединений, количестве принятых значений в среднем в каждом comet-соединении, количестве запросов передачи значения и из них успешных, а также времени выполнения этих запросов в среднем. В процессе выполнения каждого шага сценария фиксировалась загрузка процессора и объем занимаемой оперативной памяти (для comet-соединений).
Количество запросов передачи значения в этой группе тестов - 1, отклоненные запросы при передаче значения отсутствуют.
Кол-во comet-соединений | Частота установления соединений | Макс.процент загрузки процессора при установлении соединений | Занимаемая память | Макс.процент загрузки процессора при передаче значения | Среднее время выполнения запроса передачи значения |
---|---|---|---|---|---|
500 | 50 мс | 15% | 36 МБ | 15% | 12 мс |
1000 | 40 мс | 17.5% | 56 МБ | 15% | 10 мс |
3000 | 20 мс | 25% | 145 МБ | 38% | 9 мс |
3000 | 10 мс | 40.6% | 140 МБ | 63% | 17 мс |
3000 | 5 мс | 59.9% | 143 МБ | 66.8% | 26 мс |
3000 | 3 мс | 98% | 140 МБ | 65% | 12 мс |
6000 | 4 мс | 80.5% | 263 МБ | 80.9% | 16 мс |
8000 | 4 мс | 73.7% | 375 МБ | 93.4% | 9 мс |
10000 | 4 мс | 77.1% | 487 МБ | 100% | 10 мс |
Итог - 10000 comet-соединений с частотой 1 раз в 4 мс, при этом загрузка процессора остается вполне приемлемой. Полагаю, что дальнейшее увеличение количества соединений упирается только в объем оперативной памяти.
В предыдущих тестах было определено, что рассылка поступающего значения по comet-соединениям - достаточно ресурсоемкая операция, поэтому в дальнейших тестах количество соединений будет уменьшено. Кроме этого, частота установления comet-соединений будет всегда равна 4 мс.
Кол-во comet-соединений | Кол-во запросов передачи значения | Частота выполнения запросов передачи значения | Макс.процент загрузки процессора | Среднее время запроса | Кол-во отклоненных запросов |
---|---|---|---|---|---|
1000 | 10 | 10 мс | 85.7% | 163 мс | 7 |
1000 | 10 | 500 мс | 57.1% | 45 мс | 0 |
1000 | 10 | 200 мс | 59.2% | 374 мс | 2 |
500 | 10 | 200 мс | 77% | 39 мс | 0 |
100 | 10 | 100 мс | 25% | 19 мс | 0 |
100 | 100 | 50 мс | 68.9% | 35 мс | 0 |
100 | 100 | 30 мс | 100% | 250 мс | 0 |
10 | 100 | 20 мс | 31% | 12 мс | 0 |
10 | 1000 | 10 мс | 61.7% | 18 мс | 0 |
10 | 1000 | 5 мс | 98.7% | 47 мс | 0 |
1 | 1000 | 4 мс | 58.2% | 27 мс | 0 |
1 | 1000 | 3 мс | 64.5% | 27 мс | 0 |
1 | 1000 | 2 мс | 92.3% | 33 мс | 0 |
1 | 10000 | 2 мс | 100% | 400 мс | 4636 |
1 | 10000 | 3 мс | 100% | 384 мс | 3852 |
1 | 8000 | 4 мс | 100% | 415 мс | 3217 |
1 | 5000 | 5 мс | 100% | 399 мс | 292 |
1 | 3000 | 7 мс | 100% | 129 мс | 0 |
1 | 2000 | 7 мс | 98% | 58 мс | 0 |
Понятно, что в тестируемом приложении обработка большого потока запросов невозможна без уменьшения количества comet-подключений. При одном comet-соединении стала доступной кратковременная пиковая нагрузка до 500 запросов в секунду за счет асинхронных возможностей фрэймворка Play. Рабочая нагрузка составляет около 100-150 запросов в секунду.
Учитывая довольно низкую мощность аппаратной части системы, под управлением которой было запущено тестируемое приложение, полагаю, что полученные результаты более чем интересные. Play Framework 2.0 оказался не только удобным инструментом разработки, но и сервером приложений, удовлетворяющим современным требованиям производительности онлайн систем.
Автор: tolyasik
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/5940
Ссылки в тексте:
[1] уже рассказывал: http://habrahabr.ru/post/140368/
[2] Typesafe Stack 2.0: http://typesafe.com/
[3] Akka 2.0: http://akka.io
[4] Play 2.0: http://www.playframework.org/
[5] здесь: http://habrahabr.ru/post/140700/
[6] здесь: http://habrahabr.ru/post/141439/
[7] Netty: http://www.jboss.org/netty
[8] здесь: http://www.playframework.org/documentation/2.0/Iteratees
[9] Gatling Stress Tool: http://gatling-tool.org/
Нажмите здесь для печати.