Apache JmeterとBeanShellで柔軟な負荷テストをしよう!

こんにちは、幅広い視野を持つエンジニアを目指しています田中と申します。

今日はApache benchのGUI版であるApache JMeterの応用的な使い方を見ていきます。
RESTクライアントとしてだけではなく、APIのテストツールとしても使います。

はじめに

Apache JMeterとBeanShellを使って、負荷テストをやってみました。

目的

リクエストパラメータに変数を埋め込んでそれを動的に変化させながら負荷テストを行い、レスポンスのバリデーションもしたい

目次

手順の概要

  1. Apache JMeterのインストール
  2. スレッドグループの作成と編集
    1. 起動画面
    2. スレッドグループの作成
    3. スレッドグループの編集
  3. HTTPリクエストの作成と編集
    1. HTTPリクエストの作成
  4. リスナーの作成と編集
    1. リスナーの作成
  5. Javaでプロトタイピング
    1. IPアドレスを自動生成する
  6. BeanShellでコーディング
    1. リクエストパラメータに変数を埋め込む
    2. BeanShell PreProcessorの追加
    3. ロジックを変更
    4. カスタマイズ(スキップ可能j)
  7. レスポンスのバリデーションを行う

実行環境

Windows 10 Pro 64bit, Java 1.8.0_144, JMeter 3.2

手順

Apache JMeterのインストール

http://jmeter.apache.org/download_jmeter.cgi
上記のサイトからzipファイルをダウンロードします。

筆者はバージョン3.2をダウンロードしました(Java 8が必要です)。

ダウンロードとインストール

スレッドグループの作成と編集

起動画面

起動画面は以下のような感じです
起動画面

スレッドグループの作成

まず初めに、スレッドグループを作ります。
スレッドグループは、そのスレッドのAPIテストをどのように行うかの設定になります。

【テスト計画】を右クリックし、【追加】→【Thread(Users)】→【スレッドグループ】を選択します。
スレッドグループの作成

スレッドグループの編集

スレッドグループのパラメータの意味は以下です。
【スレッド数】= JMeterが生成するスレッド数、1スレッドが1ユーザに相当する。
【Ramp-up期間】= 何秒かけてそのスレッドを処理するか、0を指定するとすべて同時に実行する。
【ループ回数】= 1つのスレッドが1つのテストケースを何回実行するか。

以上を踏まえるとテストシナリオで重要な項目は以下の計算式で算出できます。

重要なパラメータ

さらにJMeterはスレッドグループにスレーブマシンを使用し並列にテストすることができます。
実際には【テストの総回数】は、
【スレッド数】×【ループ数】× (【リクエストボディの数】) × (【スレーブマシンの数】)のようになることもありますが基本は同じです。

スレッドグループの編集

JMeterはループ回数が1の場合、処理開始の時間をスレッドごとに、(Ramp-UP期間[秒] / スレッド数)だけずらして実行してくれるため、
結果として上の画像の場合は(10[秒] / 10) = 1となり、1秒間に1リクエストの計10回のアクセスが行われ、10秒でテストが完了します。
ループ回数を指定せず無限ループのチェックボックスにチェックを入れると無限ループ実行を行います。
(耐久テスト、ある一定のレスポンス速度を保証できるかどうかのテスト向き)

HTTPリクエストの作成と編集

HTTPリクエストの作成

以下のようにして、HTTPリクエストを作成します。
HTTPリクエストはAPIに実際にアクセスする際のHTTPリクエストの設定になります。

【スレッドグループ】を右クリックし、【追加】→【サンプラー】→【HTTPリクエスト】を選択します。
HTTPリクエストの作成

以下のように入力して、HTTPリクエストを編集します。
HTTPリクエストの編集

【プロトコル】はWebサーバのプロトコルです。HTTP/HTTPSを指定します。
【メソッド】にはHTTPのリクエストメソッドを指定します、今回はGETとしています。
【サーバ名またはIP】には実際にアクセスするAPIのURL(エンドポイント)を指定します。
【リクエストで送るパラメータ】には任意のパラメータが指定できるのですが、今は何もありません(この後出てきますのでお楽しみに)。

リスナーの作成と編集

リスナーの作成

これで、APIにGETリクエストを送る準備は整いましたが、このままではAPIによって返却されたレスポンスやデータを扱うことができません。
JMeterでは、APIに送った結果を受け取る受け皿の仕組みがあり、結果をグラフとして可視化したりファイルに書き出したりする機能があります。
これを、リスナーといいます。
それでは、リスナーを作ってみましょう。

【HTTPリクエスト】を右クリックし、【追加】→【リスナー】→【結果をツリーで表示】を選択します。
リスナーの作成

負荷テストでよく使われる統計量を算出するためのリスナーも追加しましょう。
【HTTPリクエスト】を右クリックし、【追加】→【リスナー】→【統計レポート】を選択します。
リスナーの追加

さて、ここまで設定出来たらウィンドウ上部にある緑のボタンを押してみましょう!

※JMeterを用いてAPIのテストや負荷テストを行う際は必ず自分や所属する組織が作成したWebサーバやAPIに対して事前に許可を取ってから行うようにしてください!
外部のWebサーバやAPIに対し許可無くJMeterでテストを行うことはDoS攻撃とみなされる可能性があります。

左のメニューから【結果をツリーで表示】を選択すると、以下のように出力されています。
APIリクエストに使われたヘッダやリクエストパラメータ、レスポンスコードなどの情報を見ることができます。
結果をツリーで表示

左のメニューから【統計レポート】を選択すると、以下のように出力されています。
統計レポート

この統計レポートの見方は以下になります。

ヘッダ 意味
Label サンプラーの名称
#Samples サンプル数
Average 平均応答時間(ms)
Median 応答時間の中央値(ms)
90% Line 90%信頼区間(少なくとも90%はこの値に収まる。単位ms)
95% Line 95%信頼区間(少なくとも95%はこの値に収まる。単位ms)
99% Line 99%信頼区間(少なくとも99%はこの値に収まる。単位ms)
Min 最小応答時間(ms)
Max 最大応答時間(ms)
Error% エラーの割合(%)
Throughput スループット(1秒間に何件のリクエストに対し応答できたか)
KB/sec 1秒当たりの平均転送データ量(KB)

ここまでで、JMeterをRESTクライアントおよび負荷テストの指標として使うことができました!
ここからは、IPアドレスを受け取ると経度および緯度情報を返すAPIに対し、JMeterを使用して有効なIPアドレスを生成し、
返ってきたレスポンスが正しいか検証するAPIテストクライアントとしてのJMeterシナリオを作成していきましょう!

Javaでプロトタイピング(スキップ可能)

IPアドレスを自動生成する

以前の私の記事のネットワークアドレスの秘密を参考に、有効なIPアドレスを生成するコードをJavaで書いてみました。
IPアドレスを整数((32bit unsigned int)0 ~ 4,294,967,295)に変換することで、複雑な正規表現チェックをしなくて済むという内容でした。
JMeterはBeanShellというJava処理系のコードを実行できるので、こちらのコードを編集して、BeanShellのコードにしていくことを目指します。
※コードが拙いのは愛嬌ということで・・・

import java.io.IOException;

class DetailOfIpAddress {

    public static String uint2ipaddr(String uintAsString) {
        String ipAddressAsString = "";
        try{
            int octet_binary_0 = (Integer.parseUnsignedInt(uintAsString) >> 24) & 0xff;
            int octet_binary_1 = (Integer.parseUnsignedInt(uintAsString) >> 16) & 0xff;
            int octet_binary_2 = (Integer.parseUnsignedInt(uintAsString) >> 8) & 0xff;
            int octet_binary_3 = Integer.parseUnsignedInt(uintAsString) & 0xff;
            StringBuilder sb  = new StringBuilder();
            sb.append(octet_binary_0);
            sb.append(".");
            sb.append(octet_binary_1);
            sb.append(".");
            sb.append(octet_binary_2);
            sb.append(".");
            sb.append(octet_binary_3);
            ipAddressAsString = sb.toString();
        } catch(NumberFormatException e) {
            System.out.println("有効な符号なし整数ではありません!");
            System.out.println(e);
        }
        return ipAddressAsString;
    }

    public static void main(String args[]) throws IOException {
        String ipAddressAsString;

        if(args.length != 1) {
            System.out.println("指定できる引数は1つです!");
            return;
        }

        ipAddressAsString = uint2ipaddr(args[0]);
        System.out.printf(ipAddressAsString);

    }
}

実行結果のスクリーンショットは以下です。
Javaプログラムのスクリーンショット

BeanShellでコーディング

リクエストパラメータに変数を埋め込む

以下のように、HTTPリクエストを編集します。
左のメニューから【HTTPリクエスト】を選択します。
画面下部の【追加】ボタンから【リクエストで送るパラメータ】にパラメータ名:ip、値:${RANDOM_IPADDR}を追加。
HTTPリクエストの編集

BeanShell PreProcessorの追加

BeanShellのスクリプトを実行するために、BeanShell PreProcessorを追加しましょう。
【HTTPリクエスト】を右クリックし、【追加】→【前処理】→【BeanShell PreProcessor】を選択します。
Bean Shell PreProcessor

ロジックを変更

Javaのプロトタイプではコマンドライン引数として32bit符号なし整数を受け取っていました。
BeanShellでもコマンドライン引数を受け取れますが、今回はスレッド毎に乱数を生成するようにします。
BeanShellについてはこちらをご覧ください。
BeanShellではJDK1.5.0互換のJavaの文法とAPIがそのまま使えます。(ちょっと古いなあ…)

Script:を以下のように編集しましょう。

random = String.valueOf((1L >> 31) + (int)(Math.random() * Integer.MAX_VALUE) + (int)(Math.random() * Integer.MAX_VALUE)); // ((32bit unsigned int)0 ~ 4,294,967,295)の乱数を生成

/* IPアドレスの各オクテットを生成 */
int octet_binary_0 = (Integer.parseUnsignedInt(random) >> 24) & 0xff; // IPアドレスの第一オクテット
int octet_binary_1 = (Integer.parseUnsignedInt(random) >> 16) & 0xff; // IPアドレスの第二オクテット
int octet_binary_2 = (Integer.parseUnsignedInt(random) >> 8) & 0xff; // IPアドレスの第三オクテット
int octet_binary_3 = Integer.parseUnsignedInt(random) & 0xff; // IPアドレスの第四オクテット

/* IPアドレスの各オクテットからIPアドレスを生成 */
StringBuilder sb  = new StringBuilder();
sb.append(octet_binary_0);
sb.append(".");
sb.append(octet_binary_1);
sb.append(".");
sb.append(octet_binary_2);
sb.append(".");
sb.append(octet_binary_3);
ipAddressAsString = sb.toString();

vars.put("RANDOM_IPADDR", String.valueOf(ipAddressAsString)); // RANDOM_IPADDRに生成されたIPアドレスを代入

Javaのコードが書ける
vars.put(“変数名”, “変数の値”);でクエリパラメータの${変数名}に値を渡すことができます!
JMeterのインスタンス変数のctx, vars, props, prev, sampler, logにアクセスしたりJMeterのAPIを呼ぶこともできます。
詳しくはこちらをご覧ください。
ここまで書けたらボタンを押して実行してみましょう!

BeanShell PreProcessorの結果

URLを見るとちゃんと有効なIPアドレスのパラメータを追加してアクセスできていますね!

カスタマイズ(スキップ可能)

さてBeanShellはJavaのプログラムなのでJavaでできることは基本的に何でもできます。
ここでは、JMeterのAPIにアクセスしてスレッドの情報をデバッグ情報として表示したり動作ログを出力してみましょう。

JMeterをJMeterがインストールされているフォルダのApacheJMeter.jarではなく、jmeter.batを起動するか、下記コマンドを実行します。

jmeter -n -t [jmxファイルのパス] -l [結果ファイルのパス] -e -o [レポートの出力先フォルダ]

JMeterをCLIモードとして起動することができます。
jmeter.batの場合、GUIとコンソール出力を受け取るためのコマンドプロンプトが両方起動します。この先はjmeter.batを使用した場合を想定します。
例えば、以下のようなコードを追加できるか試しましょう。

System.out.println("サンプラー名: " + sampler.getName()); // サンプラー名を取得
System.out.println("スレッド名: " + ctx.getThread().getThreadName()); // スレッド名を取得
/* パラメータの数だけループ */
for (int i=0;i<sampler.getArguments().getArgumentCount();i++)
{
    System.out.println("パラメータ: " + sampler.getArguments().getArgument(i)); // i番目のパラメータを表示
}
System.out.println("アクセス先URL: " + sampler.getUrl()); // アクセス先URLを表示
// 何かやりたいこと
System.out.println(""); // 改行を出力
実行結果

動作状況を出力することができました!(概ね1秒ごとにリクエストできていることも見て取れます。)
なお、log.info(“文字列”)などとすることで、JMeterの動作ログファイルに文字列を書き込むこともできます。

レスポンスのバリデーションを行う

JMeterはデフォルトでHTTPレスポンスコードの”200 OK”が返されるとリクエスト成功と判定しますが、APIテストではさらにレスポンスに対してバリデーションをかけたいこともあります。
次はレスポンスをBeanShellでバリデートしてみましょう!
【HTTPリクエスト】を右クリックし、【追加】→【アサーション】→【BeanShellアサーション】を選択します。
BeanShellアサーション
さらに【HTTPリクエスト】を右クリックし、【追加】→【リスナー】→【アサーション結果】を選択します。
アサーション結果の追加

今回作成したAPIはパラメータとしてIPアドレスを受け取り以下のようなレスポンスを返すと想定します。

{
    "items": [
        {
            "found": true, 
            "info": {
                "city": "Mountain View", 
                "continent": "North America", 
                "country": "United States", 
                "location": [
                    37.419200000000004, 
                    -122.0574
                ], 
                "postal_code": "94043", 
                "subdivision": "California", 
                "time_zone": "America/Los_Angeles"
            }, 
            "ip_address": "144.189.123.1"
        }
    ]
}

IPアドレスによっては国名が取得できなかったりするのでそれを検知したいとします。
例えば、以下レスポンスとなります。

{
    "items": [
        {
            "found": false,
            "ip_address": "193.53.1.45"
        }
    ]
}

スクリプトに以下コードを記述します。

String result = new String(ResponseData); // レスポンスを受け取る

if (!(result.contains("country"))) {
    Failure = true; // 失敗とする
    FailureMessage = "Request failure: " + result; // アサーション結果に失敗理由を出力する
}

BeanShellアサーションのコード
もちろんSystem.out.println()等を実行してコンソールにデバッグ情報を出力することも可能ですが、自動化との親和性が高いであろう、ログファイルを指定する方法を採用します。
ボタンを押して実行しましょう!
すると・・・
テスト結果
国名を取得できないIPが見つかりました!
アサーション結果も見てみましょう。
アサーション失敗
193.53.1.45はどこの国のIPアドレスか不明ということですね…(ちなみにデータベースはGeoIP2を使用してます)
GeoIP2のデモサイトでも見つからないと出てますね。
No Geo data

まとめ

Apache JMeterを使うことで高速かつ低コストに負荷テストおよびAPIテストが実現できる!!
ここまで読んでくださりありがとうございました!

・引用元および参考にさせていただいたサイト様

【公式】Apache JMeter

【公式】JMeterコンテキスト

【公式】JMeterAPIドキュメント

JMeterの簡単な使い方まとめ -Qiita

 

投稿者プロフィール

tanaka
OS非依存のスクリプト言語によるバッチ処理が得意です。C言語やJavaなどのソフトウェア開発の経験もあります。幅広い視野でプログラミングすることを第一に総合的なエンジニアを目指すため、ネットワーク管理や監視を通して勉強をし、Linuxの知識を高めていきたいです。

コメントを残す

メールアドレスが公開されることはありません。

Time limit is exhausted. Please reload CAPTCHA.

ABOUTこの記事をかいた人

OS非依存のスクリプト言語によるバッチ処理が得意です。C言語やJavaなどのソフトウェア開発の経験もあります。幅広い視野でプログラミングすることを第一に総合的なエンジニアを目指すため、ネットワーク管理や監視を通して勉強をし、Linuxの知識を高めていきたいです。