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

    Измерение производительности 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
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 31

      +2
      Очень интересная статья.
      Критики Play 2 говорят как раз о невысокой производительности.
      Было бы ещё интересно узнать о производительности подобного приложения в других java frameworks (хотя бы в том же Play 1.x), чтобы сравнить их между собой.
        0
        Kipriz,

        Сами понимаете вторая версия вышла недавно так что пока свежих сравнительный работ нет. Вот если хотите несколько ссылок с версией 1.1:

        Сранение с кучей других фреймворков
        Сравнение с нодом

        Не может версия 2 быть более медленней чем 1.1, поверьте мне на слово, а если не хотите то сравните реализацию фитч из версии 1.1 и 2.
        +10
        Мне одному кажется, что эти цифры не говорит ни о чём?
          +1
          Нет ну как минимум цифры говорят что кто-то старался их подготовить, но хорошо бы действительно сравнение с конкурирующим решением.
            0
            Да, поэтому в конце поста приведены ссылки на исходники тестируемого и тестирующего приложений.
            0
            u_story,

            А вы попробуйте реализовать похожий тест на последней версии томката и уверен ваши цифры вас удивят. Насколько я понимаю автор запускал приложение без определенных jvm параметров (server или увеличение permGen итд)?
              +2
              зачем мне запускать подобные тесты? что они должны показать?
                0
                Никаких дополнительных настроек фрэймворка не проводилось (хотя это возможно), решение «из коробки».
                0
                У меня одного нет никаких цифр, а только «Кол-во» в окантовке?
                  0
                  Временные технические неполадки, извините.
                0
                Может не очень по теме, но я как раз собирался выучить Play 1, уже начал писать что-то и тут вышла вторая ветка. Что посоветуете — забить на первую? Будет ли первая ветка поддерживаться или на неё забьют окончательно?
                  0
                  Вторая ветка все еще достаточно сырая, да и некоторые важные фичи там отсутсвуют(например упаковка в war).
                  Я бы посоветовал если вы не фанат scala и проект не для поиграться всеже использовать первую ветку.
                    0
                    Спасибо. Останусь пока на первой. Время покажет что там со второй веткой… А то пока как-то туманно все
                      0
                      Настолько сырая что линкдин и еще кое кто из очень известный компаний (официальную информацию вы можете узнать позже) изучают возможность перейти в очень коротком будущем на play 2.0. «Фитча» — упаковка в war, скорее бремя чем аватаж. Так же очень важный фактор в пользу второй версии это акка а не доморощенный асинхроно-событийный модуль который понимает Quartz.
                        0
                        Настолько сырая, что сервис Klout использует ее в продакшен и обрабатывает 10 млрд.запросов в месяц (по информации от создателей Play Framework).
                        0
                        Советую вам переходить на ветку 2.0 т.к. первая находиться только в поддержке и исправлении ошибок и непонятно сколько времени так будут обстоять дела. Скорее всего она поживет еще какое то время и на нее забьют как вы сказали.
                          0
                          Тогда ещё вопрос в догонку? Имеет ли смысл сразу изучать Scala? Как я понял это теперь основной вектор развития Play
                            0
                            Изучать что то новое и такое многообещающее всегда интересно, вы не думаете? Но если у вас реально очень сжатые сроки то нет, вы сможете писать на java, может будет выходить более громоздко это ведь старушка java. Должен с вами не согласится по поводу основного вектора, я бы сказал scala и java равноправны (это почти ничего не стоит) только на java с его недостатками читаемость кода будет ниже (ну это повсюду не только в play).
                        0
                        Настолько сырая, что сервис Klout использует ее в продакшен и обрабатывает 10 млрд.запросов в месяц (по информации от создателей Play Framework).
                          0
                          Это получается 4000 запросов в секунду. Ну, или до 40000 в секунду в часы пик.
                          Мне одному кажется, что нагрузка не запредельная?
                            0
                            Посмотрите на конфиг виртуальной машины в которой проводились тесты, не думаю что на последнем томкате-сомакате или jetty (в обычном сервлете) вы сможете приблизится и к 1000 запросам на такой конфигурации.
                          0
                          Может быть я чего-то не понял, но суть статьи в том, что используя Scala+Play получаем быстрое приложение и 500 запросов в сек.?
                          Разве это быстро?
                          Простейший сервер на netty выдает порядка 20000 запросов в сек на моем древнем нетбуке…
                            0
                            а какой сегодня курс?
                              0
                              Первый раз встречаю человека который пишет на java на нетбуке. А вообще давайте по существу какая у вас конфигурация, при каких обстоятельствах (исходники киньте) у вас получился такой результат.
                                0
                                1. Кто сказал, что я пишу на нетбуке??
                                2. Конфигурация Lenovo EDGE 13", AMD 1,4ГГц, 4Гига памяти, Win7Home64bit
                                3. Обстоятельства очень простые. Берем netty, делаем минимальный сервер на немиз 10 строк и вуаля. порядка 20000 запросов в сек получаем…
                                  0
                                  Добрый день, вы и сказали:

                                  запросов в сек на моем древнем нетбуке… (Не будем углубятся в детали со стороны мне так показалось)

                                  Теперь по порядку:
                                  1. Ваша машина в разы мощнее чем виртуалка автора
                                  2. Автор здесь описывает тесты с перманентными комет-соединениями, а теперь взгляните на пункт:
                                    Тестируемое приложение будет выполнять три основных функции. Что касается вашего приложения в 10 строк, я не думаю что оно способно выполнять те же самые функции что и тест автора. Поскольку вы не разу не сказали про комет-соединения осмелюсь предположить что вы просто тестировали нагрузку сервера на обычные входящие запросы а это очень сильно отличается от того что проделал автор. И вообще если мои предположения верны 2000 зп/сек для вашего типа тестов это маловато.
                                    –1
                                    10 строк в студию, пожалуйста
                                      0
                                      public class Main
                                      { 
                                      	ExecutorService bossExec = Executors.newCachedThreadPool();
                                              ExecutorService ioExec   = Executors.newCachedThreadPool();
                                              factory = new NioServerSocketChannelFactory( bossExec, ioExec, 4 );
                                              
                                              ServerBootstrap bootstrap = new ServerBootstrap( factory );
                                              packetHandler  = new ProtocolPacketHandler();
                                              
                                              // Set up the pipeline factory.
                                              bootstrap.setPipelineFactory( new ChannelPipelineFactory()
                                              {
                                                  @Override
                                                  public ChannelPipeline getPipeline() throws Exception
                                                  {
                                                    ChannelPipeline p = Channels.pipeline();
                                                    
                                                    p.addLast( "handler", packetHandler );
                                                    
                                                    return p;
                                                  }
                                              }); 
                                      }
                                      		
                                      public class ProtocolPacketHandler extends SimpleChannelHandler
                                      {
                                          @Override
                                          public void messageReceived( ChannelHandlerContext ctx, MessageEvent e ) throws Exception
                                          {
                                                  log.info( e.getMessage() );       
                                          }
                                      }
                                      


                                      Ну как-то так. Я только немного «раздвинул код», чтобы понятнее было.
                                      Но в принципе суть ясна, делаем минимальный сервер на netty.
                                        0
                                        Ну как я и говорил у вас с автором совершено разные тесты и их цели в целом.
                                          0
                                          Так я с этим и не спорю. Просто хочу понять почему у нас разные результаты…
                                          0
                                          Вы сравниваете теплое с мягким. Чтобы понять о чем речь, попробуйте измерить производительность какого-нибудь http-сервера (ngnix, apache и т.п.).

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое