極力頭を使わないで負荷テストをやる

# はじめに

通常、負荷テストをやろう、となった時に初心者には敷居が高いと思ってます。
そもそもツールの種類も多いし、それぞれお作法が異なるし、場合によってはプログラムも書かないといけず中々考えることが多いと思います。

なので今回はプログラムを書かず、GUIの操作のみで負荷テストツールを用意しました（負荷テスト内容の構築はGUIからで、実行はCLIで行います。CLIを使うといってもコマンド一発です）。具体的にはJmeterを使ってます。

負荷テストのテンプレートを用意したので、それをご自身の環境に入れて設定を見たり、動かしてみながらイメージを掴んで実際にやりたいシナリオを作るのに役立てていただければなと思っております。

色々事前に勉強するよりも、まず動かした方が早いかなという思いから細かい説明は極力省いています。


# 手順
M1 Mac(Ventura 13.3)で作業しています。

## インストールと起動

```bash
brew install jmeter
jmeter -v     
WARNING: package sun.awt.X11 not in java.desktop
    _    ____   _    ____ _   _ _____       _ __  __ _____ _____ _____ ____
   / \  |  _ \ / \  / ___| | | | ____|     | |  \/  | ____|_   _| ____|  _ \
  / _ \ | |_) / _ \| |   | |_| |  _|    _  | | |\/| |  _|   | | |  _| | |_) |
 / ___ \|  __/ ___ \ |___|  _  | |___  | |_| | |  | | |___  | | | |___|  _ <
/_/   \_\_| /_/   \_\____|_| |_|_____|  \___/|_|  |_|_____| |_| |_____|_| \_\ 5.5

Copyright (c) 1999-2022 The Apache Software Foundation
```

:::note
`WARNING: package sun.awt.X11 not in java.desktop`と警告が出ていますが、Jmeterのバージョンに対して私の環境のJavaのバージョンが新しいため警告が出ています。このパッケージはGUIに使われるものであり、負荷テストの実行自体はCLIで行うためJmeterの動作に影響がないので無視して構いません。
:::

**起動**
```bash
jmeter
```
こんな感じでデスクトップアプリケーションが立ち上がります
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/7534d933-9972-508d-fafe-f27d3ff3d4a5.png" width=700>

## .jmxファイルを開く
Jmeterが起動したら、そこからポチポチ設定をイジって負荷テストの内容（どこにどんなメソッドで何秒ごとにどんな内容を送るか等）を設定していくわけですが、今回はとりあえずサクッと始められるようにテンプレートを用意してみましたのでこれを使います。まずは動かしてみて、好みで修正していった方が理解も早まると思います。

用意したテンプレートの概要です。Jmeterは機能が多くかなり複雑なことができますが、今回はシンプルなものにしています。これをベースに自分がやりたいシナリオに近づけると便利かと思います。

- ３パターンのテスト
    - デバイス2種類からの送信
        - HTTP, POST
        - MQTT
    - ユーザーからのアクセス
        - HTTP, GET
- 設定値の一元管理
    - Jmeterでは送信頻度や送信先の設定を各スレッドグループ（テストの単位みたいなもの）ごとに設定するようになっていますが、送信先のURLやヘッダーの内容は共通で良くて個別に同じ内容を設定する意味がなかったり、負荷状況を見ながら設定変更することを考えると一箇所にまとまっていないと面倒なので、内容ごとに管理できるように構築しました
- １回目のインターバルのスキップ
    - 送信頻度の設定を行なった場合、最初の１回目のリクエストは設定値分待った後に行われますが１回目に関しては待つ必要もないケースは結構あると思いましたので一回目だけ待つのをスキップしてすぐ送るようにしました


デバイスからの送信とユーザーからのアクセスを同時にテストしないと思いますので現実に側していないと思いますがサンプルとして用意しました。

まずは、以下のTestPlanTemplate.jmxをコピペして、ローカルに用意してください。

```xml:TestPlanTemplate.jmx
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="TestPlanTemplate" enabled="true">
      <stringProp name="TestPlan.comments">テストプランのテンプレートです
</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTPリクエストのデフォルト値" enabled="true">
        <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
          <collectionProp name="Arguments.arguments"/>
        </elementProp>
        <stringProp name="HTTPSampler.domain">example.com</stringProp>
        <stringProp name="HTTPSampler.port"></stringProp>
        <stringProp name="HTTPSampler.protocol">https</stringProp>
        <stringProp name="HTTPSampler.contentEncoding"></stringProp>
        <stringProp name="HTTPSampler.path"></stringProp>
        <stringProp name="TestPlan.comments">上書きしない限り、全てのリクエストでこの値が使われます</stringProp>
        <stringProp name="HTTPSampler.concurrentPool">6</stringProp>
        <stringProp name="HTTPSampler.connect_timeout"></stringProp>
        <stringProp name="HTTPSampler.response_timeout"></stringProp>
      </ConfigTestElement>
      <hashTree/>
      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTPヘッダーのデフォルト値" enabled="true">
        <collectionProp name="HeaderManager.headers">
          <elementProp name="" elementType="Header">
            <stringProp name="Header.name">Content-type</stringProp>
            <stringProp name="Header.value">application/json</stringProp>
          </elementProp>
        </collectionProp>
        <stringProp name="TestPlan.comments">上書きしない限り、全てのリクエストでこの値が使われます</stringProp>
      </HeaderManager>
      <hashTree/>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="負荷テスト共通の設定値" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="TEST_DURATION_SEC" elementType="Argument">
            <stringProp name="Argument.name">TEST_DURATION_SEC</stringProp>
            <stringProp name="Argument.value">20</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
            <stringProp name="Argument.desc">テストを行うトータルの期間（秒）</stringProp>
          </elementProp>
          <elementProp name="BASE_DEVICE_PATH" elementType="Argument">
            <stringProp name="Argument.name">BASE_DEVICE_PATH</stringProp>
            <stringProp name="Argument.value">/anything/devices</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
            <stringProp name="Argument.desc">https://example.com + /anything/devices</stringProp>
          </elementProp>
          <elementProp name="BASE_USER_PATH" elementType="Argument">
            <stringProp name="Argument.name">BASE_USER_PATH</stringProp>
            <stringProp name="Argument.value">/anything/users</stringProp>
            <stringProp name="Argument.desc">https://example.com + /anything/users</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
        <stringProp name="TestPlan.comments">共通で使い回す設定値をここに集めてます。</stringProp>
      </Arguments>
      <hashTree/>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="デバイス数" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="DEVICE1_NUM_OF_DEVICES" elementType="Argument">
            <stringProp name="Argument.name">DEVICE1_NUM_OF_DEVICES</stringProp>
            <stringProp name="Argument.value">10</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="DEVICE2_NUM_OF_DEVICES" elementType="Argument">
            <stringProp name="Argument.name">DEVICE2_NUM_OF_DEVICES</stringProp>
            <stringProp name="Argument.value">20</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
      </Arguments>
      <hashTree/>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="ユーザー数" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="NUM_OF_USERS" elementType="Argument">
            <stringProp name="Argument.name">NUM_OF_USERS</stringProp>
            <stringProp name="Argument.value">100</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
      </Arguments>
      <hashTree/>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="スタートタイミング" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="DEVICE1_STARTUP_DELAY_SEC" elementType="Argument">
            <stringProp name="Argument.name">DEVICE1_STARTUP_DELAY_SEC</stringProp>
            <stringProp name="Argument.value">0</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="DEVICE2_STARTUP_DELAY_SEC" elementType="Argument">
            <stringProp name="Argument.name">DEVICE2_STARTUP_DELAY_SEC</stringProp>
            <stringProp name="Argument.value">2</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="USER_START_UP_DELAY_SEC" elementType="Argument">
            <stringProp name="Argument.name">USER_START_UP_DELAY_SEC</stringProp>
            <stringProp name="Argument.value">4</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
        <stringProp name="TestPlan.comments">実行してから何秒後に起動されるか（各テストごとにタイミングをずらしたい時に）</stringProp>
      </Arguments>
      <hashTree/>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="デバイス数/ユーザー数に到達するまでにかける時間" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="DEVICE1_RAMPUP_TIME_SEC" elementType="Argument">
            <stringProp name="Argument.name">DEVICE1_RAMPUP_TIME_SEC</stringProp>
            <stringProp name="Argument.value">1</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="DEVICE2_RAMPUP_TIME_SEC" elementType="Argument">
            <stringProp name="Argument.name">DEVICE2_RAMPUP_TIME_SEC</stringProp>
            <stringProp name="Argument.value">2</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="USER_RAMPUP_TIME_SEC" elementType="Argument">
            <stringProp name="Argument.name">USER_RAMPUP_TIME_SEC</stringProp>
            <stringProp name="Argument.value">3</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
        <stringProp name="TestPlan.comments">「デバイス数」と「ユーザー数」に設定された数に到達するまでの時間(秒)</stringProp>
      </Arguments>
      <hashTree/>
      <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="リクエストのインターバル（ミリ秒）" enabled="true">
        <collectionProp name="Arguments.arguments">
          <elementProp name="DEVICE1_EVENT_INTERVAL_MS" elementType="Argument">
            <stringProp name="Argument.name">DEVICE1_EVENT_INTERVAL_MS</stringProp>
            <stringProp name="Argument.value">50000</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="DEVICE2_EVENT_INTERVAL_MS" elementType="Argument">
            <stringProp name="Argument.name">DEVICE2_EVENT_INTERVAL_MS</stringProp>
            <stringProp name="Argument.value">70000</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
          <elementProp name="USER_EVENT_INTERVAL_MS" elementType="Argument">
            <stringProp name="Argument.name">USER_EVENT_INTERVAL_MS</stringProp>
            <stringProp name="Argument.value">100000</stringProp>
            <stringProp name="Argument.metadata">=</stringProp>
          </elementProp>
        </collectionProp>
      </Arguments>
      <hashTree/>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Device1" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">${DEVICE1_NUM_OF_DEVICES}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">${DEVICE1_RAMPUP_TIME_SEC}</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${TEST_DURATION_SEC}</stringProp>
        <stringProp name="ThreadGroup.delay">${DEVICE1_STARTUP_DELAY_SEC}</stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
      </ThreadGroup>
      <hashTree>
        <BeanShellTimer guiclass="TestBeanGUI" testclass="BeanShellTimer" testname="BeanShell Timer" enabled="true">
          <boolProp name="resetInterpreter">false</boolProp>
          <stringProp name="parameters"></stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="script">int sensorEventIntervalMs = Integer.parseInt(vars.get(&quot;DEVICE1_EVENT_INTERVAL_MS&quot;));
String threadGroupName = ctx.getThreadGroup().getName();
String loopCounterVarName = &quot;__jm__&quot; + threadGroupName + &quot;__idx&quot;;
int loopCounter = Integer.parseInt(vars.get(loopCounterVarName));
int interval = 0;

if (loopCounter &gt; 0) { // No delay for the 1st iteration
    interval = sensorEventIntervalMs;
}

return interval;
</stringProp>
        </BeanShellTimer>
        <hashTree/>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Device1 Request" enabled="true">
          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">{&#xd;
  &quot;sensor_id&quot;: &quot;${__counter(FALSE,)}&quot;,&#xd;
  &quot;sensor_name&quot;: &quot;${__RandomString(10,abcdefghijklmnopqrstuvwxyz0123456789)}&quot;&#xd;
}</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">${BASE_DEVICE_PATH}/device1/message</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
            <boolProp name="ResultCollector.error_logging">false</boolProp>
            <objProp>
              <name>saveConfig</name>
              <value class="SampleSaveConfiguration">
                <time>true</time>
                <latency>true</latency>
                <timestamp>true</timestamp>
                <success>true</success>
                <label>true</label>
                <code>true</code>
                <message>true</message>
                <threadName>true</threadName>
                <dataType>true</dataType>
                <encoding>false</encoding>
                <assertions>true</assertions>
                <subresults>true</subresults>
                <responseData>false</responseData>
                <samplerData>false</samplerData>
                <xml>false</xml>
                <fieldNames>true</fieldNames>
                <responseHeaders>false</responseHeaders>
                <requestHeaders>false</requestHeaders>
                <responseDataOnError>false</responseDataOnError>
                <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
                <assertionsResultsToSave>0</assertionsResultsToSave>
                <bytes>true</bytes>
                <sentBytes>true</sentBytes>
                <url>true</url>
                <threadCounts>true</threadCounts>
                <idleTime>true</idleTime>
                <connectTime>true</connectTime>
              </value>
            </objProp>
            <stringProp name="filename"></stringProp>
          </ResultCollector>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Users" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">${NUM_OF_USERS}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">${USER_RAMPUP_TIME_SEC}</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${TEST_DURATION_SEC}</stringProp>
        <stringProp name="ThreadGroup.delay">${USER_STARTUP_DELAY_SEC}</stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
      </ThreadGroup>
      <hashTree>
        <BeanShellTimer guiclass="TestBeanGUI" testclass="BeanShellTimer" testname="BeanShell Timer" enabled="true">
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters"></stringProp>
          <boolProp name="resetInterpreter">false</boolProp>
          <stringProp name="script">int sensorEventIntervalMs = Integer.parseInt(vars.get(&quot;USER_EVENT_INTERVAL_MS&quot;));
String threadGroupName = ctx.getThreadGroup().getName();
String loopCounterVarName = &quot;__jm__&quot; + threadGroupName + &quot;__idx&quot;;
int loopCounter = Integer.parseInt(vars.get(loopCounterVarName));
int interval = 0;

if (loopCounter &gt; 0) { // No delay for the 1st iteration
    interval = sensorEventIntervalMs;
}

return interval;
</stringProp>
        </BeanShellTimer>
        <hashTree/>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="User Request" enabled="true">
          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <boolProp name="HTTPArgument.always_encode">false</boolProp>
                <stringProp name="Argument.value">{&#xd;
  &quot;user_id&quot;: &quot;${__counter(FALSE,)}&quot;,&#xd;
  &quot;user_name&quot;: &quot;${__RandomString(10,abcdefghijklmnopqrstuvwxyz0123456789)}&quot;&#xd;
}&#xd;
&#xd;
</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain"></stringProp>
          <stringProp name="HTTPSampler.port"></stringProp>
          <stringProp name="HTTPSampler.protocol"></stringProp>
          <stringProp name="HTTPSampler.contentEncoding"></stringProp>
          <stringProp name="HTTPSampler.path">${BASE_USER_PATH}</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
          <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
          <stringProp name="HTTPSampler.connect_timeout"></stringProp>
          <stringProp name="HTTPSampler.response_timeout"></stringProp>
        </HTTPSamplerProxy>
        <hashTree>
          <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
            <boolProp name="ResultCollector.error_logging">false</boolProp>
            <objProp>
              <name>saveConfig</name>
              <value class="SampleSaveConfiguration">
                <time>true</time>
                <latency>true</latency>
                <timestamp>true</timestamp>
                <success>true</success>
                <label>true</label>
                <code>true</code>
                <message>true</message>
                <threadName>true</threadName>
                <dataType>true</dataType>
                <encoding>false</encoding>
                <assertions>true</assertions>
                <subresults>true</subresults>
                <responseData>false</responseData>
                <samplerData>false</samplerData>
                <xml>false</xml>
                <fieldNames>true</fieldNames>
                <responseHeaders>false</responseHeaders>
                <requestHeaders>false</requestHeaders>
                <responseDataOnError>false</responseDataOnError>
                <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
                <assertionsResultsToSave>0</assertionsResultsToSave>
                <bytes>true</bytes>
                <sentBytes>true</sentBytes>
                <url>true</url>
                <threadCounts>true</threadCounts>
                <idleTime>true</idleTime>
                <connectTime>true</connectTime>
              </value>
            </objProp>
            <stringProp name="filename"></stringProp>
          </ResultCollector>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Device2" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">${DEVICE2_NUM_OF_DEVICES}</stringProp>
        <stringProp name="ThreadGroup.ramp_time">${DEVICE2_RAMPUP_TIME_SEC}</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">${TEST_DURATION_SEC}</stringProp>
        <stringProp name="ThreadGroup.delay">${DEVICE2_STARTUP_DELAY_SEC}</stringProp>
        <boolProp name="ThreadGroup.same_user_on_next_iteration">false</boolProp>
      </ThreadGroup>
      <hashTree>
        <BeanShellTimer guiclass="TestBeanGUI" testclass="BeanShellTimer" testname="BeanShell Timer" enabled="true">
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters"></stringProp>
          <boolProp name="resetInterpreter">false</boolProp>
          <stringProp name="script">int sensorEventIntervalMs = Integer.parseInt(vars.get(&quot;DEVICE2_EVENT_INTERVAL_MS&quot;));
String threadGroupName = ctx.getThreadGroup().getName();
String loopCounterVarName = &quot;__jm__&quot; + threadGroupName + &quot;__idx&quot;;
int loopCounter = Integer.parseInt(vars.get(loopCounterVarName));
int interval = 0;

if (loopCounter &gt; 0) { // No delay for the 1st iteration
    interval = sensorEventIntervalMs;
}

return interval;
</stringProp>
        </BeanShellTimer>
        <hashTree/>
        <net.xmeter.samplers.ConnectSampler guiclass="net.xmeter.gui.ConnectSamplerUI" testclass="net.xmeter.samplers.ConnectSampler" testname="MQTT Connect" enabled="true">
          <stringProp name="mqtt.server">examplemq.com</stringProp>
          <stringProp name="mqtt.port">1883</stringProp>
          <stringProp name="mqtt.version">3.1</stringProp>
          <stringProp name="mqtt.conn_timeout">10</stringProp>
          <stringProp name="mqtt.protocol"></stringProp>
          <stringProp name="mqtt.ws_path"></stringProp>
          <boolProp name="mqtt.dual_ssl_authentication">false</boolProp>
          <stringProp name="mqtt.clientcert_file_path"></stringProp>
          <stringProp name="mqtt.clientcert_password"></stringProp>
          <stringProp name="mqtt.user_name"></stringProp>
          <stringProp name="mqtt.password"></stringProp>
          <stringProp name="mqtt.client_id_prefix">conn_</stringProp>
          <boolProp name="mqtt.client_id_suffix">true</boolProp>
          <stringProp name="mqtt.conn_keep_alive">300</stringProp>
          <stringProp name="mqtt.conn_attampt_max">0</stringProp>
          <stringProp name="mqtt.reconn_attampt_max">0</stringProp>
          <stringProp name="mqtt.conn_clean_session">true</stringProp>
        </net.xmeter.samplers.ConnectSampler>
        <hashTree/>
        <net.xmeter.samplers.PubSampler guiclass="net.xmeter.gui.PubSamplerUI" testclass="net.xmeter.samplers.PubSampler" testname="Device 2 Publish" enabled="true">
          <stringProp name="mqtt.topic_name">/sensor_device2</stringProp>
          <stringProp name="mqtt.qos_level">0</stringProp>
          <boolProp name="mqtt.add_timestamp">false</boolProp>
          <stringProp name="mqtt.message_type">String</stringProp>
          <stringProp name="mqtt.message_type_fixed_length">1024</stringProp>
          <stringProp name="mqtt.message_to_sent">{
  &quot;type&quot;: &quot;co2&quot;,
  &quot;sensor_id&quot;: &quot;${__counter(FALSE,)}&quot;,
  &quot;sensor_name&quot;: &quot;${__RandomString(10,abcdefghijklmnopqrstuvwxyz0123456789)}&quot;
}
</stringProp>
          <boolProp name="mqtt.retained_message">false</boolProp>
        </net.xmeter.samplers.PubSampler>
        <hashTree>
          <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="false">
            <boolProp name="ResultCollector.error_logging">false</boolProp>
            <objProp>
              <name>saveConfig</name>
              <value class="SampleSaveConfiguration">
                <time>true</time>
                <latency>true</latency>
                <timestamp>true</timestamp>
                <success>true</success>
                <label>true</label>
                <code>true</code>
                <message>true</message>
                <threadName>true</threadName>
                <dataType>true</dataType>
                <encoding>false</encoding>
                <assertions>true</assertions>
                <subresults>true</subresults>
                <responseData>false</responseData>
                <samplerData>false</samplerData>
                <xml>false</xml>
                <fieldNames>true</fieldNames>
                <responseHeaders>false</responseHeaders>
                <requestHeaders>false</requestHeaders>
                <responseDataOnError>false</responseDataOnError>
                <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
                <assertionsResultsToSave>0</assertionsResultsToSave>
                <bytes>true</bytes>
                <sentBytes>true</sentBytes>
                <url>true</url>
                <threadCounts>true</threadCounts>
                <idleTime>true</idleTime>
                <connectTime>true</connectTime>
              </value>
            </objProp>
            <stringProp name="filename"></stringProp>
          </ResultCollector>
          <hashTree/>
        </hashTree>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>
```

Jmeterの`Open`ボタンから`TestPlanTemplate.jmx`を開きます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/67e39b51-1086-e22e-9a35-ee353690a8e2.png width=600>

こんな内容で.jmxファイルの内容が読み込まれます。
![Screenshot 2023-05-08 at 14.37.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/a00b6efb-7681-d2eb-34c2-ab938e4641db.png)

JmeterはMQTTプロトコルをデフォルトではサポートしていません。Jmeterにはプラグインという拡張機能があり、プラグインをインストールすることでMQTTに対応することができます。
![Screenshot 2023-05-08 at 14.47.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/2d8b0e29-083a-96e3-934e-b26ea6e72979.png)
ご自身の環境に`MQTT Protocol Samplers`というプラグインがインストールされていない場合は以下の画像のようにインストールが促されますのでインストールしてください。

:::note
Jmeterで設定した内容は.jmxとして自動的に保存されますので、これを読み込めば利用できます。自分で作った内容を人に共有するのは楽です。

注意点としては外部ファイルを使っている場合です。例えばJmeterはCSVファイルの内容をリクエスト時のボディの内容に含めるように設定できたりしますがその内容までは.jmxファイルに反映されていないので別途渡す必要があります。

.jmxに記述があり、読み込まれた環境に存在しないプラグインがある場合は別途インストールしてもらう必要があります。
:::

## テンプレートの解説

赤枠の部分が設定値です。
![config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/a3566457-8f97-673f-c89b-8dc8dc0b6f22.png)

それぞれ`Config Element（設定エレメント）`という機能を使っています。ご自身で追加したい場合は`TestPlanTemplate`を右クリック-> `追加` -> `設定エレメント`から追加できます。

|このテンプレでの名前|Config Element名|機能|
|-|-|-|
|HTTPリクエストのデフォルト値|HTTP Request Defaults（HTTPリクエスト初期設定）|HTTPリクエストのデフォルト値、個別に上書きしない限りHTTPリクエストではここの設定が使われる|
|HTTPヘッダーのデフォルト値|HTTP Header Manager（HTTPヘッダマネージャ）|HTTPヘッダーのデフォルト値、個別に上書きしない限りHTTPリクエストではここの設定が使われる|
|※その他全て|User Defined Variables（ユーザー定義変数）|任意の変数を定義でき、`${変数名}`で呼び出すことができる ※1|

> ※1 実際に`Device1` `Device2` `Users`の設定を見ていただくと`${変数名}`で設定値を呼び出していることが確認いただけます

設定値の下の部分が各テストを表しています。デバイスからの送信が2種類と、ユーザーからのアクセスという内容と前述しましたが、それぞれ`Device1` `Device2`と`Users`が対応してます。
![tests.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/1b1a6321-2b91-c11f-f9f2-02dc424482d2.png)

似たような構成になっていることが画像からわかると思います。`BeanShell Timer` `xxx Request` `View Results Tree`で構成されています。Device2はMQTTを使うのでRequestではなくConnectとPublishとなっています。それぞれ解説していきます。


### Thread Group
まず、ここでDevice1やUsersと書いてある部分はJmeterでは`Thread Group`というコンポーネントです。ここでは以下の設定が可能です。

|設定名|内容|
|-|-|
|Number of Threads（スレッド数）|今回の場合はデバイス数とユーザー数。ユーザーからのアクセスというていでテストする場合、具体的に何ユーザーのアクセスとするかの設定が必要かと思いますがこの項目が該当します。|
|Ramp-up Period（Ramp-Up期間（秒））|何秒かけて`スレッド数`の値に到達させるか。テスト開始後にいきなりスレッド数に設定した数のリクエストが実行されるわけではなく、Ramp-Up期間に設定した秒数をかけて全ての数が揃います。|
|Loop Count（ループ回数）|各スレッドごとの実行回数。無限ループにもできます。|
|Duration（持続時間（秒））|テストを実行する秒数。ループ回数を無限にした場合、この持続時間の間ループし続けることになります。|
|Startup Delay（起動遅延（秒））|テストを実行してから何秒後にThread Groupを実行するかです。|

### Thread Group配下の要素
#### BeanShell Timer
いくつかあるタイマー機能の一つです。送信頻度の設定に使っています。
BeanShell（Javaっぽいスクリプト言語）を書くことができるので柔軟な条件を書くことができます。
このテンプレートでは１回目のループではインターバル期間を0とし、２回目以降はUser Defined Variablesで用意した`リクエストのインターバル（ミリ秒）`に設定された変数の値分待ち時間を発生させるようにしています。

```java
// Device1のインターバルの設定値を取得
int sensorEventIntervalMs = Integer.parseInt(vars.get("DEVICE1_EVENT_INTERVAL_MS"));
// ThreadGroup名を取得
String threadGroupName = ctx.getThreadGroup().getName();
// そのThreadGroupのループのインデックスの変数を取得
String loopCounterVarName = "__jm__" + threadGroupName + "__idx";
// 現在インデックスを取得
int loopCounter = Integer.parseInt(vars.get(loopCounterVarName));
int interval = 0;
// ループが２回目以降なら設定値をインターバル期間として設定
if (loopCounter > 0) { // No delay for the 1st iteration
    interval = sensorEventIntervalMs;
}

return interval;
```

:::note
他のタイマーを使いたい場合は`[ThreadGroup名]を右クリック` -> `追加` -> `タイマ`から追加できます。
:::

#### HTTP Request（テンプレ上の名前はDevice1 Request, Device2 Request）
`Sampler`と呼ばれるコンポーネントの一つで、名前の通りHTTPリクエストを行うことができます。
HTTP以外にもKafka、FTP、SMTP、JDBCなど様々な種類があります。

HTTPリクエストに必要な、
- プロトコル
- ドメイン or IP
- ポート
- メソッド
- パス
- クエリパラメータ
- ボディ
などを設定できます。今回は、`HTTP Request Defaults`でプロトコルとドメインを、`HTTP Header Manager`でヘッダー情報を既に設定していますので、HTTP Requestサンプラーでは、メソッド、パス、ボディを設定しています。

こちらはDevice1の設定です。
![Screenshot 2023-05-09 at 11.23.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/785f9bd2-5b7d-59de-ee58-b2cab433cd3b.png)

ボディは`sensor_id`と`sensor_name`というキーを持ち、sensor_idは連番の数字が、sensor_nameには10文字のランダムな文字列が入ります。
```json
{
  "sensor_id": "${__counter(FALSE,)}",
  "sensor_name": "${__RandomString(10,abcdefghijklmnopqrstuvwxyz0123456789)}"
}
```

:::note
他のサンプラーを使いたい場合は`[ThreadGroup名]を右クリック` -> `追加` -> `サンプラー`から追加できます。
:::

#### View Results Tree

`Listener`と呼ばれるコンポーネントの一つで、Samplerの結果を表示したり統計したりする機能です。
今回はシンプルにリクエストやパブリッシュの結果がわかれば良いので、`View Results Tree（結果をツリーで表示）`を選定してます。

そもそもどういうリクエストを行なったかや、
![Screenshot 2023-05-09 at 11.36.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/166c07c6-6b30-3f37-2467-9a789d925aea.png)

どんなレスポンスが返ったかなどがわかります。
![Screenshot 2023-05-09 at 11.38.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/bc42a519-0fd1-1ed2-3631-9eb37b182158.png)

#### Thread Group配下の要素まとめ
![Screenshot 2023-05-09 at 11.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/fd989e9e-2708-0398-5603-e1bdbaae78e8.png)
この流れは単純で、BeanShell Timerで待ち、Device1 Requestでリクエストを実行、View Results Treeで結果を見る、というだけです。

他の２つのThread Groupも流れは同じなので同じなので見てみて下さい。

## 動かし方

現状送信先に仮の値が入っているので動かす前に送信先をご自身の任意のサーバー・ブローカーに変更して下さい。
**HTTPリクエスト**
`HTTPリクエストのデフォルト値`にて設定
![Screenshot 2023-05-10 at 18.17.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/fc2efd7f-b817-1e22-c553-51aafdabe3d7.png)
**MQTTブローカーへのパブリッシュ**
`Device2`の`MQTT Connect`にて設定
![Screenshot 2023-05-10 at 18.17.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/fdde8def-ff46-2396-605d-76bf3ecf6a79.png)


JmeterはGUIで動かす方法とCLIで動かす方法があります。

jmeterコマンドでJmeterを起動した際に以下のメッセージが表示されたと思います。
```bash
================================================================================
Don't use GUI mode for load testing !, only for Test creation and Test debugging.
For load testing, use CLI Mode (was NON GUI):
   jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]
& increase Java Heap to meet your test requirements:
   Modify current env variable HEAP="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m" in the jmeter batch file
Check : https://jmeter.apache.org/usermanual/best-practices.html
================================================================================
```
`Don't use GUI mode for load testing`とある通りGUIはテスト作成とデバッグ用に使い、実際の負荷テストはここにある通り、`jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]`というコマンドでCLIモードで実行します。

GUIで動かす場合は、赤枠のボタンをクリックすればOKです。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/d15a9390-65c1-cee0-89a4-fc3846ba409a.png width=400 >

実際に負荷テストを行う際はCLIで実行することになりますが、実行前にやることがあります。可能な限りオーバーヘッドを減らすため不要なコンポーネントを無効にします。Jmeterの各要素はそれぞれ有効・無効を設定することができます。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/5aad4bcc-1b76-5ce9-a7c5-be6f30ae5a03.png width=400 >
`View Results Tree`は動作確認のために入れていたので3つとも無効にします。
> このテストではデバイス２つ、ユーザー1つですが無効にすれば特定の内容だけ実行することができます。

それでは実行です。任意のパスに書き換えて実行して下さい。
```bash
jmeter -n -t TestPlanTemplate.jmx -l results/result.csv -e -o ./results
```
実行すると、指定したパスに結果のレポートがHTMLファイルで出力されます。

![Screenshot 2023-05-09 at 13.55.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556201/a4c96d7b-f8e7-3ad4-ff74-9ae74f4c9acf.png)



# 細かい仕様
Jmeterを触り始めた時、仕様がよくわからなかった箇所があったのでここにまとめます。

#### Q: Ramp-Up期間内に全てのスレッドの作成が完了するのか？
A: スレッド数分のスレッドを作成を開始ししきるのであって完了するわけではない

#### Q: ループ回数を指定した場合、ループするごとにRamp-Up期間が発生するのか？
A: スレッドの起動は１回だけで２回目以降はスレッドを使い回すのでRamp-Up期間はループごとには発生しない

#### Q: ループは何の単位でループするのか
A: スレッド単位でループするため前スレッドが同じタイミングでループするわけではない

#### Q: 結局、全スレッドが揃った状態でテストできる期間はどうやって決まる？

`持続期間 - 起動遅延（秒）- Ramp-Up期間（秒） = 全スレッドが揃ったテスト期間`


以上です。

確認したいコンポーネントだけ有効にして動かしたり、Samplerを追加してみたりするとよりイメージがつかめるかと思います。皆さんの負荷テストの一歩目の役に立てれば光栄です。