この文書は『JavaWorld』2005年12月号(IDGジャパン)に掲載された「入門! JMeter」の原稿です(掲載記事はこれに若干の修正が入っています)。この文書の著作財産権は(株)IDGジャパンに帰属し、複製や二次利用を行う場合には(株)IDGジャパンの許可が必要です。この文書は『JavaWorld』編集部の了承を得て公開しています。

はじめに

負荷テストの必要性

今年5月、関東地方で大きな地震が発生したとき、各地の震度情報を気象庁に送る東京都のサーバの能力が十分でなかったため、伝送完了まで想定を大きく超える時間がかかったというトラブルがあった。直接の原因として、システム構築当初と比較して震度情報を送ってくる震度計が大幅に増えたこと、震度以外の気象情報も処理するようになったことがあげられている。このトラブルはなぜ防げなかったのだろうか。

どのようなシステムでもリリースにあたってはリリースに足る品質を保証するテストが行われる。ただ往々にしてそのテストは機能面に重点が置かれがちだ。かつてワインバーグはプログラムを評価する指標として

  1. 仕様
  2. スケジュール
  3. 適応力
  4. 効率

という順位付けをおこなったが*、それは今日も同様で、まず機能テストを満足することが求められるのはごく自然な話である。とはいえそれだけでは十分ではない。順位は低くても適応力や効率を保証するテストも必要である。
*ジェラルド=M=ワインバーグ著、木村泉・角田博保・久野靖・白濱律雄訳、『プログラミングの心理学』、毎日コミュニケーションズ、東京、2005、pp.48-62。

メインフレームを中心としたクローズドなシステムを構築するのであれば、利用者数をある程度コントロールできたかもしれない(実際にはそう単純ではないだろうが)。しかし最近のWebアプリケーションに代表されるオープンなシステムでは、たとえばサーバにつながるクライアントの数だけ見ても短期間に大きく変動する可能性がある。開発のサイクルが短くなり機能追加なども比較的頻繁に行われる。これらの変化に適応できなければ効率の著しい低下を招き、システムの機能に問題がなくてもトラブルの要因となってしまう。 『プログラミングの心理学』が書かれた時代と比べて、今日はシステム環境をめぐる変化は速く大きくなっており、機能テスト以外の検証の重要性も高くなっている。そのひとつが負荷テストというわけだ。

負荷テストはシステムに対して高い負荷をかけた状態で、その品質がどの程度維持されるかを検証するものである。負荷テストを実施するのは、想定する環境下での品質を保証することはもちろん、どの程度までの変動まで許容できるかをあらかじめ把握しておくことで、その後発生する機能追加やクライアント数の増加に伴ってハードウェア増強などの対応が必要になるかどうかを判断することができるからだ。冒頭言及した東京都のシステムではこのレベルのテストが行われていなかったことがトラブルの一要因と言える。

負荷テストツール

では負荷テストはどのように実施すれば良いだろうか。最も原始的な方法はシステムが想定する数の端末を用意し、人手でサーバに対してリクエストを送ることである。が、この方法ではすぐに限界に行き当たることが容易に想像できる。不特定多数の利用者が存在するような公開のWebアプリケーションで、テストに必要な端末を揃えること自体が現実的でない。かりに数量の問題をクリアできても、人手によるテストでは正確な操作・結果の取得が難しく、検証の精度に問題が残ることがままある。昔話になるが、テストに必要な人数を集めて人手で負荷テストを実施し問題なしと判定したものの、いざリリースしてみると実際の負荷に耐え切れずトラブルになった事例が筆者の身のまわりにもあった。さらに人を集めることにコストがかかる上、テストを繰り返し実行することも簡単ではない。人手による負荷テストはごく限られた条件でしか意味がないと考えるべきだ。

そこで負荷テストツールの出番となる。世の中には負荷テストを行う多くの製品が出回っている(主要な製品を表1にあげる)。

表1
製品名 開発元
LoadRunner マーキュリーインタラクティブ
e-Load エンピレックス
Assam WebBench 日立ソフトウェアエンジニアリング
WebLoad RadView Software
Rational Performance Tester IBM
QALoad コンピュウェア

これらの製品はおおむね以下のような機能を持っている。

  • 各種クライアント(HTTP、FTP、DBMSなど)をシミュレートする。
  • 複数のクライアントを擬似的に生成し、処理を実行する。
  • テストスクリプトを作成し、一連の動作として実行する。
  • 人手による操作からスクリプトを生成する。
  • クライアントの入力データ、時間にばらつきを持たせることができる。
  • レスポンス内容を検証する。
  • テスト結果をエクスポートし、ほかのツールで解析できる。

製品の価格はさまざまであり、高価な製品は上記の基本機能に加えて、複数のブラウザのシミュレート、アプリケーションサーバとの統合、サーバサイドの性能統計の取得、検証結果の解析支援などの高度な機能を持っている。

その一方で、これから紹介するJMeterのような無償の製品も存在する。JMeterはApache Jakarta Projectが開発する、Javaベースの負荷テストツールである。商用製品と比較すると、付加的な機能や細かな使い勝手では及ばないが、基本機能は一通り揃っており、一般的なサーバに対する最低限の負荷テストができるレベルにある。本稿ではJMeterのセットアップから基本的な使い方を概観するが、これはJMeterそのものの紹介であると同時に、一般的な負荷テストのイメージ(ツールに対してどういう設定を行って、何を検証するのかなど)をつかんでいただくことも大切な目的と認識している(JMeter自体の踏み込んだ使い方については第2部で紹介する)。

実際のところ一口に負荷テストといっても、対象のシステムの規模や性質によって最適なツールも変わってくる。本当に高機能なツールが必要な場面もあれば、むしろ安価(あるいは無償)であることが重要になる場面もあるだろう。これから負荷テストを実施しようという人であれば、まず無償のツールであるJMeterを試行してみるのも良いかもしれない。結果、JMeterで十分だというケースも少なくないだろうし、より高機能のツールが必要になったにせよ、試行を通して得た知見はほかのツールでも生きてくるはずだ。

セットアップ

システム要件

この文書を書いている時点でのJMeterの最新版は2.0.3である。これを動かすにはJRE1.4以上が必要である。JMeterは100%Pure Javaアプリケーションなので、JRE 1.4が動作すればOSなどのプラットフォームは何であってもよい。JMeterの基本機能を動かすだけならこれで十分だ。

JDK 1.4以上
JMeterのソースをビルドしたり、プラグインを開発する場合。
SAX XMLパーサ
JMeterに付属するXerces以外のSAX XMLパーサを利用する場合。
JDBCドライバ
DBMSの負荷テストを行う場合、テストするDBMSに対応したJDBCドライバが必要になる。
JavaMailとJAF
メーラービジュアライザを利用したり、SOAPのテストを行う場合。

インストール

JMeterをインストールするには、Jakartaプロジェクトのダウンロードページ(http://jakarta.apache.org/site/downloads/downloads_jmeter.cgi)より最新のバイナリのアーカイブファイルを取得すればよい。アーカイブファイルにはtar.gzとzipがあるが、実行環境に合わせてどちらかを取得する。Windows系であればzip、Unix系であればtar.gzをダウンロードすることになるだろう。

アーカイブファイルを取得したら、それを任意のディレクトに展開する。すると指定したディレクトリ配下のjakarta-jmeter-<バージョン番号>というサブディレクトリに実行に必要なファイル一式が作成される。これでインストールは完了である。

JMeterの起動

インストールで作成されたjakarta-jmeter-<バージョン番号>の下のbinにあるjmeter(Windowsの場合はjmeter.bat)を実行する。コマンドラインからbinディレクトリに移動して

$ jmeter

を叩けばよい($はプロンプト)。WindowsでExplorerから起動するならjmeterw.batをダブルクリックする。正常に起動すれば図1のような画面が表示される。

JMeterの起動画面
図1

システムによってはテストしたいサーバがプロキシ越えに存在することもあるだろう。その場合は以下のコマンドラインオプションを指定する。

-H
プロキシのホスト名またはIPアドレス。
-P
プロキシのポート番号
-u (必要があれば)
プロキシ認証のユーザ名
-a (必要があれば)
プロキシ認証のパスワード

たとえばプロキシサーバがproxy.example.comでポート番号が8080であった場合、

$ jmeter -H proxy.example.com -P 8080

を実行することになる。

まずは動かしてみる

(疑似)サーバを用意する

JMeterの機能を個別に説明する前に、まずJMeterを実際に動かしてみて、おおよそどのような動作をするのかをつかんでおこう。今回はもっぱらHTTPサーバに対するテストを取り上げるが、当然のことながらHTTPサーバがなければテストできない。もしJMeterの試行に使えるHTTPサーバが身近にあるのならそれを使えばいい。しかしながら都合良くそうしたサーバがあるとは限らないので、ここでは擬似サーバを用意してJMeterの動作を見ていくことにする。

本当は擬似サーバと呼ぶのもおこがましいようないい加減なプログラムなのだが、これでもJMeterはちゃんと相手をしてくれるので、JMeterの動きを確認するくらいのことはできる。擬似サーバを動かすにはまずリスト1のソースをTestServer.javaという名前で保存する。

リスト1
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.ServerSocket;
import java.util.Date;
import java.text.SimpleDateFormat;

public class TestServer {
    public static void main(String[] args) throws Exception {
        SimpleDateFormat sdf
            = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        ServerSocket server = new ServerSocket(80);

        for (;;) {
            Socket client = server.accept();

            System.out.print(sdf.format(new Date()));
            System.out.print(" ");
            System.out.println(client.getInetAddress().getHostAddress());

            BufferedReader br = new BufferedReader
                (new InputStreamReader(client.getInputStream()));

            String line;
            int length = 0;

            while (!(line = br.readLine()).equals("")) {
                System.out.println(line);
                if (line.startsWith("Content-Length: ")) {
                    length = Integer.parseInt
                        (line.replaceFirst("Content-Length: ", ""));
                }
            }

            if (length > 0) {
                char[] buf = new char[length];
                br.read(buf, 0, length);
                System.out.println(new String(buf));
            }

            PrintWriter pw = new PrintWriter(client.getOutputStream());

            pw.print("HTTP/1.0 200 OK\r\n");
            pw.print("Content-Type: text/plain\r\n");
            pw.print("Content-Length: 4\r\n");
            pw.print("Connection: close\r\n");
            pw.print("\r\n");
            pw.print("OK\r\n");
            pw.close();

            client.close();
        }
    }
}

このプログラムが何をするかは見ていたたければわかると思う。

  • 80/tcpをLISTENする。
  • リクエストが来たら時刻とクライアントのIPアドレスを標準出力に出力する。
  • リクエストを空行(HTTPであればヘッダの終わり)まで読み込み、その内容を標準出力に出力する。
  • ヘッダ内にContent-Lengthというフィールドがあればその値を取得し、空行以降その大きさの分のデータを読み込み、そのまま標準出力に出力する。
  • 固定のレスポンスを返し接続をクローズして次のリクエストを待つ。

ということをやっているに過ぎない。とりあえずJMeterが、いつどんなリクエストを送ってきたかを見ようというわけだ。これだとリクエストをシリアルにしか捌けないが、リクエストに対して大した処理をしているわけでもないので良しとしよう。

保存したソースをコマンドラインから

$ javac TestServer.java

を実行してコンパイルし

$ java TestServer

を実行して起動すれば、HTTPサーバがなくてもJMeterのリクエストを受けられるようになる。

最初のテスト

それではJMeterからHTTPリクエストを送ってみよう。JMeterを起動すると図1に示したように左右二つのフレームに分かれた画面が表示される。その左側の「テスト計画」を右クリックして「追加」→「スレッドグループ」を選択する(図2)。

スレッドグループ追加画面
図2

すると左側のフレームにスレッドグループが追加される。これを右クリックして「追加」→「サンプラー」→「HTTPリクエスト」を選択する(図3)。

HTTPリクエスト追加画面
図3

右側のフレームで追加されたHTTPリクエストの設定ができるので必要な項目を埋めていく。上述の擬似サーバを使う場合は、「サーバ名またはIP」に「127.0.0.1」を、「パス」に「/」を設定する(図4)。別のテスト用のHTTPサーバが存在するのであれば、テストを行う任意のURLに合わせて設定内容を読みかえる。

HTTPリクエスト設定画面
図4

再び左側のフレームの「スレッドグループ」を右クリックして「追加」→「リスナー」→「結果をツリーで表示」を選択する(図5)。

リスナー追加画面
図5

これでJMeter側の準備は完了である。擬似サーバ、もしくはそのほかのテストサーバが起動していることを確認して、JMeterのメニューから「実行」→「開始」を選択する(図6)。

テスト開始画面
図6

正常に動作すれば、擬似サーバを実行したコンソールに

2005-08-27 21:39:01.656 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

といった出力が表示されるはずだ(HTTPサーバを利用している場合はログを確認してほしい)。これでJMeterからHTTPリクエストが送られてきたことがわかる。

サーバ側からのレスポンスはJMeterで確認できる。まず左側のフレームの「結果をツリーで表示」をクリックし、右側のフレームの「Root」をダブルクリックする。するとテストで送られた数だけの「HTTPリクエスト」が展開される(図7)。

HTTPリクエスト結果一覧画面
図7

今回はHTTPリクエストはひとつだけなのでそれをクリックする。「Sampler result」タブでレスポンス時間とヘッダの情報が、「リクエスト」タブでJMeterからのリクエストメソッドが、「応答データ」タブでレスポンスの内容が参照できる(図8)。

HTTPリクエスト結果画面
図8

作成したテスト計画はメニューの「ファイル」→「テスト計画を保存」で保存可能だ。保存したテスト計画は「ファイル」→「開く」でいつでも読み込むことができる。このテスト計画はこのあとも繰り返し使うつもりなので、このタイミングで一度保存しておいた方がいいだろう。

基本的なテスト計画の構成要素

クライアントの設定 - スレッドグループ

テスト計画ではJMeterで行う一連の動作を定義する。最初に追加したスレッドグループはJMeterが生成するクライアントを設定するものだ。後述するサンプラーやリスナーといった要素は、このスレッドグループの下に追加する。

さきほどのテスト計画の「スレッドグループ」をクリックすると設定内容が表示される。そこには

  • スレッド数
  • Ramp-Up期間
  • ループ回数

の3つのプロパティがあり、すべて「1」になっていると思う。スレッド数はJMeterが生成するクライアントの数である。生成したクライアントすべてが独立してスレッドグループ以下に追加された要素を実行していく。Ramp-Up期間はすべてのクライアントが起動するまでの時間である。ここを「0」にするとすべてのクライアントが一斉に生成され、「0」以外の値が指定されるとそれをスレッド数で割った時間ごとにクライアントが生成される。たとえばスレッド数が「5」でRamp-Up期間が「10」であれば、2秒間隔でクライアントが生成される。ループ回数は各クライアントがテストを繰り返す回数である。

では設定をちょっと変えて実行してみよう。スレッド数を「3」、Ramp-Up期間を「9」、ループ回数を「2」として、メニューから「実行」→「開始」を選択する。そのときの擬似サーバの出力例をリスト2に示す。リスト2

リスト2
2005-08-28 19:30:20.234 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-28 19:30:20.234 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-28 19:30:23.234 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-28 19:30:23.250 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-28 19:30:26.234 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-28 19:30:26.250 127.0.0.1
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

とても見づらくて申し訳ないのだが、3つのクライアントが、3秒おきに起動して、2回ずつリクエストを送ってきていることがわかるだろうか。

このほかスレッドグループの画面では、リクエストがエラーになった場合の振る舞い(そのままテストを継続、該当スレッドのテストを中断、テスト全体を中断)を指定したり、スケジューラを使って、テストの開始・終了時間を指定したり、あるいは何秒後にテストを開始して、何秒継続したらテストを終了するといった設定も可能だ。

分散クライアント

スレッドグループで大量のクライアントを設定することは簡単だが、実際に動かすとなると相応のCPU能力を必要とする。これを単一のマシンで実行できるとは限らない。マシンの能力が足りなくて必要な負荷がかけられないということも起こりうる。JMeterには1台のマシンから複数のマシンをコントロールしてテストを実行する機能があり、そのような大きな負荷をかけたい場合にも対応できるようになっている。

JMeterの分散クライアントの機能を利用するには、サーバに対して負荷をかけるすべてのマシンでjmeter-serverを起動する。コマンドラインからJMeterのインストールディレクトリの下のbinに移動して

$ jmeter-server

を実行すればよい。このときjmeter-serverからrmiregistryが起動されるので、JDKまたはJRE配下のrmiregistryに検索パスが通っていることを確認して、必要があれば検索パスを通しておかねばならない。

これらをコントロールするJMeterを起動する前に、jmeterコマンドと同じディレクトリにあるjmeter.propertiesを編集する。修正するのはremote_hostsプロパティである。ここにコントロールしたいマシンのIPアドレスまたはホスト名をコンマ区切りで列挙する。たとえば

remote_hosts=127.0.0.1,192.168.0.12

と書けば、自ホストと192.168.0.12のマシンをコントロールすることになる。

jmeter.propertiesを保存してからJMeterを起動する。とりあえずここでは前に使ったテスト計画を動かしてみよう。ただ今度はリモートからのアクセスがあるので、HTTPリクエストにループバックアドレスを指定するわけにはいかない。JMeterの画面のHTTPリクエストを選択して、WebサーバのIPアドレスを通常のアドレスに変更する(図9。ここでは「192.168.0.11」に変更している)。

HTTPリクエスト設定画面
図9

まずリモートのクライアントから個別にテストを実行してみる。メニューから「実行」→「開始(リモート)」から実行するマシンを選択する(図10)。

分散クライアント実行画面
図10

ローカルで実行したのと同じリクエストが、選択したマシンから送られてきたことがわかる。

2005-08-28 21:29:18.970 192.168.0.12
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 192.168.0.11
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

次に本来の目的である複数のマシンからの分散テストを実行してみよう。メニューから「実行」→「全て開始(リモート)」を選択する。送られてきたリクエストは以下の通りである。

2005-08-28 21:42:04.350 192.168.0.11
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 192.168.0.11
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-28 21:42:04.756 192.168.0.12
GET / HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 192.168.0.11
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

スレッドグループで設定したスレッド数×ループ回数分、それぞれのマシンからリクエストが送られてくることがわかる。つまり全体のリクエスト数はスレッド数×ループ回数×マシン数となる。これに対してRamp-Up期間は全てのマシンを通じてコントロールされる。

サーバに対する処理を実行する - サンプラー

スレッドグループで生成するクライアントを設定したら、その配下にテストで行う一連の具体的な処理を追加していく。中でもサーバに対する直接の処理を定義するのがサンプラーである。

サンプラーはプロトコルごとに存在し、対応するサーバに対してリクエストを送りレスポンスを取得する。JMeterにはHTTPをはじめ、FTP・JDBC・Webサービス・LDAPなどさまざまなサンプラーが用意されている。残念ながら紙面(と筆者の能力)の都合でそのすべてに言及することはできない。ここでは最もよく使われるであろうHTTPリクエストの設定方法などを見ていこう。

これまで使ってきたテスト計画の「HTTPリクエスト」をクリックすると、右側のフレームにHTTPリクエストの設定画面が表示される。上のほうに並んでいる

  • サーバ名またはIP
  • ポート番号
  • プロトコル
  • パス

がURLを構成する要素となる。このプロトコルにはHTTPかHTTPSを指定できる。さらにメソッド(GETまたはPOST)の指定やパラメータ付きのリクエストの送信も可能だ。

このほかリダイレクトへの対応、マルチパートフォームでのファイルのアップロード、レスポンスのHTMLの中の組み込みリソース(画像やJavaアプレットなど)の読み込み、モニタ結果リスナー用の情報の取得、などの設定ができるが、ここでは詳細な説明は割愛する。

せっかくなのでPOSTメソッドでパラメータ付きのリクエストを送ってみよう。ちょうどフォームに入力されたデータをサーバに送信するイメージだ。仮に以下のような設定をして擬似サーバに対して送信してみる。

  • サーバ名またはIP: 127.0.0.1
  • パス: /input_form

これにパラメータの定義を追加する。

名前 Encode? 等号含む?
name
job プログラマ

「追加」ボタンをクリックして必要な行を追加し、パラメータの名前と値をそれぞれの欄に入力する。「Encode?」はURLエンコードが必要な値に対してURLエンコードを行うかどうか、「等号含む?」は値が空のときに等号を加えるかどうかを指定する。ここは基本的にチェックしておけば間違いない(ただし日本語のエンコードがUTF-8で行われることに注意)。

この状態で実行したときのサーバの出力を以下にあげる。

2005-08-29 13:45:54.833 127.0.0.1
POST /input_form HTTP/1.1
Connection: keep-alive
Content-Length: 64
Content-Type: application/x-www-form-urlencoded
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
name=%E6%9D%B1&job=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E

設定した内容が期待通りリクエストに反映されていることがわかるだろうか。

処理結果を取得・表示する - リスナー

サンプラーが実行した処理結果を取得し、さまざまな形で表示するのがリスナーである。これまでのテストでは「結果をツリーで表示」というリスナーを使ってきたが、ほかのリスナーでも基本的な使い方は変わらない。「結果をツリーで表示」のときと同じように、スレッドグループを右クリックして「追加」→「リスナー」から使用するリスナーを選択するだけで、すべてのサンプラーの処理結果がそれぞれのリスナーに応じた形で表示される。「結果をツリーで表示」や「結果を表で表示」のようにひとつひとつの結果を個別に表示するものや、「統計レポート」や「グラフ表示」のように結果全体のレスポンス時間の統計データ(平均・偏差・中央値など)を表やグラフで表示するものなどがある。図11は比較的見栄えのする「グラフ表示」のサンプルであるが、個々のリスナーがどんな情報をどのように表示するかについては、JMeterのユーザマニュアルを参照しながら実際にリスナーを追加して確認してほしい。

グラフ表示画面
図11

すべてのサンプラーではなく特定のサンプラーのみの結果を取得したい場合は、スレッドグループではなく対象のサンプラーを右クリックし、「追加」→「リスナー」で使用するリスナーを選択する(図12)。するとリスナーはスレッドグループの直下ではなく、選択したサンプラーの下に追加される。

特定サンプラーに対するリスナー追加画面
図12

これまで単一のサンプラーを使った例しか取り上げていないので、この違いはちょっとわかりにくいかもしれない。もちろんJMeterでは複数の処理を一連で実行するテストも可能で、それについては後述する。

リスナーは結果を表示するだけでなく、ファイルに保存することもできる。これはすべてのリスナーに共通の機能である(ファイルへの保存のみを行う「シンプルデータライタ」というリスナーもある)。いずれのリスナーからファイルを保存する場合でも、対象のリスナーを選択して「全てのデータをファイルに出力」の「参照」ボタンをクリックし、出力先のファイルを選択もしくは新規ファイルを入力すればいい。

出力されるファイルはデフォルトでXML形式となっているが、これはCSV形式に変更することもできる。インストールディレクトリの下のbinにあるjmeter.propertiesを開いて、jmeter.save.saveservice.output_formatプロパティを

jmeter.save.saveservice.output_format=csv

のように修正してJMeterを再起動すればいい。以下にCSV形式で保存したサンプルをあげる。

1125381025072,30,HTTP リクエスト,200,OK,スレッドグループ 1-1,text,true
1125381025112,40,HTTP リクエスト,200,OK,スレッドグループ 1-1,text,true
1125381028086,20,HTTP リクエスト,200,OK,スレッドグループ 1-2,text,true
1125381028116,30,HTTP リクエスト,200,OK,スレッドグループ 1-2,text,true
1125381031080,20,HTTP リクエスト,200,OK,スレッドグループ 1-3,text,true
1125381031130,50,HTTP リクエスト,200,OK,スレッドグループ 1-3,text,true

この例ではタイムスタンプ、実行時間、サンプラー名、レスポンスコードとその意味、スレッドグループ名、データタイプ、実行成否が羅列されている。これら出力する項目もjmeter.propertiesで変更可能だ(jmeter.save.saveserviceで始まるプロパティを参照のこと)。

なお現在のJMeterにはXML形式で結果を保存した場合、日本語のエンコードが正常に行われない不具合があるので注意されたい(CSVの場合はUTF-8でエンコードされる)。

もう少し複雑なテスト計画のために

各種設定の共通化 - 設定エレメント

あまり例を複雑にしないで各要素を説明するために、ずっと単一のサンプラーのみを動かしてきたが、実際には一連の操作をテストしたいことの方が多いと思う。JMeterで順次処理を実現するのは簡単で、単純にサンプラーを直列に並べればよい。たとえば最初にメニュー画面を表示し、そこから検索処理を実行する、といった操作であれば、それぞれの処理を行うURLに対応したHTTPリクエストのサンプラーを、スレッドグループの下に順番に追加する。複数のサンプラーが並ぶとそのままではツリーが見づらくなるので、適当に各サンプラーの名前を変えておくといいだろう(図13)。

複数のサンプラーを追加した画面
図13

これを擬似サーバに対して実行した結果を以下にあげる。

2005-08-30 16:08:05.916 127.0.0.1
GET /menu HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-30 16:08:06.667 127.0.0.1
GET /search?keyword=JMeter HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

確かに追加した順序でサンプラーが処理を実行していることがわかる。

このように処理を追加していくこと自体は簡単だが、サンプラーの数が増えてくると、個々のサンプラーについていちいち必要な項目を埋めていくのが面倒になる。そんなとき設定エレメントに複数のサンプラーに共通する項目を書いておくと、個々のサンプラーの記述を省略することができる。

HTTPリクエストの場合であれば、スレッドグループを右クリックして、「追加」→「設定エレメント」→「HTTPリクエスト初期値設定」を選択する(図14)。

HTTPリクエスト初期値設定追加画面
図14

そして表示された画面に対してデフォルトとしたい値を設定する。先ほどの例では「サーバ名またはIP」の部分がふたつのサンプラーで共通しているので、ここに「127.0.0.1」を設定しておくことにしよう(図15)。

HTTPリクエスト初期値設定画面
図15

するとメニュー画面・検索処理の「サーバ名またはIP」の設定を省略することができる。その状態で再び擬似サーバに対して実行すると、面白みはないが先ほどと同じ結果が得られる。

2005-08-30 16:23:51.826 127.0.0.1
GET /menu HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-30 16:23:51.876 127.0.0.1
GET /search?keyword=JMeter HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

基本認証を利用する場合、HTTPリクエストのAuthorizationフィールドに認証情報を設定する。JMeterには共通するHTTPリクエストヘッダを設定するための、HTTPヘッダマネージャという設定エレメントもあるが、基本認証についてはさらにこれに特化したHTTP認証マネージャが存在する。利用するにはスレッドグループを右クリックして、「追加」→「設定エレメント」→「HTTP認証マネージャ」を選択し、表示された画面に必要な認証情報を追加する。たとえば

基底URL ユーザー名 パスワード
http://127.0.0.1/ higashi password

という情報を追加して、擬似サーバに対して処理を実行してみる。

2005-08-30 16:44:47.256 127.0.0.1
GET /menu HTTP/1.1
Connection: keep-alive
Authorization: Basic aGlnYXNoaTpwYXNzd29yZA==
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-30 16:44:47.416 127.0.0.1
GET /search?keyword=JMeter HTTP/1.1
Connection: keep-alive
Authorization: Basic aGlnYXNoaTpwYXNzd29yZA==
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

それぞれのリクエストヘッダに認証情報が追加されていることがわかる。

もうひとつHTTPリクエストを利用する際に便利なのがHTTPクッキーマネージャである。利用の仕方はHTTP認証マネージャなどと同様で、スレッドグループの下に追加すればいい。ただHTTP認証マネージャと違い、固定のCookie情報を登録できるだけでなく、レスポンスヘッダのSet-Cookieフィールドに対応して動作してくれる。最初に認証を行った時点でセッションIDを発行し、それをCookieで持ちまわってセッション管理を行っているWebアプリケーションは少なくないと思うが、そうしたアプリケーションもこのHTTPクッキーマネージャを追加するだけでテストできる。

HTTPクッキーマネージャの動作を確認するため、擬似サーバのソースをちょっと変更しよう。固定のレスポンスを返している部分に適当なSet-Cookieフィールドを追加する。
たとえば以下のように。

            pw.print("HTTP/1.0 200 OK\r\n");
            pw.print("Set-Cookie: session_id=abcdefg12345\r\n");
            pw.print("Content-Type: text/plain\r\n");
            pw.print("Content-Length: 4\r\n");
            pw.print("Connection: close\r\n");
            pw.print("\r\n");
            pw.print("OK\r\n");
            pw.close();

変更後したソースをコンパイルして立ち上げなおした擬似サーバに対して、メニュー画面・検索処理を実行するリクエストを送った結果は以下の通りである。

2005-08-30 20:05:18.390 127.0.0.1
GET /menu HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-30 20:05:18.406 127.0.0.1
GET /search?keyword=JMeter HTTP/1.1
Connection: keep-alive
Cookie: session_id=abcdefg12345
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

これも見づらくて恐縮なのだが、最初のリクエストにはCookieフィールドがなく、一度レスポンスを受けたあとのリクエストにCookieフィールドが存在することがわかるだろうか。

HTTPクッキーマネージャの設定画面には「繰り返しごとにクッキーを破棄しますか?」という項目がある。最初のリクエストでセッションIDを生成する動作をテストしたいのなら、ここにチェックを入れておくといいだろう。

もっと人間らしく - タイマ

これまでのテスト結果を見てもわかる通り、複数のサンプラーあるいは繰り返しの処理では、間髪入れることなく次の処理が動いている。しかしながら実際に人がアプリケーションを操作する場合、それぞれの処理のあいだにはある程度の間隔が存在するはずだ。それを無視してテストを実行しても現実的な検証にはならない。そのあたりをシミュレートするのがタイマである。

JMeterが用意するタイマには以下の4種類がある。 定数スループットタイマ 各サンプラー実行のあいだで、指定された1分あたりのサンプラー実行回数にしたがって処理を待機する。 定数タイマ 各サンプラー実行のあいだで一定時間処理を待機する。 ガウス乱数タイマ 各サンプラー実行のあいだで指定されたガウス乱数で生成された時間だけ処理を待機する。 一様乱数タイマ 各サンプラー実行のあいだで指定された乱数で生成された時間だけ処理を待機する。

タイマもまたスレッドグループの下に追加する。するとスレッドグループの下に並んでいるすべてサンプラーのあいだで、タイマが指定する時間だけウェイトがかかる。具体的なタイマの動きを見るために、たぶん一番わかりにくそうな定数スループットタイマを例に取り上げてみよう。

タイマを追加するのも、ほかの要素と同様にスレッドグループを右クリックして、「追加」→「タイマ」から必要なタイマを選択する。定数スループットタイマであれば図16のようになる。

定数スループットタイマ追加画面
図16

定数スループットタイマの設定画面には「ターゲットスループット(ターゲット数/ミリ秒)」という項目があるが、これはおそらく翻訳の誤りである。実際に指定するのはミリ秒ではなく1分あたりのスループット数だ。すなわち1分間でテスト計画にあるサンプラーが何回実行できるかを指定する。「ターゲットスループット(ターゲット数/ミリ秒)」を「10」と指定すれば、60秒÷10=6秒、つまり各サンプラーのあいだに実行時間を含めて6秒の待機時間が入るというわけだ。

一応、この動作を確認してみよう。スループットという意味を持たせるために擬似サーバに一定のウェイトを入れる。たとえば

            PrintWriter pw = new PrintWriter(client.getOutputStream());
            Thread.sleep(3000);

のように各サンプラーの処理に3秒かかるようにしてみる。その上で定数スループットタイマの「ターゲットスループット(ターゲット数/ミリ秒)」の値を「10」と設定する。そうして擬似サーバに対して実行した結果を以下にあげる。

2005-08-30 21:38:02.656 127.0.0.1
GET /menu HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded
2005-08-30 21:38:08.656 127.0.0.1
GET /search?keyword=JMeter HTTP/1.1
Connection: keep-alive
User-Agent: Java/1.5.0_04
Host: 127.0.0.1
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Content-type: application/x-www-form-urlencoded

サンプラーの処理時間を含めて次の処理が6秒後に動いていることがわかる。

さらに複雑なテストのために - ロジックコントローラ

ここまで紹介してきたテスト計画は、せいぜい複数のサンプラーを順次実行するところまでである。これだけでも負荷テストという意味ではある程度の検証は可能であろう。しかしながらJMeterにはさらに複雑なテスト計画を実行する機能がある。それがロジックコントローラだ。

ロジックコントローラはプログラミング言語が備える基本的な制御構造、条件分岐や繰り返しといった制御を可能にする。まことに申し訳ないのだが、今回は紙面の都合でその詳細を紹介することができない。興味のある方はJMeter付属のユーザマニュアルを参照して試していただきたい。

HTTPプロキシサーバ

設定エレメントを使えばある程度軽減されるとはいえ、サンプラーをひとつひとつ設定していく手間は小さくない。できることならばブラウザ等に対して行った操作をテスト計画として保存して、これを再利用したいという要求はごく自然なものだ。JMeterではHTTPプロキシサーバの機能がそれに応える。HTTPプロキシサーバの使いこなしについては第2部に譲るとして、ここでは簡単な利用手順をざっと見ていこう。

まず左側のフレームの「ワークベンチ」を右クリックし、「追加」→「Non-Testエレメント」→「HTTPプロキシサーバ」を選択する(図17)。

HTTPプロキシサーバ追加画面
図17

HTTPプロキシサーバの設定画面の「ポート番号」にはHTTPプロキシサーバが使用するポート番号を指定する。ほかのプログラムとの競合がなければ「8080」のままでよい。「対象となるコントローラ」には記録として残るサンプラーの保存場所を指定する。説明を割愛したがロジックコントローラのひとつである「記録コントローラ」を使用することもできるし、直接、特定のスレッドグループやワークベンチ、あるいはそのほかのロジックコントローラを指定することもできる。記録したサンプラーは画面上でドラッグすれば任意の場所に移動できるので、この指定にさほど神経質になる必要はない。とりあえずここでは「テスト計画>スレッドグループ」を選択しておくことにしよう(図18。もちろんこれを選択するには、あらかじめテスト計画にスレッドグループが追加されていなければならない)。

HTTPプロキシサーバ設定画面
図18

このほかにもいくつか設定項目があるが、とりあえずそれらはデフォルトの状態でHTTPプロキシサーバを起動してみる。画面の一番下にある「開始」ボタンをクリックすればいい。

これでHTTPプロキシサーバで記録が取れるようになった。今度は実際に操作を行うブラウザからのアクセスをHTTPプロキシサーバ経由で行うように設定する。Internet Explorerを利用する場合、メニューの「ツール」→「インターネットオプション」を選択して、表示されたダイアログの「接続」タブをクリックし、さらに「LANの設定」ボタンをクリックする。「ローカルエリアネットワーク(LAN)の設定」ダイアログが表示されたら、「LANにプロキシサーバーを使用する」にチェックし、「アドレス」を「127.0.0.1」、「ポート」をHTTPプロキシサーバで設定した値(ここでの例だと「8080」)にする(図19)。

IEのプロキシ設定画面
図19

そのほかのブラウザを使う場合も同様にプロキシの設定を行う(たとえばFirefoxであれば、メニューの「ツール」→「オプション」を選択して、表示されたダイアログの「全般」をクリックし、さらに「接続設定」をクリックすればプロキシの設定画面が表示される)。

プロキシの設定ができたら実際にブラウザからテストしたい操作を行えばいい。今回は「http://www.google.co.jp/」にアクセスして「JMeter」というキーワードで検索を行ってみることにする。するとJMeterのスレッドグループの下にブラウザから送られたHTTPリクエストのサンプラーが追加される(図20)。

HTTPプロキシサーバ実行結果
図20

あとはこれにリスナーや設定エレメント等を追加してテスト計画を仕上げていくわけだ。記録が終わったらHTTPプロキシサーバの設定画面の「停止」ボタンでHTTPプロキシサーバを停止すること、ブラウザのプロキシの設定を元に戻しておくことを忘れないようにしよう。

おわりに

ここまでJMeterの基本となる要素を駆け足で説明してきたが、もちろんこれがJMeterのすべてではない。まったく言及していない要素もあれば、言及した要素についてもすべてが説明できているわけではない。また設定に対するJMeterの直接の動作の説明に重点を置いたため、アプリケーションレベルのどういうテストにつながるのかという部分が弱く、「はじめに」で謳った目的が十分に果たせているかもいささか自信がない。

それでもクライアントのシミュレートという意味では、ここに書いてあることを理解できていれば基本となるテスト計画を書くことができるのではないかと思う。そしてテスト計画を書くことができれば一定の負荷テストが可能になり、かりに複雑なシナリオまでを実現できないにせよ、それなりに有意な結果を得ることができるはずだ。少なくとも「はじめに」で触れた例にあるような、負荷テストそのものを実施しない、あるいはツールを使わない負荷テストのみしか行わなかったときのトラブルの多くは回避できるだろう。そして第2部ではJMeterでより精度の高いテストを行うための方法を紹介する。

何よりもまず負荷テストの必要性を認識し、そのためにJMeterというツールを使った具体的な作業をイメージをつかんでいただければ、本稿の最低限の目的は達せられたのではないかと思う。