Я работаю в нагрузочном тестировании относительно недолго, и одним из моих основных инструментов является Apache Jmeter. Тем не менее, большинство моих коллег не использовали Beanshell в JMeter, и в этой статье я хочу показать пару способов как он может упростить и сократить время подготовку к самим тестам. А покажу это на примере конвертации текста в base64-кодировку и простых стрельб в MongoDB.
Предисловие
Эта статья расчитана на тех, кто уже имеет некий опыт работы с JMeter, но при этом не пользовался beanshell, поэтому, если вам что-то не понятно, пожалуйста, не стесняйтесь спрашивать.
Что же такое Beanshell и зачем он нужен?
JMeter это opensource инструмент для нагрузочного тестирования написанный на Java. В него легко встраивать свои Samplers (объекты, которые стреляют), но для этого необходимо открыть IDE, отнаследоваться от класса из пакетов JMeter, и написать 4 метода. Экспортировать jar. И вставить его. А по хорошему нужно сделать еще красивую обертку для этого всего, с кнопочками, чекбоксами и всем-всем-всем.
Окей, так что же такое beanshell и зачем он нужен? Beanshell — это java-интерпретатор. Используя beanshell вы можете писать java-код сразу в JMeter, который будет исполнятся во время теста.
Java? Интерпретатор? Load Testing? Это шутка?
Используя интерпретатор вы должны осознавать все минусы и плюсы такого подхода. Главный минус — это производительность интерпретатора. Она всегда будет ниже Sampler'а, который вы скомпилируете. Из плюсов — скорость разработки. Вы всегда можете набросать прототип семплера в beanshell, а когда уже упретесь в его производительность, перекинуть всю логику в полноценный семплер.
Более того, в beanshell можно писать не только семплеры, но pre-processors (выполняют действия перед выстрелом), post-processors (выполняют действия после выстрелов) и listeners (получают отчеты о всех выстрелах samplers/transactions).
Это все звучит здорово, но почему про JMeter знают многие, а про интерпретатор так мало? Вся проблема в документации, а именно её отсутствии. На вики есть всего одна страница с описанием, и одна с примером.
Пример с Base64
Представьте, что у нас есть два сервиса, которые общаются между собой передавая сообщения в base64 кодировке. У нас стоит задача нагрузить один из этих сервисов. Нам необходимо эмитировать сообщения от одного другому. Мы можем конечно сразу набрать пул данных в base64-кодировке, но работать с ним, нам, тестировщикам, будет очень неудобно. Хотелось бы иметь пул данных в осмысленном, привычном для нас виде, а чтобы на сервер шла информация уже в необходмой кодировке. Более того, возможны ситуации когда у нас даже пула данных нет. Когда сам сервис дает нам информацию, и её мы используем для того чтобы слать новые запросы.
У нас есть сервис (http://base64.nomanlab.org/?key=base64_key), куда мы и должны передавать значение в виде GET-параметра. Сервис я поднял просто для примера. Во время публикации статьи, он будет работать, можете поиграться, но искренне прошу, не убивайте его, нагрузочные тестировщики=)
Давайте сначала составим test-plan нашего use-case. Добавляем Thread Group, а в него HTTP Request. Тест план будет выглядеть примерно так.
Ключ (base64_key) описываем отдельно, я для примера использовал User Defined Variables и задал ему имя key.
А сам HTTP Request имеет такой вид.
Но нам необходимо чтобы перед выстрелом с данными в патроне что-то произошло. Поэтому мы выбираем beanshell preprocessor и добавляем его как дочерний элемент к http request. А в нем пишем java-код. Java-код? Но как он будет связан с нашим патроном? Для этого есть объекты, которые связывают наш тест-план и код в интерпретаторе.
Сейчас мы подменяем переменную key, поэтому нам нужен до нее доступ. Её значение для данной нити (thread), хранится в объекте vars (представляет из себя что-вроде map).
Java код:
//Импортируем класс из пакета, который идем в комплекте с Apache JMeter
import org.apache.commons.codec.binary.Base64;
//Создаем объект, который занимается кодированием/декодированием
Base64 magicBox = new Base64();
//Достаем ключ из словаря, кодируем его
String key = new String (magicBox.encodeBase64(vars.get("key").getBytes()));
//Кладем в словарь новое значение ключа
vars.put("key", key);
Наш ключ подменился на значение в base64-кодировке. Если мы от сервиса получаем ответ в base64, и его необходимо распарсить, то сначала используем post-processor Regular Expression Extractor, чтобы вынуть сообщение, а потом таким же способом проходимся по нему BeanShell post-processor. Код будет следующим:
//Импортируем класс из пакета, который идем в комплекте с Apache JMeter
import org.apache.commons.codec.binary.Base64;
//Создаем объект, который занимается кодированием/декодированием
Base64 magicBox = new Base64();
//Достаем ключ из словаря, декодируем его
String key = new String (magicBox.decodeBase64(vars.get("key").getBytes()));
//Кладем в словарь новое значение ключа
vars.put("key", key);
Пример с MongoDB
А теперь то, ради чего все сюда пришли. Стрельбы в MongoDB. Использовать подобный способ для нагрузки MongoDB можно только в крайних случаях, когда нужно стрелять по медленным запросам, чтобы понять их предел в монге.
Connection Pool
Чтобы стрелять нам нужен объект, через который мы это будем делать. Нужен некий объект, с помощью которого мы будем управлять кол-вом коннекций, их параметрами. И перед стрельбой эти коннекции необходимо установить, поэтомы мы разделим логику стрельбы и установки соединений. Установку вынесем в отдельный ThreadGroup, который должен запуститься перед тестом. По этой причине выбираем setUp Thread Group. В нем ставим опции чтобы семлер выполнился всего 1 раз и конечно же сам BeanShell Sampler. А в него следующий код:
import com.mongodb.DB;
import com.mongodb.Mongo;
import com.mongodb.MongoOptions;
import com.mongodb.ServerAddress;
//bsh.shared это общее пространство между всеми beanshell интерпретаторами
bsh.shared.mongo = null;
bsh.shared.mongoOptions = new MongoOptions();
bsh.shared.db = null;
//Ставим свои опции соединения
bsh.shared.mongoOptions.autoConnectRetry = true;
bsh.shared.mongoOptions.connectionsPerHost = 50;
bsh.shared.mongoOptions.socketKeepAlive = true;
bsh.shared.mongoOptions.socketTimeout = 10000;
//Вместо precise-mongodb-amd64 укажите свой хост
bsh.shared.mongo = new Mongo(new ServerAddress("precise-mongodb-amd64", 27017), bsh.shared.mongoOptions);
//А тут вместо test свою бд
bsh.shared.db = bsh.shared.mongo.getDB("test");
if(bsh.shared.db.getStats().ok() == false) {
System.err.println("MongoDB Connection error");
}
Shoot!!
Теперь у нас есть объекты через которые мы можем стрелять, осталось сделать sampler, который просто ими воспользуется.
Тест-план выглядит так:
Я стрелял просто на проверку статуса (DB.getStats().ok()), и уперся не в BeanShell, а в 1 ядро монги, которое отдал ей в KVM на локальном ноуте.
Сделаем отдельную ThreadGroup и в ней BeanShell Sampler с таким кодом:
import com.mongodb.DB;
if(bsh.shared.db.getStats().ok() == true) {
IsSuccess = true;
}else IsSuccess = false;
Ну и конечно же красивые графики, которые показывают нашу производительность.
В конце виден 1 failure, но это из-за того что я вручную завершил тест.
Но имея доступ к DB, вы можете создать объект коллекции (DBCollection) и играться с ней. Можно инсерты, можно хитрые селекты, может что-то еще. Нужно просто знать Java API для MongoDB.
Но в реальности, для тестирования монги, или любой друго бд, лучше писать свой полноценный семплер. Я просто хотел показать как можно обойтись и без них.
Файлы
Скачать тест-план с Base64
Скачать тест-план с монгой
Эпилог
Примерно так, нехитрыми действиями, не залезая в IDE можно делать очень много интересных вещей.
Автор: sch1z0phr3n1a