Туториалы28 августа 2026 г. · 7 мин

Запуск существующих JMeter-планов с PerfScale

Уже есть .jmx-планы? Показываем, как запускать их headless через CLI perfscale или на агентах платформы — живые логи, summary в формате k6 и честный разговор про перцентили.

Автор: Команда Perfscale

Запуск существующих JMeter-планов с PerfScale

Если у вашей команды есть папка с .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-репозитории. Ваши планы, ваша форма нагрузки — просто теперь вокруг них есть пайплайн.