...

среда, 10 июля 2013 г.

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

Привет, хабрадевелоперам!

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


Поэтому было решено писать свой. Основные требования: быстрота и динамическая генерация запросов. При этом быстрота это не просто тысячи 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% меньше заданного.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий