シナリオ
単発のリクエストから卒業して、「ログイン → 商品を探す → カートに入れる → 注文する」 という人間の行動の流れを再現します。そのうえで失敗を確実に捕まえ、グラフで眺められるようにします。
STEP 5 ・ シナリオを組む
実務で報告するのは「/api/products が何msでした」ではなく、
「商品検索が何msでした」です。1つの画面遷移が複数のリクエストでできている以上、
まとめて測らないと意味がありません。
用意されたプラン 03_journey_api.jmx を JMeter で開いてください。構造はこうなっています。
テスト計画
└ HTTPリクエスト初期値 ← 宛先をまとめて指定
└ 購入する利用者(スレッドグループ)
├ users.csv を読む ← ユーザーを散らす
├ HTTPヘッダマネージャ ← Content-Type と Authorization
├ T01 ログイン ← トランザクションコントローラ
│ └ POST /api/auth/login
│ └ JWTを抽出 ← JSON抽出
│ └ ステータスが200 ← アサーション
├ 考える時間 0.3〜1.3秒 ← タイマー
├ T02 商品を探す
│ ├ GET /api/products
│ └ GET /api/products/id
│ └ products.csv を読む
├ 考える時間 0.5〜2.5秒
└ T03 カートに入れて注文
├ POST /api/cart/items
└ POST /api/orders
└ 注文番号を抽出
└ 201で注文できること ← アサーション
└ 注文は3秒以内 ← アサーション
JWT の受け渡し — APIでの相関
前章の CSRF は HTML から正規表現で抜きました。JSON API の場合は JSON抽出 を使います。 こちらの方が簡単です。
| 項目 | 値 | 意味 |
|---|---|---|
| 変数名 | token | ${token} で使えるようになる |
| JSON Path式 | $.token | レスポンスJSONの token フィールド |
| デフォルト値 | TOKEN_NOT_FOUND | 抽出失敗が一目で分かるようにする |
抜いた値は HTTPヘッダマネージャ で全リクエストに自動的に付きます。
Content-Type : application/json
Authorization : Bearer ${token}
Accept : application/json
なぜヘッダマネージャに書くのか
リクエストごとに Authorization を書くと、10個あれば10箇所直すことになります。
スレッドグループ直下に1つ置けば全部に効くので、変更が1箇所で済みます。
ログイン前は ${token} が未定義のまま送られますが、
ログインAPIはこのヘッダを見ないので問題ありません。
トランザクションコントローラ
T02 商品を探す の中には2本のリクエストが入っています。 トランザクションコントローラは、この2本の合計時間を1つの数字として記録します。
GET /api/products … 12ms ← 個別の記録
GET /api/products/id … 35ms ← 個別の記録
────────────────────────────────
T02 商品を探す … 47ms ← まとめた記録(これを報告に使う)
設定で1つだけ注意
「子サンプラーの時間を含める(親としてのサンプル生成)」のチェックは外したままにしてください。
入れると個別の記録が消え、どのリクエストが遅かったのか分からなくなります。
「タイマーの遅延時間を含める」も外します。入れると「考える時間」まで応答時間に加算され、
実際の3倍くらい遅い数字が出ます。
考える時間を入れる
人間はページが表示された瞬間に次をクリックしません。この「間」を入れないと、 現実にはありえない密度の攻撃をしていることになります。
| タイマー | 使う場面 |
|---|---|
| 一様乱数タイマー | 普通の「考える時間」。この教材で使用(0.3〜1.3秒など) |
| 固定タイマー | 毎回きっちり同じ間隔。あまり現実的ではない |
| Constant Throughput Timer | 「毎秒600件」など流量を固定したいとき |
| Synchronizing Timer | 全員を待たせて同時に発射する。次章で使用 |
一様乱数タイマーの設定の読み方
「遅延のオフセット 300」「ランダム遅延の最大値 1000」なら、待ち時間は 300ms 〜 1300ms のあいだのランダムになります。 足し算である点に注意してください。
実行する
cd M:\html\jmetertest
docker compose run --rm jmeter 03_journey_api.jmx -Jthreads=5 -Jloops=2
期待値(実測値)
summary = 50 in 00:00:31 = 1.6/s Avg: 6 Min: 2 Max: 27 Err: 0 (0.00%)
Err: 0 (0.00%) であること。 スループットが 1.6/s と極端に低いのは正常です。 1回あたり合計1〜3秒の「考える時間」が入っているためで、 これが人間らしい負荷です。
エラーが出たら
409 (Conflict) … 前回のテストのカートが残っています。次を実行してリセットしてください。
401 … JWTの抽出に失敗しています。JSON抽出の設定を確認してください。
curl.exe -s -X POST -H "x-admin-token: loadlab-admin" http://localhost:8080/api/admin/reset-data
数が合わない理由 — コンソールとレポートの違い
コンソールには 50 と出るのに、HTMLレポートを開くと 80 件ある。
これは不具合ではありません。
| どこ | 件数 | 数えているもの |
|---|---|---|
| コンソールの summary | 50 | HTTPリクエストのみ(5本 × 10回) |
| HTMLレポート / .jtl | 80 | HTTPリクエスト+トランザクション(5+3本 × 10回) |
実際に数えた結果
result.jtl の中身を数えると、全体80行のうち30行が
T01 ログイン T02 商品を探す T03 カートに入れて注文 でした。
残り50行が実際のHTTPリクエストです。報告に使うのはトランザクションの方です。
STEP 6 ・ アサーション — 「速いけど失敗している」を捕まえる
JMeter は既定では HTTPステータスが 200 番台なら成功と判断します。 しかし現実には、
- 200 を返しながら本文に「エラーが発生しました」と書いてあるAPI
- 200 だが中身が空っぽ
- 200 だが 5秒かかっている
といったものが山ほどあります。アサーションを置かないと、これらは全部「成功」として集計されます。
3種類のアサーション
| 種類 | 判定するもの | 設定例 |
|---|---|---|
| 応答アサーション (ステータス) |
HTTPステータスコード | テストする対象=応答コード / パターン一致=等しい / 値=200 |
| 応答アサーション (本文) |
レスポンスの中身 | テストする対象=テキスト応答 / パターン一致=含む / 値="ok":true |
| 継続時間のアサーション | 応答時間の上限 | ミリ秒=3000(3秒を超えたら失敗扱い) |
実験:わざとエラーを混ぜる
練習台には「指定した割合でエラーを返す」スイッチがあります。20%に設定して同じテストを流します。
curl.exe -s -X POST -H "x-admin-token: loadlab-admin" -H "content-type: application/json" -d "{\"error_rate\":20}" http://localhost:8080/api/admin/flags
ブラウザで http://localhost:8080/admin を開き、error_rate に 20 を入れて「適用する」でも同じです。
docker compose run --rm jmeter 03_journey_api.jmx -Jthreads=5 -Jloops=2
期待値(実測値)
summary = 50 in 00:00:34 = 1.5/s Avg: 9 Min: 1 Max: 82 Err: 10 (20.00%)
Err: 10 (20.00%) ── 設定した20%とぴったり一致します。 応答時間(Avg 9ms)はほとんど変わっていない点に注目してください。 「速い=正常」ではないことがよく分かります。
終わったら必ず元に戻します。
curl.exe -s -X POST -H "x-admin-token: loadlab-admin" http://localhost:8080/api/admin/flags/reset
失敗は連鎖する — 実測データから読む
さきほどの実行結果を result.jtl から集計すると、失敗の内訳はこうなっていました。
3 件 500 POST /api/cart/items
2 件 500 POST /api/orders (注文確定)
1 件 500 POST /api/auth/login
1 件 500 GET /api/products/id (詳細)
1 件 401 POST /api/orders (注文確定)
1 件 401 POST /api/cart/items
1 件 400 POST /api/orders (注文確定)
ここから読み取れること
エラーを注入したのは 500 だけなのに、401 と 400 が混ざっています。これは連鎖です。
- ログインが 500 で失敗する
- → JWTが取れない(
TOKEN_NOT_FOUNDのまま) - → 以降のリクエストが 401 未認証 になる
- → カートに何も入らないので、注文が 400 カートが空 になる
実務で効く教訓
エラーの数だけ見て「10件も失敗した」と報告するのは間違いです。 本当の原因は最初の1件で、残りはその巻き添えです。 障害報告では時系列で最初に落ちたものを探すのが鉄則で、 JMeter の結果もそう読みます。
STEP 7 ・ グラフでリアルタイムに見る
JMeter には Backend Listener という部品があり、 テスト実行中にリアルタイムで結果を外部に送れます。 送り先が InfluxDB(時系列データベース)で、それを Grafana で表示します。
JMeter ──(Backend Listener)──▶ InfluxDB ──▶ Grafana(ブラウザで見る)
│
└──▶ 対象サイト ──▶ Prometheus ──▶ Grafana(同じ画面の下半分)
04_stress_influx.jmx にはこの Backend Listener が最初から入っています。
設定は触らなくて構いませんが、中身はこうなっています。
送り先(influxdbUrl)は実行時に上書きできるようになっていて、
上の環境スイッチをサーバーにすると、自動でサーバーのInfluxDB宛てになります。
| 項目 | 値 |
|---|---|
| 実装クラス | InfluxdbBackendListenerClient |
| influxdbUrl | http://influxdb:8086/write?db=jmeter |
| measurement | jmeter |
| percentiles | 50;90;95;99 |
| summaryOnly | false(トランザクション別に送る) |
操作
- ブラウザで http://localhost:3800(Grafana)を開く
- 左メニュー → Dashboards → loadlab フォルダ → 「loadlab — JMeter と サーバー側を並べて見る」 を開く
- 右上の時間範囲を Last 15 minutes、更新間隔を 5s にする
- Grafana を開いたまま、別ウィンドウで次を実行する
docker compose run --rm jmeter 04_stress_influx.jmx -Jthreads=30 -Jramp=10 -Jduration=120
期待値
Grafana のグラフが数秒遅れで動き出します。上半分と下半分で見るものが違います。
- 上半分(JMeter側) … スループット、応答時間p95、エラー件数、実行中のスレッド数
→ スレッド数が10秒かけて0から30まで階段状に上がるのが見えます(Ramp-up) - 下半分(サーバー側) … アプリの応答時間、DBコネクションプール、コンテナCPU、 MySQLのスレッド数、Nodeのイベントループ遅延
サーバーの場合の見に行き先
Grafana はサーバー上で動いているので http://localhost:3800 を開きます
(admin でのログインが必要)。
JMeter は手元で動いていますが、結果はサーバーの InfluxDB に送られるので、
上半分・下半分とも同じ画面で見られます。
この2つを並べる意味
上半分だけ見ても「遅い」としか分かりません。 下半分を同時に見て初めて「なぜ遅いのか」が分かります。 次章はまさにこの読み方の練習です。
グラフが出ないとき
- 時間範囲が古い → 右上を Last 15 minutes に
- 観測用のコンテナが起動していない →
docker compose --profile obs up -d - テストが終わっている → グラフは実行中しか動きません。もう一度流してください
STEP 5-7 完了の判定
- トランザクションコントローラが何をまとめているか説明できる
- 「考える時間」を入れるとスループットが下がるが、それが正しいと分かる
- エラーを20%注入して、ぴったり20%検出できた
- 401や400が「巻き添え」であることを読み取れた
- Grafana でスレッド数が階段状に上がるのを見た