Простой и быстрый фреймворк для стресс-тестирования приложений

    [ english version ]

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

    Поэтому было решено набросать свой, ведь это всего 3-5 классиков в данном случае. Основные требования: быстрота и динамическая генерация запросов. При этом быстрота это не просто тысячи RPS, а в идеале — когда стресс упирается только в пропускную способность сети и работает с любой свободной машины.

    Движок


    С требованиями ясно, теперь нужно решить на чем это все будет работать, т.е. какой http/tcp клиент использовать. Конечно мы не хотим использовать устаревшую модель thread-per-connection (нить на соединение), потому что сразу упремся в несколько тысяч rps в зависимости от мощности машины и быстроты переключения контекстов в jvm. Т.о. apache-http-client и им подобные отметаются. Здесь надо смотреть на т.н. неблокирующие сетевые клиенты, построенные на NIO.

    К счастью в java мире в этой нише давно присутствует стандарт де-факто опенсорсный Netty, который к тому очень универсален и низкоуровневый, позволяет работать с tcp и udp.

    Архитектура


    Для создания своего отправщика нам понадобится ChannelUpstreamHandler обработчик в терминах Netty, из которого и будет посылать наши запросы.

    Далее нужно выбрать высокопроизводительный таймер для отправки максимально возможного количества запросов в секунду (rps). Здесь можно взять стандартный ScheduledExecutorService, он в принципе с этим справляется, однако на слабых машинах лучше использовать HashedWheelTimer (входит в состав Netty) из-за меньших накладных расходов при добавлении задач, только требует некоторого тюнинга. На мощных машинах между ними практически нет разницы.

    И последнее, чтобы выжать максимум rps с любой машины, когда неизвестны какие лимиты по соединениям в данной ОСи или общая текущая нагрузка, надежней всего воспользоваться методом проб и ошибок: задать сначала какое-нибудь запредельное значение, например миллион запросов в секунду и далее ждать на каком количестве соединений начнутся ошибки при создании новых. Опыты показали что предельное количество rps обычно чуть поменьше этой цифры.
    Т.е. берем эту цифру за начальное значение rps и потом если ошибки повторяются уменьшаем ее на 10-20%.

    Реализация

    Генерация запросов


    Для поддержки динамической генерации запросов создаем интерфейс с единственный методом, который наш стресс будет вызывать чтобы получать содержимое очередного запроса:
    public interface RequestSource {
        /**
         * @return request contents
         */
        ChannelBuffer next();
    }
    


    ChannelBuffer — это абстракция потока байтов в Netty, т.е. здесь должно возвращаться все содержимое запроса в виде потока байт. В случае http и других текстовых протоколов — это просто байтовое представление строки (текста) запроса.
    Также в случае http необходимо ставить 2 символа новой строки в конце запроса(\n\n), это является признаком конца запроса и для Netty (не пошлет запрос в противном случае)

    Отправка

    Чтобы отправлять запросы в Netty — сначала нужно явно подключиться к удаленному серверу, поэтому на старте клиента запускаем периодические подключения с частотой в соответствие с текущим rps:
    scheduler.startAtFixedRate(new Runnable() {
        @Overrid
        public void run() {
           try {
                ChannelFuture future = bootstrap.connect(addr);
                connected.incrementAndGet();
            } catch (ChannelException e) {
                if (e.getCause() instanceof SocketException) {
                    processLimitErrors();            
                }
                ...
            }, rpsRate);
    


    После успешного подключения, сразу посылаем сам запрос, поэтому наш Netty обработчик удобно будет наследовать от SimpleChannelUpstreamHandler где для этого есть специальный метод. Но есть один нюанс: новое подключение обрабатывается т.н. главном потоке («boss»), где не должны присутствовать долгие операции, чем может являться генерация нового запроса, поэтому придется перекладывать в другой поток, в итоге сама отправка запроса будет выглядеть примерно так:

    private class StressClientHandler extends SimpleChannelUpstreamHandler {        
            ....
            @Override
            public void channelConnected(ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception {
                ...
                requestExecutor.execute(new Runnable() {
                    @Override
                    public void run() {
                        e.getChannel().write(requestSource.next());
                    }
                });
                ....
            }
        }
    

    Обработка ошибок

    Далее — обработка ошибок создания новых соединений когда текущая частота отправки запросов слишком большая. И это самая нетривиальная часть, вернее сложно сделать это платформонезависимо, т.к. разные операционные системы ведут себя по разному в этой ситуации. Например linux выкидывает BindException, windows — ConnectException, а MacOS X — либо одно из этих, либо вообще InternalError (Too many open files). Т.о. на мак-оси стресс ведет себя наиболее непредсказуемо.

    В связи с этим, кроме обработки ошибок при подключении, в нашем обработчике тоже необходимо это делать (попутно подсчитывая количество ошибок для статистики):
    private class StressClientHandler extends SimpleChannelUpstreamHandler {        
            ....
            @Override
            public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
                e.getChannel().close();
    
                Throwable exc = e.getCause();
                ...
                if (exc instanceof BindException) {
                    be.incrementAndGet();
                    processLimitErrors();
                } else if (exc instanceof ConnectException) {
                    ce.incrementAndGet();
                    processLimitErrors();
                } 
                ...
            }
                ....
       }
    

    Ответы сервера

    Напоследок, надо решить что будем делать с ответами от сервера. Поскольку это стресс тест и нам важна только пропускная способность, здесь остается только считать статистику:

    private class StressClientHandler extends SimpleChannelUpstreamHandler {
      @Override
            public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
                ...
                ChannelBuffer resp = (ChannelBuffer) e.getMessage();
                received.incrementAndGet();
                ...
            }
    }
    


    Здесь же может быть и подсчет типов http ответов (4xx, 2xx)
    Весь код

    Весь код с дополнительными плюшками вроде чтения http шаблонов из файлов, шаблонизатором, таймаутами и тп. лежит в виде готового maven проекта на GitHub (ultimate-stress). Там же можно скачать готовый дистрибутив (jar файл).

    Выводы


    Все конечно упирается в лимит открытых соединений. Например на linux при увеличении некоторых настроек ОС (ulimit и т.п.), на локальной машине удавалось добиться около 30K rps, на современном железе. Теоритечески кроме лимита соединений и сети больше ограничений быть не должно, на практике все же накладные расходы jvm дают о себе знать и фактический rps на 20-30% меньше заданного.

    Only registered users can participate in poll. Log in, please.

    Что вы используете для стресс-тестирования java приложений?

    • 5.6%apache http client6
    • 56.1%jmeter60
    • 7.5%tsung8
    • 6.5%siege7
    • 12.2%apache ab13
    • 5.6%какой-либо опенсорсный6
    • 15.0%самописный16
    • 7.5%soapUI/loadUI8
    • 11.2%Yandex.Tank12
    • 4.7%Gatling Tool5

    Какую нагрузку удавалось создать (rps)?

    • 20.8%<1K15
    • 16.7%1-5K12
    • 22.2%5-10K16
    • 11.1%10-20K8
    • 12.5%20-50K9
    • 11.1%50-100K8
    • 18.1%>100K13

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 21

      0
      soapUI/loadUI добавьте пожалуйста в опрос
      0
      И последнее, чтобы выжать максимум rps с любой машины, когда неизвестны какие лимиты по соединениям в данной ОСи или общая текущая нагрузка, надежней всего воспользоваться методом проб и ошибок: задать сначала какое-нибудь запредельное значение, например миллион запросов в секунду и далее ждать на каком количестве соединений начнутся ошибки при создании новых. Опыты показали что предельное количество rps обычно чуть поменьше этой цифры.
      Т.е. берем эту цифру за начальное значение rps и потом если ошибки повторяются уменьшаем ее на 10-20%.


      Есть более простой способ. То что вы описали, это подача нагрузки RPSами, но можно подавать «свободными инстансами или потоками».

      Я изначально знаю что мой сервис может обрабатывать 1000 соединений и при этом скорее всего мы достигнем предела scalability.
      Беру любой тул, ставлю в нем такое кол-во сокетов и убираю подачу нагрузку по таймеру. Как только соединение освобождается, отсылается новый запрос. В итоге 1000 сокетов будет использоваться настолько быстро, насколько быстро отвечает сервер. Так и определим максимальный RPS, который может отдавать сервер.

      С помощью такого метода не нужно делать других ретестов или ждать пока нагрузка дорастет до пары десятков RPS.

      А зачем писать свой, когда уже есть Gatling Tool который уже умеет это все из коробки?:)
        0
        Gatling Tool тоже монстр +на scala, это конечно не проблема, но порог вхождения есть: dsl, статистика, гуй, интерграция всего со всем… сомневаюсь что это все добавляет быстродействия к netty, который там внутри.

        +опять же если совсем свою логику дергать надо для генерации запроса, которая в коде твоего проекта лежит то как?

        Про 1000 сокетов не понял — вы ставите в туле ограничение на 1000 сокетов/потоков и в каждом таком потоке посылаете по таймеру запросы?
          0
          Gatling Tool тоже монстр +на scala, это конечно не проблема, но порог вхождения есть: dsl, статистика, гуй, интерграция всего со всем… сомневаюсь что это все добавляет быстродействия к netty, который там внутри.

          +опять же если совсем свою логику дергать надо для генерации запроса, которая в коде твоего проекта лежит то как?


          Кмк, это изначально лучший путь, т.к. у того проекта уже есть community и куча фич, включая требования.

          Про 1000 сокетов не понял — вы ставите в туле ограничение на 1000 сокетов/потоков и в каждом таком потоке посылаете по таймеру запросы?


          Нет, вообще без таймеров

          while(1) { do_request(); }
            0
            Кмк, это изначально лучший путь, т.к. у того проекта уже есть community и куча фич, включая требования.


            в целом я согласен, что когда нужно хотя бы несколько фич из этой кучи, а там уже и community и все остальное, тогда безусловно, но если просто нужно дернуть какой-то метод из своего легаси кода при генерации контента запроса — с этим как, или он это тоже может?

            Нет, вообще без таймеров

            while(1) { do_request(); }


            Но это вроде бы не совсем эмулирует боевую нагрузку: там же каждый раз новые соединения создаются а не старые переиспользуются как у вас.

            И сколько максимально rps вам удавалось добиться таким способом? на localhost-е и на удаленный сервер в локальной сетке например?
              0
              Но это вроде бы не совсем эмулирует боевую нагрузку: там же каждый раз новые соединения создаются а не старые переиспользуются как у вас.


              Вы не поняли. Задача такого теcта «быстро оценить максимальную пропускную способность», чтобы потом уже проводить тесты около неё, эмулируя боевую.

              И сколько максимально rps вам удавалось добиться таким способом? на localhost-е и на удаленный сервер в локальной сетке например?


              Дело не только в RPSе, дело в протоколе. дело в самой подаче нагрузке даже с обычным таймером. С таймером, у вас запросы идут на сервер с равномерным распределением. Если вы посылаете на сервер 100rps, то каждые 10ms приходит новый запрос. В жизни все иначе, для большинства сервисов подойдет процесс Пуассона, который к слову, почему-то реализован только в twitter iago.

              Дело в логике работы и еще много в чем.
              Окей, давайте про циферки. В апреле выступал на SQA Days 2013, в докладе показывал 40krps, но с той же Cassadnra были тесты на 95krps.

              Пару недель назад делал тесты с asynchbase. 1 нагрузочноая станция, HBase кластер из трех машин, 110krps на отправке 1кб запросов на запись:)
                0
                Как только соединение освобождается, отсылается новый запрос. В итоге 1000 сокетов будет использоваться настолько быстро, насколько быстро отвечает сервер. Так и определим максимальный RPS, который может отдавать сервер.

                Вы не поняли. Задача такого теcта «быстро оценить максимальную пропускную способность», чтобы потом уже проводить тесты около неё, эмулируя боевую.


                А как в этом бесконечном цикле понять что соединение закрылось? Что-то вроде своего пула сокетов, и как только какой-то освобождается нотифицировать все потоки и посылать в этот сокет?

                Дело не только в RPSе, дело в протоколе. дело в самой подаче нагрузке даже с обычным таймером. С таймером, у вас запросы идут на сервер с равномерным распределением.


                не совсем так, с равномерным распределением мы коннектимся к серверу, а у ж как только коннект асинхронно отработает — посылаем запрос. Это конечно не Пуассоновское распределение, но вся эта асинхронность тоже вносит определенный элемент случайности.
                  0
                  А как в этом бесконечном цикле понять что соединение закрылось? Что-то вроде своего пула сокетов, и как только какой-то освобождается нотифицировать все потоки и посылать в этот сокет?


                  Не совсем понял вопроса:)

                  не совсем так, с равномерным распределением мы коннектимся к серверу, а у ж как только коннект асинхронно отработает — посылаем запрос. Это конечно не Пуассоновское распределение, но вся эта асинхронность тоже вносит определенный элемент случайности.


                  Совсем не понял, как соотносится асинхронность движка с интервалами запросов?
                    0
                    Не совсем понял вопроса:)

                    Так?

                    тыща потоков дает на моей машине где то 8 тыщ rps, 4 тысячи потоков => 10K rps, дальнейшее увеличение потоков прироста rps не дает.
                    Как мне добиться 30K rps которые я на этой же машине получаю с помощью таймера?
                      0
                      Профилируйте, ищите хотспоты и узкие места, расширяйте и передвигайте их. Золотого правила дать, простите, не могу;)
                        0
                        Почитал исходный код. Если вы получаете 30Krps на таком тесте, с переоткрытием сокетов, то можно считать что это предел. Ядро GNU/Linux уже не может чаще переоткрывать сокеты. Граница как раз на 30-35krps.

                        Посмотрите на утилизацию первых ядер, у вас в tope ядра должны много времени проводить в software irq.

                        Кстати вы его не закрываете;-)
                          0
                          Почитал исходный код. Если вы получаете 30Krps на таком тесте,


                          30К может и предел, но получаю я их описанным в посте способом т.е. с использованием таймера, где сокеты тоже переоткрываются естественно. И тогда мне неясно почему Вашим методом я получаю только 10К на той же машине в тех же условиях?

                          Кстати вы его не закрываете;-)


                          он там неявно закрывается из br.close(), на всякий случай добавил явно — ничего не поменялось
                            0
                            30К может и предел, но получаю я их описанным в посте способом т.е. с использованием таймера, где сокеты тоже переоткрываются естественно. И тогда мне неясно почему Вашим методом я получаю только 10К на той же машине в тех же условиях?


                            Ну необходимо смотреть на машинку. Условия априорите не те же. Вы используете синхронные блокирующие сокеты, вместое асинхронных-неблокирующих в netty. Уже разное количество нитей, которые обрабатывают сокеты и много еще нюансов. Давайте перейдем в личку, если хотите продолжить.

                            Мой основной поинт в закленной отправке запросов был в том, «как быстро оценить максимальную пропускную способность хендлера». Для этого нужно натравить что-то простое вроде ab/wrk или даже тот же yandex-tank без схемы нагрузки и таймеров. и понять максимум. Дальше уже делать новый тест с таймерами и играться около этого уровня пропускной способности.
          0
          Коллеги смогли выжать больше 200 000 (да, двести тысяч) RPS, но там было постоянное соединение, операция типа прочитать ключ из кеша. 8 ядер в sys, 8 ядер в usr как на стреляющей машине, так и на сервере-мишени. Стреляли танком.
            0
            Ну такие циферки много где актуальны, даже если wrk по nginx пулять можно 500Krps выжать:) lowlatencyweb.wordpress.com/2012/03/20/500000-requestssec-modern-http-servers-are-fast/
              0
              Честно говоря, слабо верится, что это с передачей данных по сети.
                0
                Можем вечером проверить в том же кластере нагрузочного тестирования эллиптикса:)
              0
              Yandex.Tank он на питоне? Тогда сдается мне что это не быстрей простого netty при прочих равных — таже реализация NIO через виртуальную машину (питона в данном случае)
                0
                Там обвязка на питоне. Внутри можно ставить разные движки, основной phantom, он написан на Сях и может подавать нагрузку порядка 200Krps. Можно так же пользоваться ab и jmeter.
              0
              Помнится на Scaladev 2012 был доклад про нагрузочное тестирование на базе Akka 2. Слайды и почти немое кино.

              Only users with full accounts can post comments. Log in, please.