Нагрузочное тестирование «не-HTTP». Ч.1 JMeter
В монолите вызовы методов делались внутри одного приложения — теперь, в микросервисной архитектуре, всё взаимодействие осуществляется через сеть. Соответственно, скорость обмена информации между приложениями падает вне зависимости от используемого протокола обмена.
В этой статье мы расскажем, как писать код для нагрузочного тестирования «не-HTTP» протоколов на примере Apache Thrift с помощью таких инструментов, как JMeter и Gatling (часть 2). Тестировать будем микросервис, который должен справляться с 50K RPS. С одной нагрузочной машины постараемся достичь производительности, заявленной в этом твите:
Successfully tested #thrift protocol based service with #JMeter, got 14K responses per second. Thrift API is very nice to use in TCPClient
— JMeter-Plugins.org (@jmeter_plugins) November 1, 2011
Мы берем за основу Thrift, но это не играет особой роли, с небольшими корректировками можно тестировать gRPC, Avro и другие протоколы. Подходы в статье общие, и необходимо будет только заменить клиента.
Формально, описанные протоколы это RPC framework and/or data serialization system, но даже сами разработчики употребляют слово protocol.
Почему JMeter и Gatling
Одна из причин — opensource-происхождение. Можно заглянуть в код и попытаться понять, что и как реализовано в инструменте. И платить не надо.
На просторах GitHub существуют заточенные под конкретные протоколы инструменты. Например, для Thrift это Iago от разработчиков Twitter или Bender от ребят из Pinterest. Но работа с такими фреймворками резко увеличивают порог вхождения и bus factor. В отличие от них Gatling и тем более JMeter широко распространены и имеют большое комьюнити, у которого всегда можно попросить совет или найти описание проблемы.
Ну и нам, как любителям писать код в любой непонятной ситуации, оба инструмента отлично подходят.
Следующий вопрос: почему бы не взять один из двух инструментов? На первый взгляд мы имеем паритет между ними и хотим понять всё в деталях: какой работает лучше, у кого какие проблемы. Кроме того, интересно сопоставить подходы — Threads vs Actors.
JMeter
Хотя JMeter и является GUI-ориентированным средством нагрузки, для него можно писать код. Такой подход улучшает читаемость, позволяет хранить данные в адекватном виде в VCS и проводить code review без боли в глазах. К тому же для написания клиента для любого протокола, код является не только достаточным, но и необходимым условием. Наконец, с кодом легко обратиться к разработке за помощью, не рассказывая им о конкретном инструменте, и тем самым опять же понизить bus factor.
Посмотрим, что JMeter предлагает для нагрузки кастомных протоколов:
- Плагин «не-HTTP». Вариант, конечно, хороший, но не реализованный для нашего протокола и слабо модифицируемый.
- Написание кода внутри JMeter. Идея так себе — сложно работать с зависимостями и возможна проблема с производительностью. Согласно статье BeanShell Sampler очень медленный и писать там надо на shell — отбросим его сразу. JSR223 Sampler на первый взгляд показывает сносную производительность — оставим для дальнейшего теста.
- Написание кода в IDE всегда приятнее и удобнее. JMeter предлагает для этого два sampler: Java и Junit.
Cравнение производительности sampler
Чтобы понять, какой sampler использовать, проведем сравнительный анализ. Для тестирования напишем простенький скрипт, который соединяет строки в цикле, и установим VALUE маленьким (10) и побольше (100).
import java.security.SecureRandom;
for (int i = 0; i < VALUE; i++) {
new StringBuilder().append(
new SecureRandom().nextInt());
}
Все тесты проводились на версии JMeter 3.3:
Видно, что JSR223 работает медленнее, значит, он вылетает. У Java и Junit схожая производительность, чтобы понять, какой удобнее, придется разбирать оба.
Java Request Sampler
Java Sampler имеет всего две настройки: выбор теста для нагрузки и параметры для передачи в тест. Можно задать набор дефолтных параметров в коде — они появятся в GUI JMeter. Но следует учесть, что после добавления нового параметра прямо из GUI и сохранения тест-плана только что добавленные параметры сбросятся.
Java Sampler тест расширяет стандартный AbstractJavaSamplerClient из библиотеки JMeter (которую подключаем любым удобным для вас средством сборки, например, Gradle). Собственно, тестовый класс может состоять из самого теста, пред- и постусловий и параметров по умолчанию.
public class ThriftJavaSampler extends AbstractJavaSamplerClient {
@Override
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
SampleResult sampleResult = new SampleResult();
sampleResult.sampleStart();
boolean result = Utility.getScenario();
sampleResult.sampleEnd();
sampleResult.setSuccessful(result);
return sampleResult;
}
}
Тестовый метод принимает контекст JMeter, из которого следует брать параметры, и возвращает SampleResult, который, в свою очередь, содержит совокупность методов, помогающих настраивать тестовый прогон и оценивать результаты. Для наших целей важны 3 приведенных метода: время старта и окончания запроса, а также результат.
Junit Request Sampler
Junit Sampler также имеет выбор теста для нагрузки и здесь в одном классе можно написать несколько методов. Дефолтные параметры пробрасываются в код через элемент User Defined Variables. Все остальные настройки понятны из описания: не вызывать пред- и постусловий, добавлять в вывод assert-ы и ошибки выполнения. Не стоит включать создание экземпляра теста для каждого нового запроса, так как это значительно затормозит производительность.
Junit Request Sampler похож на обычный Junit тест, но работает немного по-другому. JMeter никогда не вызывает @BeforeClass и @AfterClass, поэтому для настройки глобального предусловия необходимо использовать отдельный тест. Еще стоит заметить, что код из Before и After не учитывается во времени прогона теста.
@BeforeClass
public static void setUpClass() {assert false;}
@Before
public void setUp() {}
@Test
public void test() {}
@After
public void cleanUp() {}
@AfterClass
public static void cleanUpTest() {assert false;}
Сами разработчики JMeter говорят, что GUI-режим следует использовать только для дебага. Но и тут надо быть осторожным со статиками и синглтонами. Например, после повторного запуска теста всё, что объявляется внутри синглтона, не будет инициализировано. В то же время без использования синглтона все объекты будут заново инициализироваться перед каждым тестом, что пагубно скажется на производительности. Статические переменные навсегда запомнят свои значения после запуска теста и не изменятся, даже если их переопределить из GUI.
Сравнив оба sampler, мы в итоге остановились на Junit Request Sampler за его простоту и, в то же время, легкость модификации.
Пишем Thrift клиент
При написании клиента следует учесть всё многообразие настроек протокола Thrift. Главное правило: клиент и сервер должны работать с одинаковыми транспортом и протоколом, иметь одну версию артефактов.
Чтобы не тратить время на создание клиента в каждом тесте и не исчерпать порты машины, с которой генерируем нагрузку, напишем сразу пул клиентов. Для пула можно указать количество клиентов, он сам будет держать нужное количество подключений, необходимо будет только брать клиента из пула перед использованием и возвращать после.
asyncClientPool = new ThriftClientPool<>(() ->
new PaymentsCreate.AsyncClient(
(TProtocolFactory) tTransport ->
new TMultiplexedProtocol(new TBinaryProtocol(tTransport), SERVICE),
new TAsyncClientManager(),
new TNonblockingSocket(host, port, TIMEOUT)),
PoolConfig.get());
Вот так мы создали пул и клиент. Главное, что в данном примере пул ничего не знает про реализацию, и конфигурируется при создании. Это самый базовый пул, написанный на GenericObjectPool, на его основе наши разработчики сделали саморегулирующийся пул с логированием и единым протоколом/транспортом.
Написали код, соберем его в jar файлы и положим рядом с JMeter:
$JMETER_DIR/lib/junit
$JMETER_DIR/lib/ext
$JMETER_DIR/lib
Следует не забыть и про сторонние библиотеки и их уникальность. Без них тест может даже не появиться в списке доступных или получить конфликт версий при запуске нагрузки.
JMeter без jmx
Статья о написании кода для JMeter будет неполной, если мы не обсудим, как избавиться от громоздких тест-планов на сотни строк XML в jmx файлах.
Напишем приложение с помощью библиотек JMeter и пройдемся по основным моментам по порядку.
Перед запуском приложения необходимо указать, где живут настройки для JMeter. При этом локально устанавливать JMeter необязательно:
final String JMETER_HOME = Utility.getJMeterHome();
JMeterUtils.loadJMeterProperties(JMETER_HOME + "jmeter.properties");
JMeterUtils.initLogging();
JMeterUtils.setLocale(Locale.ENGLISH);
JMeterUtils.setJMeterHome(JMETER_HOME);
Назовем наш тест план и укажем протокол:
TestPlan testPlan = new TestPlan("Thrift test");
TestElement sampler = Utility.getSampler();
LoopController и ThreadGroup отвечают за генератор нагрузки — как и сколько будем грузить. Тут всё стандартно:
LoopController controller = new LoopController();
controller.setLoops(10);
controller.setContinueForever(false);
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.setNumThreads(10);
threadGroup.setRampUp(0);
threadGroup.setDuration(30);
threadGroup.setSamplerController(controller);
Результаты в сжатом виде можно видеть в течении нагрузки (благодаря summariser), а затем сохранить их для последующей обработки (за это отвечает resultCollector):
Summariser summariser = new Summariser();
SampleSaveConfiguration saveConfiguration = new SampleSaveConfiguration(true);
ResultCollector resultCollector = new ResultCollector(summariser);
resultCollector.setFilename(JMETER_HOME + "report.jtl");
resultCollector.setSuccessOnlyLogging(true);
resultCollector.setSaveConfig(saveConfiguration);
Все элементы объединены в специальное JMeter дерево. Похоже на то, как мы это делаем в GUI моде:
HashTree config = new HashTree();
config.add(testPlan)
.add(threadGroup)
.add(sampler)
.add(resultCollector);
Конфигурируем и запускаем нагрузку:
StandardJMeterEngine jMeterEngine = new StandardJMeterEngine();
jMeterEngine.configure(config);
jMeterEngine.runTest();
Все, можно забыть об XML. Ура!
Предельная нагрузка
Посмотрим, сколько сможет выдать наш генератор с одной нагрузочной машины. Естественно, все настройки системы для нагрузочного тестирования произведены, мы уже поколдовали с настройками сети и увеличили лимиты на открытые дескрипторы. После прогона получаем не очень ровный график нагрузки, согласно которому мы можем сделать около 30K RPS:
Естественно встает вопрос, это предел клиентской или серверной части? Мы поставили рядом еще один JMeter в кластере и убедились, что сервис может выдать целевые 50 тысяч RPS.
P.S.
В следующей части мы разберем нагрузку «не-HTTP» с помощью Gatling, и расскажем, как нам удалось увеличить его производительность более чем в 100 раз.