パフォーマンステストとは～JMeter実践についてまとめる

# はじめに

最近、JMeterを使ってパフォーマンステストをやっています。学んだことを残すため記事に書いておきます。
初めてJMeterに触りパフォーマンステストについて考え始めるきっかけになれば幸いです。

# JMeterとは

公式ページは以下のものになります。

https://jmeter.apache.org/index.html

また、日本語でも解説ページが色々あるので、引用を載せておきます。

> JMeterはApacheソフトウェア財団が開発しているオープンソースの負荷検証ツールです。 サーバに対して指定した量のリクエストを送り、そのレスポンスを受けることで、パフォーマンス計測することができます。
JMeterはApacheの公式サイトからダウンロードできます。 （Javaを入れていない方は、インストールしてからご使用ください。）
JMeterはGUIモードとNon-GUIモードがありますが、ここではGUIモードの説明をします。 

（https://www.wantedly.com/companies/rakus/post_articles/131684）

## JMeterのGUIと非GUIモードについて

JMeterの動作モードにはGUIとNon-GUIモードがあります。
使い分けははっきり分かれています。

* GUIモード: シナリオファイルを作成するため。
* 非GUIモード: 作成したシナリオファイルを実行するため

GUIでもシナリオファイルを実行できますが、正しい結果を得ることができないため、推奨されていません。
（https://qiita.com/tatesuke/items/827e6190753964e46814）

1からJMeterを使うにあたって、どちらのモードも使う場面はありますが、動かすための要件も大きく異なってきます。
GUIモードでは、インストールした端末をGUIで動かすためのセットアップが必要になります。
非GUIモードでは、大きなCPU、メモリが必要になる場合があります。並列アクセスを再現する際にスレッドが大量に立ち上がるためです。また、テスト環境にアクセスするため、プライベートなネットワークに置く必要があるかもしれません。

そのため、両モードを一つの端末で実現するのは大変です。
端末を分け、GUIで作成したシナリオファイルを、テスト環境にある端末に配置し、非GUIで実行した方が楽だと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/9cb11bb1-4516-1d98-e392-02e757f28ee4.png)

## JMeterのインストール方法

* Amazon Linux2

EC2では以下のようなコマンドでインストールできます。

```bash
echo "install java"
sudo yum install java -y

echo "install jmeter"
if [[ -z ${JMETER_VERSION} ]];then
  JMETER_VERSION="5.6.2"
fi
mkdir /tmp/jmeter
cd /tmp/jmeter
wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz
tar -xzvf apache-jmeter-${JMETER_VERSION}.tgz
sudo mv apache-jmeter-${JMETER_VERSION} /opt/
sudo ln -s /opt/apache-jmeter-${JMETER_VERSION}/bin/jmeter /usr/local/bin/jmeter
```

* Windows

以下のページが参考になりました。

https://qiita.com/shotets/items/d553d7be0d407a9a9a53

# 余談: テストの例え話

初めてテストに触る人の場合、そもそもテストとは何かから入った方が目的がわかりやすいと思います（1年前の私がそうでした。。）
ただ、わからない専門用語が1つでも入ると思考がとまるので、私の身近なたとえ話で全体像を話したいと思います。

**唐突ですが、私は居酒屋が好きです。**

お酒だけでなく、その雰囲気も好きです。
そんな居酒屋で私がいつも思っていることが、「箸置き作りたいなー」ということです。
何となく箸入れの紙を折って箸置きにしているのを見ると、スマートでいいなと思います。

**とします。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1474054/235a682b-ff2e-8179-08e6-55f5a8328f18.png)

そんな時、ユーザー（私）にどんなサービスを提供すればいいでしょうか（箸置き以上でも以下でもないですが💦）。

この場合の箸置きを作る過程は↓の通りです。

1. 箸入れの紙を折って、箸置きを作る
1. 箸置きを机に置く
1. 箸置きに箸を置く

まず失敗することはなさそうですが、かっこ悪くてはダメです（スマートさを求める要件を満たしません）
そのため、要件を満たせるかテストします。（何度も言いますが、箸置き以上以下の話ではありません）

箸置き作成において、起こりうるトラブルとその対応の項目を挙げてみます。

1. 設計通りに折れているか
「設計なんていらなくて見ればわかるじゃないか」と言われそうですが、わかりやすさのためです。。見逃してください。。。
実際問題になると、設定はより複雑になり、直接目で見えない故に忘れることもあります。目で見える形になっていても視界に入らないこともあります。
1. 箸置きが机に自立できるか
箸置き単体は問題なくとも、机にうまくたたなくては意味がありません。
1. 箸置きから箸が滑り落ちないか
1. 箸置き作成手順がまとめられているか
この例題のゴールは「箸置きを作ること」ではなく「箸置きの作り方を教える」ことです。どの居酒屋でも箸置きを作れるようになりたいのです。

↑のように挙げてみると確認することは色々と分解できます。視点を変えると「箸置き作りが失敗した」といっても色々な原因があるということです。
なので、作業工程に対応したテストして問題なく次の段階に行けるか確認します。（いわゆるV字モデルのイメージです。）

# テストの種類について

例え話で何となくテストでやることが分かった（と希望的観測をした）ところで、テストの種類について記載します。
テストといっても色々な種類があります。私自身マスターしているわけではないので、経験の範囲内で記載します。

参考: https://www.atlassian.com/ja/continuous-delivery/software-testing/types-of-software-testing

* 単体テスト
設計通りの設定値か確認します。
アプリ側では、単体テストによりませんがテストコード作成が相当します。
参考: https://qiita.com/jnchito/items/2a5d3e15761fd413657a
* 結合テスト
モジュール間のやり取り、疎通確認など行います。
* 機能テスト
アプリを操作して仕様通りに動くか確認します。
WEBアプリでは自動化ツールとしてSeleniumなどがあります。
参考: https://gihyo.jp/dev/serial/01/tech_station/0006
* パフォーマンステスト
本番環境相当の負荷に耐えられるか確認します。
* 受入テスト
発注者側がテストを行い、要求通りに動作するか確認します。
※参考: https://hblab.co.jp/blog/what-is-the-uat-test/

上記で挙げたテスト以外にも色々な種類のテストがあります。

# パフォーマンステストの私なりの心構え

これまでパフォーマンステストをやっていて気を付けていることを書きます。

## アプリが「動く」前提で行われる

テストの種類を見ると、パフォーマンステストはだいぶ後の工程でやることだとわかります。そのため、アプリが無風（負荷がかかっていない）状態で動くことは確認済みの前提です。お金がかかりやすいパフォーマンステストで、基本的な動作確認をしていては、時間とお金の無駄が発生してしまいます。まして、パフォーマンステストを行うテスト環境（ステージング環境など）が自分の管理外のものである場合、予算に無理がないかよく確認する必要があります。

## テストは予想があって成り立つ

テストの方法にはホワイトボックステスト、ブラックボックステストの分類があります。

https://ssaits.jp/promapedia/method/black-box-white-box.html

視点によってテスト方法が異なりますが、いずれにしても「こうあるべき」という予想があって行うものです。
何となく動いたでは何もテストできていません。あらかじめ何をテストしたいのか決めて実施します。

## テスト項目に優先順位をつける

パフォーマンステストでは、アプリを構成する要素を細かく見ようとすると途方もないテスト項目になります。さらに作成者＝テスターとは限らないので、理解に時間がかかる場合もあります。ものによってはパフォーマンステストにどのような影響を与えるか予想しづらい場合もあり、パフォーマンステストをやって初めて理解できることもあるかもしれません。
お金や時間が限られている中で行うため、テスト範囲、優先順位を明確にする必要があります。

## Try&Errorで挑む

ミドルウェアのあるパラメータをチューニングしたい場合、はっきりとどこが最適値かわからない場合があります。
パラメータがパフォーマンスに与える影響を調べることから始まり、どの程度まで値を変えるのか、一つずつ確認していきます。

# シナリオの概要

JMeterを触っていると度々、「シナリオ」というワードを目にします。
説明されているページの引用を載せておきます。

> JMeterでやることは至ってシンプル。
シナリオを作って実行する
この一点だけ。
じゃあこのシナリオっていうのが何なのか。
では一例として、いくつかWhatを投げかけてみる。
シナリオとは？
→負荷のテストケースのこと
負荷のテストケースとは？
→仮定のユーザー操作
仮定のユーザー操作とは？
→APIリクエストの動きの再現
つまりシナリオとは
「ユーザーがとある操作をした時のAPIリクエストの動きを再現したもの」

（https://ramble.impl.co.jp/689/）

# シナリオファイルのよく使う項目

シナリオファイルはXMLファイルになっています。よく使う項目は以下の通りです。

* ループ回数（LoopController.loops）：負荷を何回繰り返すか決めます。回数を無限とすることもできます（LoopController.continue_foreverにてtrueを指定する）。
* スレッド数（ThreadGroup.num_threads）：いくつ同時に負荷をかけるかを決めます。
* ランプタイム（ThreadGroup.ramp_time）：負荷をかける時間を決めます。

例えば、10秒に3回の間隔でリクエストが来る状況を5回再現したいとします。
この場合、ループ回数は5回、スレッド数は3個、ランプタイムは10秒となります。

他にも負荷をかける時間、日時の指定などができます。

※参考
https://jmeter.apache.org/usermanual/test_plan.html#thread_group

# 大量のホストにアクセスするシナリオファイルを作成してみる

実際にJMeterのシナリオファイルを作成してみます。
作成方法は色々な記事で紹介されていて、以下のようなページが参考になります。

https://qiita.com/shotets/items/d553d7be0d407a9a9a53

ただし、大量のホスト名にリクエストを投げるシナリオを作りたい場合は、手作業では大変です。
シナリオファイルの中身はXML形式なので、構造を読み解けばコピペで作ることもできるかもしれません。

さらにChatGPTを使えば、基本的なシナリオファイルをすぐに出してくれます。GUIを使うより断然早いですね。。そこから、編集していくと複雑なテストも作りやすいのではないかと思います。

大量のホスト名が書かれたシナリオファイルの作成ですが、shellファイルを作成することとしました。
以下のようなshellを作成しました。（やっつけ間満載なのは目をつぶっていただきたいです。。。）

```bash:make_scenario_files.sh
#!/bin/bash -e

IN_FILE=$1
OUT_FILE=$2

cat /dev/null > "${OUT_FILE}"
cat << EOF >> "${OUT_FILE}"
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="2.9">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" 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>
EOF

grep -v '^[ ]*#' "${IN_FILE}" | while read line ; do
  DOMAIN=`echo ${line}"" | cut -d' ' -f1`
  HOST=`echo ${line}"" | cut -d' ' -f2`
  REQ_PATH=`echo ${line}"" | cut -d' ' -f3`
  LOOP=`echo ${line}"" | cut -d' ' -f4`
  NUM_THREADS=`echo ${line}"" | cut -d' ' -f5`
  RAMP_TIME=`echo ${line}"" | cut -d' ' -f6`

  if [ "${LOOP}" == "-1" ];then
    CONTINUE_FOREVER=true
  else
    CONTINUE_FOREVER=false
  fi

  cat << EOF >> "${OUT_FILE}"
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="${HOST}" 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">${CONTINUE_FOREVER}</boolProp>
<stringProp name="LoopController.loops">${LOOP}</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">${NUM_THREADS}</stringProp>
<stringProp name="ThreadGroup.ramp_time">${RAMP_TIME}</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
</ThreadGroup>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true">
<stringProp name="HTTPSampler.domain">${DOMAIN}</stringProp>
<stringProp name="HTTPSampler.port">443</stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">${REQ_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>
EOF

  if [ -n "${HOST}" ];then

    cat << EOF >> "${OUT_FILE}"
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
<collectionProp name="HeaderManager.headers">
<elementProp name="" elementType="Header">
<stringProp name="Header.name">Host</stringProp>
<stringProp name="Header.value">${HOST}</stringProp>
</elementProp>
</collectionProp>
</HeaderManager>
<hashTree/>
EOF

  fi

  cat << EOF >> "${OUT_FILE}"
</hashTree>
</hashTree>
EOF
done

cat << EOF >> "${OUT_FILE}"
</hashTree>
</hashTree>
</jmeterTestPlan>
EOF
```

* ループ回数、スレッド数といったパラメータは別ファイルに記述し、変数IN_FILEとして、渡すこととしました。
* 出来上がった、シナリオファイルはファイル名OUT_FILEとして出力しています。
* シナリオファイル中にHostヘッダーを使ってアクセスしています。テスト環境ではドメイン登録していない前提でアクセスできるようにしています。変数DOMAINにはパブリックIPを入れます。

変数IN_FILEに渡す設定ファイルは例えば以下のような形式で記載します。

```text:sample.txt
1.2.3.4 host /path -1 1 100
```

JMeterを実行するコマンドは以下のようになります。

```bash
$ jmeter -n -t ${scenario_file} -l ${log_file_head}.jtl -j ${log_file_head}.log > ${log_file_head}.jmeter.log
```

ログのファイル名は変数で変えられるようにしています。パフォーマンステストでは厳密な実施時間が重要となるため、いつどんな設定で行ったのかわかるように保管するのがよいと思います。

ログの種類は以下の通り

* `${log_file_head}.jtl`: アクセス結果が出力されます。リクエストのパス、レスポンスコードなど確認できます。
* `${log_file_head}.log`: JMeter自体の実行ログが出力されます。JMeterの起動ログやレスポンス結果のサマリーが表示されます。
* `${log_file_head}.jmeter.log`: レスポンス結果のサマリーに絞って表示されます。

もしも、シナリオのループ回数を無限にした場合、別途JMeterを停止させるコマンドが必要となります。
以下のようにプロセスIDを指定してkillコマンドで停止させることができます。

```
$ kill <プロセスID>
```

# おわりに

今回は私がこれまでに学んだパフォーマンステストについて、記載しました。
テストの目的を理解して、納得感を持って臨めるといいなと思います。
これからも勉強します。