Pull to refresh

Измерение производительности Play Framework 2.0

Reading time 6 min
Views 7K

Измерение производительности Play Framework 2.0


Я уже рассказывал о программной платформе Typesafe Stack 2.0. В том посте шла речь об одном из компонентов платформы — фрэймворке Akka 2.0, реализующем модель акторов на JVM. Сегодня я хочу написать о возможностях другой составляющей Typesafe Stack — фрэймворке Play 2.0. Хотя о функциональности данного компонента уже рассказывали здесь и здесь, тема производительности решений под управлением Play 2.0 по-моему осталась не раскрытой.

Тестирование фрэймворка будет проводиться с помощью простейшего приложения разработанного на его основе. В результате выполнения тестов необходимо ответить на следующие вопросы. Какое максимально возможное количество одновременных подключений? Сколько оперативной памяти потребляют эти подключения? Сколько запросов в единицу времени может обработать тестируемое приложение?

Тестируемое приложение

Прежде чем перейти к описанию тестируемого приложения, следует прояснить основные архитектурные особенности фрэймворка Play 2.0. HTTP-сервер Play основан на высокопроизводительной библиотеке Netty. Это не только позволяет использовать его «из коробки», исключая настройку сервлет-контейнера, но и обеспечивает возможность асинхронной обработки клиентских запросов. В классическом синхронном варианте обработки, любой поступающий запрос, для ответа на который требуется выполнить некоторое вычисление, будет занимать поток операционной системы все время пока осуществляется данное вычисление. Play же позволяет на время вычисления вернуть поток в пул сервера и снова занять поток для ответа, когда вычисление будет готово. Технически это означает возможность одновременного подключения большего количества клиентов, чем в синхронном варианте.

Тестируемое приложение будет выполнять три основных функции:
  • создавать comet-соединение с браузером клиента (/wait?cid={connection_id})
  • принимать поступающее значение и рассылать его в консоли браузеров всех имеющихся comet-соединений (/put?v={value})
  • закрывать все существующие comet-соединения (/closeall)

При разработке использовалась библиотека Akka 2.0. Приложение разработано на языке Scala, так как с моей точки зрения он более удобен для работы с Akka, по сравнению с Java. Ниже я приведу основные части кода, чтобы только показать простоту работы с подключениями в Play 2.0 и не уходить от сути данного поста. Весь код можно получить из git-репозитория, ссылка на который приведена в конце публикации.

Актор comet-подлючения

...
  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 можно прочитать здесь. Передача данных в 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
    }
  }
...

Контроллер Application

...
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, например:
<iframe src='/wait?cid=1' style='display:none'/>

Процесс тестирования

Тестируемое приложение было запущено на виртуальной машине под управлением Ubuntu 11.10 (32-bit) c 1 ГБ оперативной памяти и 1-ядром процессора (процессор физического компьютера — Intel Core i5-2400 3.1GHz).

Провести тестирование стандартными средствами (JMeter, Visual Studio Load Test) не удалось, т.к. запуск даже 700 параллельных потоков озадачил тестирующую систему настолько сильно, что создать хоть сколько-нибудь существенную нагрузку оказалось невозможным. Использование специального тестирующего инструмента такого как Gatling Stress Tool (архитектура которого также основана на Akka) оказалось невозможным ввиду отсутствия функции тестирования comet-подключений. При этом провести доработку также оказалось сложной задачей, т.к. документация разработчика находится в стадии создания. По этим причинам был разработан собственный инструмент для тестирования.

Сценарий тестирования

Сценарий состоит из трех шагов:
  • создается заданное количество comet-подключений с определенной частотой
  • производится передача заданного количества значений с определенной частотой
  • comet-соединения закрываются соответствующим запросом

В процессе тестирования собираются данные о количестве установленных comet-соединений, количестве принятых значений в среднем в каждом comet-соединении, количестве запросов отправки значения и из них успешных, а также времени выполнения этих запросов в среднем. В процессе выполнения каждого шага сценария фиксировалась загрузка процессора и объем занимаемой оперативной памяти (для comet-соединений).

Результаты тестирования

  1. Измерение максимального количества comet-соединений и объема занимаемой оперативной памяти.

    В данной группе тестов после установки comet-соединений отправляется единственное значение, после чего подключения закрываются.
    Кол-во comet-соединений Частота установления соединений (1/мс) Занимаемая память (МБ) Макс.загрузка процессора при установке соединений Макс.загрузка процессора при отправке значения
    500 50 36 15% 15%
    1000 40 56 17.5% 15%
    3000 20 145 25% 62%
    3000 10 142 40.6% 61%
    3000 5 140 59.9% 70.8%
    3000 3 138 98% 69%
    6000 4 262 80.5% 93.4%
    8000 4 394 73.7% 80.9%
    10000 4 485 77.1% 100%

    Загрузка процессора в процессе установления comet-соединений с периодом 4 мс находится в разумных пределах, поэтому добавление дополнительных подключений является лишь вопросом объема оперативной памяти.

  2. Измерение максимального потока запросов отправки значения и кратковременной пиковой нагрузки.

    Как видно из предыдущих тестов, отправка значения в comet-соединения является ресурсоемкой операцией, поэтому в данной группе тестов, для измерения максимальной пропускной способности сервера, количество соединений будет уменьшено.
    Кол-во comet-соединений Кол-во отправленных значений Частота отправки (1/мс) Макс.загрузка процессора при отправке Среднее время выполнения запроса (мс) Количество отклоненных запросов
    1000 10 100 85.7% 163 7
    1000 10 500 76.5% 45 0
    1000 10 200 100% 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 2 92.3% 33 0
    1 10000 2 100% 400 4636
    1 8000 4 100% 415 3217
    1 5000 5 100% 399 292
    1 3000 7 100% 129 0
    1 2000 7 98% 58 0

    Проведенные тесты показывают, что приложение справляется с кратковременными пиковыми нагрузками до 500 запросов в секунду и нормально работает при нагрузке 100-150 запросов в секунду.


UPDATE:
Тестируемое приложение: git://github.com/tolyasik/testeeApp.git
Тестирующее приложение: git://github.com/tolyasik/testerApp.git
Tags:
Hubs:
+18
Comments 31
Comments Comments 31

Articles