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

в 19:02, , рубрики: actor model, akka, comet, highload, java, play framework, scala, высокая производительность, метки: , , , ,

Измерение производительности 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 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 (архитектура которого также основана на Akka) оказалось невозможным ввиду отсутствия функции тестирования comet-подключений. При этом провести доработку также оказалось сложной задачей, т.к. документация разработчика находится в стадии создания. По этим причинам был разработан собственный инструмент для тестирования.

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

Сценарий состоит из трех шагов:

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

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

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

  1. Измерение максимального количества 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 мс, при этом загрузка процессора остается вполне приемлемой. Полагаю, что дальнейшее увеличение количества соединений упирается только в объем оперативной памяти.

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

    В предыдущих тестах было определено, что рассылка поступающего значения по 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

Поделиться

* - обязательные к заполнению поля