STEP 5-7 ・ 所要 90分

シナリオ

単発のリクエストから卒業して、「ログイン → 商品を探す → カートに入れる → 注文する」 という人間の行動の流れを再現します。そのうえで失敗を確実に捕まえ、グラフで眺められるようにします。

STEP 5 ・ シナリオを組む

観点 個々のリクエストではなく「画面遷移」の単位で測る 時間 30分

実務で報告するのは「/api/products が何msでした」ではなく、 「商品検索が何msでした」です。1つの画面遷移が複数のリクエストでできている以上、 まとめて測らないと意味がありません。

用意されたプラン 03_journey_api.jmx を JMeter で開いてください。構造はこうなっています。

03_journey_api.jmx の構造
テスト計画
 └ 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ヘッダマネージャ で全リクエストに自動的に付きます。

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 のあいだのランダムになります。 足し算である点に注意してください。

実行する

PowerShell
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 件ある。 これは不具合ではありません。

どこ件数数えているもの
コンソールの summary50HTTPリクエストのみ(5本 × 10回)
HTMLレポート / .jtl80HTTPリクエスト+トランザクション(5+3本 × 10回)

実際に数えた結果

result.jtl の中身を数えると、全体80行のうち30行が T01 ログイン T02 商品を探す T03 カートに入れて注文 でした。 残り50行が実際のHTTPリクエストです。報告に使うのはトランザクションの方です。

STEP 6 ・ アサーション — 「速いけど失敗している」を捕まえる

観点 アサーションが無いと、失敗を成功として数えてしまう 時間 30分

JMeter は既定では HTTPステータスが 200 番台なら成功と判断します。 しかし現実には、

  • 200 を返しながら本文に「エラーが発生しました」と書いてあるAPI
  • 200 だが中身が空っぽ
  • 200 だが 5秒かかっている

といったものが山ほどあります。アサーションを置かないと、これらは全部「成功」として集計されます。

3種類のアサーション

種類判定するもの設定例
応答アサーション
(ステータス)
HTTPステータスコード テストする対象=応答コード / パターン一致=等しい / 値=200
応答アサーション
(本文)
レスポンスの中身 テストする対象=テキスト応答 / パターン一致=含む / 値="ok":true
継続時間のアサーション 応答時間の上限 ミリ秒=3000(3秒を超えたら失敗扱い)

実験:わざとエラーを混ぜる

練習台には「指定した割合でエラーを返す」スイッチがあります。20%に設定して同じテストを流します。

PowerShell — 20%の確率で500を返すようにする
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 から集計すると、失敗の内訳はこうなっていました。

失敗10件の内訳(実測)
  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 だけなのに、401400 が混ざっています。これは連鎖です。

  1. ログインが 500 で失敗する
  2. → JWTが取れない(TOKEN_NOT_FOUND のまま)
  3. → 以降のリクエストが 401 未認証 になる
  4. → カートに何も入らないので、注文が 400 カートが空 になる

実務で効く教訓

エラーの数だけ見て「10件も失敗した」と報告するのは間違いです。 本当の原因は最初の1件で、残りはその巻き添えです。 障害報告では時系列で最初に落ちたものを探すのが鉄則で、 JMeter の結果もそう読みます。

STEP 7 ・ グラフでリアルタイムに見る

観点 数字の羅列ではなく、時間の流れで形を見る 時間 30分

JMeter には Backend Listener という部品があり、 テスト実行中にリアルタイムで結果を外部に送れます。 送り先が InfluxDB(時系列データベース)で、それを Grafana で表示します。

データの流れ
JMeter ──(Backend Listener)──▶ InfluxDB ──▶ Grafana(ブラウザで見る)
   │
   └──▶ 対象サイト ──▶ Prometheus ──▶ Grafana(同じ画面の下半分)

04_stress_influx.jmx にはこの Backend Listener が最初から入っています。 設定は触らなくて構いませんが、中身はこうなっています。 送り先(influxdbUrl)は実行時に上書きできるようになっていて、 上の環境スイッチをサーバーにすると、自動でサーバーのInfluxDB宛てになります

項目
実装クラスInfluxdbBackendListenerClient
influxdbUrlhttp://influxdb:8086/write?db=jmeter
measurementjmeter
percentiles50;90;95;99
summaryOnlyfalse(トランザクション別に送る)

操作

  1. ブラウザで http://localhost:3800(Grafana)を開く
  2. 左メニュー → Dashboardsloadlab フォルダ → 「loadlab — JMeter と サーバー側を並べて見る」 を開く
  3. 右上の時間範囲を Last 15 minutes、更新間隔を 5s にする
  4. Grafana を開いたまま、別ウィンドウで次を実行する
PowerShell — 2分間の負荷
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 でスレッド数が階段状に上がるのを見た