Если у вашей команды есть папка с .jmx-планами — этот пост для вас. Это не сравнение инструментов и не призыв к миграции: вы уже выбрали JMeter, возможно, много лет назад, и эти планы содержат реальные знания о вашей системе: сценарии, ассерты, форму thread-групп. Вопрос в другом — как запускать их в современном пайплайне: headless, в CI, со стримингом логов и summary в том же формате, что и у остальных ваших инструментов.
Именно это делает JMeter-раннер perfscale. Ваши планы работают без изменений.
Headless из CLI
OSS CLI напрямую оборачивает non-GUI режим JMeter:
perfscale run --jmeter plan.jmx
Под капотом это jmeter -n -t plan.jmx — JMeter должен быть установлен и доступен в PATH. Если его нет, CLI прямо скажет об этом и укажет на документацию по установке, вместо того чтобы уронить прогон с простынёй шелл-вывода. Во время прогона stdout/stderr JMeter стримятся в терминал вживую.
Две вещи намеренно не делаются за вас:
- perfscale не передаёт никаких
-J-свойств. JMeter полностью владеет формой нагрузки. Хотите параметризовать потоки, циклы или хосты — делайте это внутри плана функциями свойств JMeter:${__P(vus,10)},${__P(host,example.com)}— и либо задавайте дефолты в плане, либо оборачивайте вызовjmeterв скрипт, который их выставляет. - perfscale не переписывает ваш план. Никаких внедрённых листенеров, никаких изменённых thread-групп. Что закоммичено — то и запускается.
Как выглядит summary
Во время прогона JMeter печатает периодические дельта-строки summary +, а по завершении — одну финальную строку summary =. В ней есть количество сэмплов, throughput, avg/min/max латентности и число ошибок — но нет перцентилей. Консольное summary JMeter их просто не содержит.
perfscale ловит эту финальную строку и переводит её в тот же k6-совместимый блок summary, в котором отчитываются остальные движки:
summary = 100 in 00:00:01 = 109.2/s Avg: 4 Min: 3 Max: 72 Err: 0 (0.00%)
http_reqs..............: 100 109.2/s
http_req_failed........: 0.00% ✓ 0 ✗ 100
http_req_duration......: avg=4.00ms min=3.00ms max=72.00ms
Это реальный вывод демо-прогона: план с 5 потоками × 20 циклами против локального HTTP-сервера — 100 сэмплов со скоростью 109.2/с, ноль ошибок. С точки зрения CI-джоба JMeter-прогон теперь заканчивается summary той же формы, что и k6-прогон или нативный YAML-прогон, так что ваш парсинг логов и отчётность работают для обоих.
Про перцентили
Будем честны про ограничение: http_req_duration из JMeter-прогона содержит только avg/min/max. Если вам нужны p95/p99 для SLO-гейтов, консольное summary их не даст — эти данные просто не покидают JMeter. Поддерживаемые пути — собственные механизмы JMeter:
- Добавьте в план Backend Listener (InfluxDB, Graphite) и считайте перцентили по получившемуся временному ряду.
- Сгенерируйте HTML-отчёт JMeter (
-e -o <dir>при самостоятельном запускеjmeter) — в его таблице статистики есть p90/p95/p99 по каждому сэмплеру.
Summary в perfscale остаётся avg/min/max, и мы предпочитаем сказать об этом заранее, а не рисовать несуществующую точность.
Минимальный план для пробы
Вот урезанная версия демо-плана, давшего цифры выше: одна thread-группа, один HTTP-сэмплер, один ассерт статуса:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="demo" enabled="true">
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="users" enabled="true">
<stringProp name="ThreadGroup.num_threads">${__P(vus,5)}</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">${__P(loops,20)}</stringProp>
</elementProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="GET /" enabled="true">
<stringProp name="HTTPSampler.domain">${__P(host,127.0.0.1)}</stringProp>
<stringProp name="HTTPSampler.port">${__P(port,8080)}</stringProp>
<stringProp name="HTTPSampler.protocol">http</stringProp>
<stringProp name="HTTPSampler.path">/</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
</HTTPSamplerProxy>
<hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="status is 200" enabled="true">
<collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp>
</collectionProp>
<stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
<intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion>
<hashTree/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>
Обратите внимание на дефолты в ${__P(...)}: план запускается как есть в GUI JMeter, а те же свойства вы переопределяете из обёрточного скрипта в CI, когда нужна другая форма нагрузки. perfscale сознательно не вмешивается в эту схему — один план, параметризованный одним способом, работает в обоих мирах.
Ещё один нюанс про exit code: JMeter завершается с ненулевым кодом при ошибках плана или запуска, но упавшие сэмплы внутри плана exit code не меняют. Если CI должен падать на проваленных ассертах — гейтите по распарсенному summary (например, http_req_failed), а не по $?.
На платформе
Те же планы запускаются и на controlplane perfscale. Создайте тест типа JMeter, вставьте XML плана — он хранится вместе с тестом — и запускайте на любом из ваших агентов. Агент выполняет его через свой endpoint POST /api/v1/run/jmeter, логи стримятся вживую на страницу прогона, а переведённое summary попадает в тот же дашборд и пайплайн метрик, что и k6-прогоны. Один дашборд для всех движков, без отдельного «JMeter-угла».
Агентам нужен установленный JMeter — как и CLI; dev docker-образ поставляется с JRE и JMeter 5.6.3, так что контейнерные агенты работают из коробки.
Попробовать
python3 -m http.server 8080 &
perfscale run --jmeter plan.jmx # план из примера выше
Если интересно, как именно устроена трансляция summary — реализация раннера в runner/jmeter.rs в OSS-репозитории. Ваши планы, ваша форма нагрузки — просто теперь вокруг них есть пайплайн.
