Overview

Jmeter Master-Slave Overview

まずJmeterとは何ですか?については以下のとおりです。

JMeter はJakarta プロジェクト で開発が進められている、パフォーマンス計測用のJavaアプリケーションです。元々はWebアプリケーションのテストのために作成されたもので、さまざまなWebアプリケーションをテストする機能を持っています。
参考:1. JMeterの基本 | TECHSCORE(テックスコア)

軽い負荷をかけて、ちょっと動作を見てみたいなレベルであれば、Apache Bench で良いかと思います。参考:Apache Benchでサクッと性能テスト - Qiita

強い負荷をかけて、動作をちゃんと確認したいなレベルであれば、Apache JMeter が選択肢に挙がってきます。

参考までに、他の手段としては、単に複数マシンに ssh で入って、一斉に負荷掛けツールを実行っていうのも選択肢に挙がるかなとは思います。

では、以下のような目次で説明しておこうと思います。

1. Jmeter Setting(ex.. how to make jmx)

まず、JMXの作成方法、そして、Jmeterの設定(Master&Slave側の設定 と Master側だけの設定 と Slave側だけの設定)について説明します。

1.1. How to make JMX

以下の図のようにJmeter上で設定し、Save Test Plan as 等で保存すると、JMXファイルができる。

1.1.1. Advanced Setting

実行時に動的にスレッド数やRamp-Up期間(秒)、ループ回数、HTTPのリクエスト先等を設定したくなるかと思います。

その場合は以下のような手順で出来ます。

  1. JMX作成時に、各項目欄に ${__P(thread_num)}$__P{ramp_up} 等のように値を設定しておきます。
  2. Jmeterの実行時に、~/jmeter/bin/jmeter -n -t test.jmx -r -Jthread_num=10 -Jramp_up=60 などのようにすればOKです。

NOTE: CLIモードのJmeterのオプションについては、Apache JMeter - User’s Manual: Getting Started を参考にしてください。

2. Master-Slave Setting

2.1. Jmeter Master & Slave’s Settings

2.1.1. File Placement

Master側マシンとSlave側マシンで、
Jmeterで利用するファイルは、同じフォルダ構成 にして配置する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[centos@ip-xx-xx-xx-xx ~]$ pwd
/home/centos
[centos@ip-xx-xx-xx-xx ~]$ tree -L 2
.
├── test.jmx
├── apache-jmeter-5.1.1.tgz
├── jmeter
│   ├── LICENSE
│   ├── NOTICE
│   ├── README.md
│   ├── bin
│   ├── docs
│   ├── extras
│   ├── lib
│   ├── licenses
│   └── printable_docs
├── jmeter.log
└── rmi_keystore.jks

2.1.2. Heap Memory Setting

以下のファイルの159行目あたりの : "${HEAP:="-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m"}" を設定する。

1
vi ~/jmeter/bin/jmeter

2.1.3. TimeStamp Format Setting

以下のファイルの531行目あたりの #jmeter.save.saveservice.timestamp_format=yyyy/MM/dd HH:mm:ss.SSS のコメントアウトを外す。

1
vi ~/jmeter/bin/jmeter.properties

2.1.4. Generate KeyStore

jmeter-serverとの通信がSSLであるため、 SSL通信で使用するキーおよび証明書(keystore)の作成が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ LANG=C ~/jmeter/bin/create-rmi-keystore.sh
What is your first and last name?
  [Unknown]:  # rmi と入力
What is the name of your organizational unit?
  [Unknown]:  # My unit name と入力
What is the name of your organization?
  [Unknown]:  # My organisation name と入力
What is the name of your City or Locality?
  [Unknown]:  # Your City と入力
What is the name of your State or Province?
  [Unknown]:  # Your State と入力
What is the two-letter country code for this unit?
  [Unknown]:  # XY と入力
Is CN=rmi, OU=My unit name, O=My organisation name, L=Your City, ST=Your State, C=XY correct?
  [no]:  # yes と入力

Enter key password for <rmi>
    (RETURN if same as keystore password): # 未入力のままEnter

Warning:
The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12 which is an industry standard format using "keytool -importkeystore -srckeystore rmi_keystore.jks -destkeystore rmi_keystore.jks -deststoretype pkcs12".
Copy the generated rmi_keystore.jks to jmeter/bin folder or reference it in property 'server.rmi.ssl.keystore.file'

2.2. Jmeter Master’s Settings

2.2.1. Remote Host Settings

Master側のマシンに、Slaveのマシンはこれだよ!って教えるための設定です。
以下のファイルの259行目あたりを remote_hosts=aa.aa.aa.aa:1099,bb.bb.bb.bb:1099 等のように、Slaveで利用するマシンのURLを設定する。

1
vi ~/jmeter/bin/jmeter.properties

2.3. Jmeter Slave’s Settings

2.3.1. jmeter-server Automatic Start Setting

Slaveのマシンが再起動してしまったとしても、jmeter-serverが自動起動されるようにする設定です。
以下のファイルの末尾に、/home/centos/jmeter/bin/jmeter-server & を追加する。

1
sudo vi /etc/rc.d/rc.local

3. N.B 1 SpringBoot Simple RestController

すごく単純ですが、、、JmeterのSlaveマシンからアクセスを受けるためのSpringBootのRestControllerは以下のようにしました。

host:port/test で受けて、リクエストの累計回数をログに出力し、HTTP STATUS OKを返却するだけのコントローラーです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Slf4j
@RestController
public class SimpleRestController {

    AtomicLong count = new AtomicLong(0);

    @RequestMapping(value = "/test")
    public ResponseEntity<String> method1() {

        count.addAndGet(1);
        log.info("request count = {}", count);

        return ResponseEntity.status(HttpStatus.OK).body("ok");
    }
}

4. Execution Result

4.1. Jmeter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[centos@ip-xx-xx-xx-xx ~]$ ~/jmeter/bin/jmeter -n -t ./test.jmx -r
OpenJDK 64-Bit Server VM warning: If the number of processors is expected to increase from one, then you should configure the number of parallel GC threads appropriately using -XX:ParallelGCThreads=N
Creating summariser <summary>
Created the tree successfully using ./Test Plan.jmx
Configuring remote engine: ec2-aa-aa-aa-aa.compute-1.amazonaws.com:1099
Configuring remote engine: ec2-bb-bb-bb-bb.compute-1.amazonaws.com:1099
Configuring remote engine: ec2-cc-cc-cc-cc.compute-1.amazonaws.com:1099
Starting remote engines
Starting the test @ Sat Jun 13 14:10:47 UTC 2020 (1592057447006)
summary =      1 in 00:00:01 =    2.0/s Avg:   344 Min:   344 Max:   344 Err:     0 (0.00%)
Tidying up remote @ Sat Jun 13 14:10:49 UTC 2020 (1592057449322)
summary +      1 in 00:00:00 =    4.8/s Avg:    22 Min:    22 Max:    22 Err:     0 (0.00%) Active: 0 Started: 1 Finished: 2
summary =      2 in 00:00:01 =    2.8/s Avg:   183 Min:    22 Max:   344 Err:     0 (0.00%)
Tidying up remote @ Sat Jun 13 14:10:49 UTC 2020 (1592057449514)
Remote engines have been started
Waiting for possible Shutdown/StopTestNow/HeapDump/ThreadDump message on port 4445
summary +      1 in 00:00:00 =    4.3/s Avg:    13 Min:    13 Max:    13 Err:     0 (0.00%) Active: 0 Started: 2 Finished: 3
summary =      3 in 00:00:01 =    3.2/s Avg:   126 Min:    13 Max:   344 Err:     0 (0.00%)
Tidying up remote @ Sat Jun 13 14:10:49 UTC 2020 (1592057449728)
... end of run
... end of run
... end of run
[centos@ip-xx-xx-xx-xx ~]$ client_loop: send disconnect: Broken pipe

4.2. RestController

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
20:49:30: Executing task 'SimpleRestcontrollerApplication.main()'...

> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE

> Task :SimpleRestcontrollerApplication.main()

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.0.RELEASE)

2020-06-14 20:49:34.580  INFO 43952 --- [           main] c.e.s.SimpleRestcontrollerApplication    : Starting SimpleRestcontrollerApplication on MacBook-Air.local with PID 43952 (/Users/takuto-n/Documents/Basic_study/private_repo/ctrl-key-6bit-mask/java/simple-restcontroller/build/classes/java/main started by takuto-n in /Users/takuto-n/Documents/Basic_study/private_repo/ctrl-key-6bit-mask/java/simple-restcontroller)
2020-06-14 20:49:34.584  INFO 43952 --- [           main] c.e.s.SimpleRestcontrollerApplication    : No active profile set, falling back to default profiles: default
2020-06-14 20:49:36.489  INFO 43952 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-06-14 20:49:36.518  INFO 43952 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-06-14 20:49:36.518  INFO 43952 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.35]
2020-06-14 20:49:36.666  INFO 43952 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-06-14 20:49:36.666  INFO 43952 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1971 ms
2020-06-14 20:49:36.945  INFO 43952 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-14 20:49:37.385  INFO 43952 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-14 20:49:37.405  INFO 43952 --- [           main] c.e.s.SimpleRestcontrollerApplication    : Started SimpleRestcontrollerApplication in 3.904 seconds (JVM running for 4.914)
2020-06-14 20:49:49.705  INFO 43952 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-06-14 20:49:49.707  INFO 43952 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-06-14 20:49:49.722  INFO 43952 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 15 ms
request count = 1
request count = 2
request count = 3

5. FYI Jmeter Customize

こちらは参考までにですが、Jmeterのカスタマイズをする方法も調査してみたので記載しておきます。

どんな場合にするのかですが、単純なHTTPリクエストじゃなくて、以下のような場合にカスタマイズが必要なのかなと思います。

  • 動的にある業務データを作成して投げたい場合
  • kinesisやmq など特定のソフトウェアに対してデータを投入したい場合

5.1. Gradle Setting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
plugins {
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	// Jmeter
	compile 'org.apache.jmeter:ApacheJMeter_core:4.0'

	// Jackson
	compile 'com.fasterxml.jackson.core:jackson-databind:2.11.0'

	// lombok
	compileOnly 'org.projectlombok:lombok:1.18.12'
	annotationProcessor 'org.projectlombok:lombok:1.18.12'
}

test {
	useJUnitPlatform()
}

5.2. PreProcessor Impl and GUI Setting

5.2.1. PreProcessor Impl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.jmeter.gui;

import org.apache.jmeter.processor.PreProcessor;
import org.apache.jmeter.testelement.AbstractTestElement;

public class RsyncPreProcessor extends AbstractTestElement implements PreProcessor {
    @Override
    public void process() {
        // 前処理が必要であれば実施する。
        // getProperty("dataSize"), // GUIで入力したデータサイズ
    }
}

5.2.2. GUI Setting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.example.jmeter.gui;

import org.apache.jmeter.gui.util.VerticalPanel;
import org.apache.jmeter.processor.gui.AbstractPreProcessorGui;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jorphan.gui.JLabeledTextField;

import java.awt.*;

public class RsyncPreProcessorGui extends AbstractPreProcessorGui {

    private JLabeledTextField dataSize;

    public RsyncPreProcessorGui() {
        // データサイズを設定するためのテキストフィールド
        dataSize = new JLabeledTextField("data size(KiB): ");
        // コンポーネントの配置
        setLayout(new BorderLayout(0, 5));
        setBorder(makeBorder());
        add(makeTitlePanel(), BorderLayout.NORTH);
        VerticalPanel mainPanel = new VerticalPanel();
        mainPanel.add(dataSize);
        add(mainPanel, BorderLayout.CENTER);
    }

    @Override
    public String getLabelResource() {
        return null;
    }

    public String getStaticLabel() {
        return "Rsync PreProcessor";
    }

    // JMeter上でこの前処理が追加された時に呼び出される。
    @Override
    public TestElement createTestElement() {
        // 前処理クラスのオブジェクトを生成
        RsyncPreProcessor preProcessor = new RsyncPreProcessor();
        modifyTestElement(preProcessor);
        return preProcessor;
    }

    // GUIの入力値が更新された時に呼び出される。
    @Override
    public void modifyTestElement(TestElement testElement) {
        testElement.clear();
        configureTestElement(testElement);
        // フィールドに入力されているデータサイズを前処理で使用できるようにする。
        testElement.setProperty("dataSize", dataSize.getText());
    }

    // 必須ではないがこのメソッドを実装しておくと再起動時に
    // 保存している設定ファイルから設定値を引き継げる。
    public void configure(TestElement testElement) {
        dataSize.setText(testElement.getPropertyAsString("dataSize"));
        super.configure(testElement);
    }
}

5.3. Sampler Impl and GUI Setting

5.3.1. Sampler Impl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.jmeter.gui;

import com.example.jmeter.resource.RequestResource;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.jmeter.samplers.AbstractSampler;
import org.apache.jmeter.samplers.Entry;
import org.apache.jmeter.samplers.SampleResult;

public class RsyncSampler extends AbstractSampler {

    // GUIで指定したURLを取得する。
    String url = getProperty("destinationUrl").getStringValue();

    ObjectMapper mapper = new ObjectMapper();

    // コマンドを作成
    private String createCommand(String url) {
        StringBuilder sb = new StringBuilder();
        sb.append("curl ")
          .append(url);
        return sb.toString();
    }

    @SneakyThrows
    @Override
    public SampleResult sample(Entry entry) {
        SampleResult result = new SampleResult();

        // 送信コマンドを生成する。
        String command = createCommand(url);

        result.sampleStart();
        try {
            // サンプラー処理を実施する。

            // コマンドを実行する。
            Runtime runtime = Runtime.getRuntime();
            Process process;
            process = runtime.exec(command);
            process.waitFor();

            result.setSuccessful(true);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            result.setSuccessful(false);
        }
        result.sampleEnd();
        return result;
    }
}

5.3.2. GUI Setting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.example.jmeter.gui;

import org.apache.jmeter.gui.util.VerticalPanel;
import org.apache.jmeter.samplers.gui.AbstractSamplerGui;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jorphan.gui.JLabeledTextField;

import javax.swing.*;
import java.awt.*;

public class RsyncSamplerGui extends AbstractSamplerGui {
    private static final long serialVersionUID = 1L;
    private JLabeledTextField destinationUrl;

    public RsyncSamplerGui() {
        // コンポーネントの配置
        setLayout(new BorderLayout(0, 5));
        setBorder(makeBorder());
        add(makeTitlePanel(), BorderLayout.NORTH);
        VerticalPanel mainPanel = new VerticalPanel();
        mainPanel.add(createDestinationPanel());
        add(mainPanel, BorderLayout.CENTER);
    }

    private Component createDestinationPanel() {
        // 負荷掛け対象である送信先のURL
        destinationUrl = new JLabeledTextField("url: ");

        JPanel dataPanel = new VerticalPanel();
        dataPanel.setBorder(BorderFactory.createTitledBorder("destination"));
        dataPanel.add(destinationUrl);
        return dataPanel;
    }

    @Override
    public String getLabelResource() {
        return null;
    }

    public String getStaticLabel() {
        return "Rsync Sampler";
    }

    // JMeter上でこのサンプラーが追加された時に呼び出される。
    @Override
    public TestElement createTestElement() {
        // サンプラークラスのオブジェクトを生成
        RsyncSampler sampler = new RsyncSampler();
        modifyTestElement(sampler);
        return sampler;
    }

    // GUIの入力値が更新された時に呼び出される。
    @Override
    public void modifyTestElement(TestElement testElement) {
        testElement.clear();
        configureTestElement(testElement);
        // 入力されている値をサンプラーで使用できるようにする。
        testElement.setProperty("destinationUrl", destinationUrl.getText());
    }

    // 必須ではないがこのメソッドを実装しておくと再起動時に
    // 保存している設定ファイルから設定値を引き継げる。
    public void configure(TestElement testElement) {
        destinationUrl.setText(testElement.getPropertyAsString("destinationUrl"));
        super.configure(testElement);
    }
}

5.4. File Placement

上の設定で出来たソースコードを、gradle jar でビルドし、Jarファイルを作成します。

Jmeterの /lib/ext フォルダに格納することで、カスタマイズされたJarファイルが Jmeter に読み込まれて利用することが出来ます。

Macの場合は、こちらのファイルパスです。
/usr/local/Cellar/jmeter/5.3/libexec/lib/ext

6. Reference