[ 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%.
Для поддержки динамической генерации запросов создаем интерфейс с единственный методом, который наш стресс будет вызывать чтобы получать содержимое очередного запроса:
ChannelBuffer — это абстракция потока байтов в Netty, т.е. здесь должно возвращаться все содержимое запроса в виде потока байт. В случае http и других текстовых протоколов — это просто байтовое представление строки (текста) запроса.
Также в случае http необходимо ставить 2 символа новой строки в конце запроса(\n\n), это является признаком конца запроса и для Netty (не пошлет запрос в противном случае)
Чтобы отправлять запросы в Netty — сначала нужно явно подключиться к удаленному серверу, поэтому на старте клиента запускаем периодические подключения с частотой в соответствие с текущим rps:
После успешного подключения, сразу посылаем сам запрос, поэтому наш Netty обработчик удобно будет наследовать от SimpleChannelUpstreamHandler где для этого есть специальный метод. Но есть один нюанс: новое подключение обрабатывается т.н. главном потоке («boss»), где не должны присутствовать долгие операции, чем может являться генерация нового запроса, поэтому придется перекладывать в другой поток, в итоге сама отправка запроса будет выглядеть примерно так:
Далее — обработка ошибок создания новых соединений когда текущая частота отправки запросов слишком большая. И это самая нетривиальная часть, вернее сложно сделать это платформонезависимо, т.к. разные операционные системы ведут себя по разному в этой ситуации. Например linux выкидывает BindException, windows — ConnectException, а MacOS X — либо одно из этих, либо вообще InternalError (Too many open files). Т.о. на мак-оси стресс ведет себя наиболее непредсказуемо.
В связи с этим, кроме обработки ошибок при подключении, в нашем обработчике тоже необходимо это делать (попутно подсчитывая количество ошибок для статистики):
Напоследок, надо решить что будем делать с ответами от сервера. Поскольку это стресс тест и нам важна только пропускная способность, здесь остается только считать статистику:
Здесь же может быть и подсчет типов http ответов (4xx, 2xx)
Весь код с дополнительными плюшками вроде чтения http шаблонов из файлов, шаблонизатором, таймаутами и тп. лежит в виде готового maven проекта на GitHub (ultimate-stress). Там же можно скачать готовый дистрибутив (jar файл).
Все конечно упирается в лимит открытых соединений. Например на linux при увеличении некоторых настроек ОС (ulimit и т.п.), на локальной машине удавалось добиться около 30K rps, на современном железе. Теоритечески кроме лимита соединений и сети больше ограничений быть не должно, на практике все же накладные расходы jvm дают о себе знать и фактический rps на 20-30% меньше заданного.
Веб-приложений под высокие нагрузки в последнее время создается все больше, но вот с фреймворсками позволяющими гибко их стресс тестировать и добавлять свою логику — не густо.
Их конечно много разных (см. голосование в конце поста) но кто-то куки не поддерживает, кто-то нагрузку слабую дает, кто-то очень тяжеловесен, да и годятся они в основном для совсем однотипных запросов, т.е. динамически генерить каждый запрос используя свою логику и при этом еще максимально быстро (и в идеале на 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.61% apache http client6
56.07% jmeter60
7.48% tsung8
6.54% siege7
12.15% apache ab13
5.61% какой-либо опенсорсный6
14.95% самописный16
7.48% soapUI/loadUI8
11.21% Yandex.Tank12
4.67% Gatling Tool5
107 users voted. 119 users abstained.
Only registered users can participate in poll. Log in, please.
Какую нагрузку удавалось создать (rps)?
20.55% <1K15
16.44% 1-5K12
21.92% 5-10K16
10.96% 10-20K8
12.33% 20-50K9
10.96% 50-100K8
19.18% >100K14
73 users voted. 131 users abstained.