AIエージェントと二人三脚で進める負荷テストのトラブルシューティング体験記

Development
スポンサーリンク
スポンサーリンク

はじめに

JMeterで社内Webアプリの負荷テストを一から組み立てることになり、AIエージェント(Claude Code / Kiro)とペアを組んで進めることにしました。この記事は、その過程で実際にぶつかったトラブルと、AIエージェントとのやり取りを通じてどう切り分け・解決していったかを振り返る体験記です。

このブログでは「JMeter × AIエージェント活用」というテーマで、以下の5本立てのシリーズ記事を公開しています。手順そのものを知りたい方は、まず①の基本ガイドから読むことをおすすめします。

  1. JMeterとは?基本概念とインストール
  2. シナリオ記録実践ガイド
  3. 本番アクセスログからのシナリオ設計
  4. sessionKey動的化のハマりどころ
  5. 本記事(体験記)


スポンサーリンク

この記事で分かること

  • HTTP接続エラーに遭遇したときに、AIエージェントとどう原因を切り分けていったか
  • プロキシ設定・DNS周りの調査が思わぬ方向に転がった顛末
  • PowerShellの一括置換で再発した「BOMなしUTF-8の文字化け」という実務あるある
  • スレッド数を10→50に増やしたときに初めて見えてきた問題
  • AIエージェントと二人三脚で進めることで得た気づき

スポンサーリンク

第1話:記録はできたのに、再生すると繋がらない

シナリオの記録自体は思ったよりスムーズに終わりました。ブラウザ操作をJMeterのHTTPプロキシ機能で記録し、余計な静的リソース(CSS/JS/画像)を除外フィルタで取り除く。ここまではシナリオ記録実践ガイドで解説している手順どおりです。

問題は「記録した内容をそのまま再生」した瞬間に起きました。

Response code: Non HTTP response code: java.net.SocketException
Response message: Non HTTP response message: Connection reset

「さっきブラウザで普通に見えていたページに、なぜJMeterから繋がらないのか」——ここから長い調査が始まりました。AIエージェントに状況を伝えると、まず整理してくれたのはこういう構図です。

記録時はブラウザ→JMeterプロキシ→サーバーという経路だったが、再生時はJMeterが直接サーバーに接続しにいく。社内ネットワークのルーティング次第では、この直接接続が失敗することがある。

「記録できた=再生できる」ではない、という当たり前のようで見落としがちな事実に、AIエージェントの説明で気づかされた瞬間でした。


第2話:犯人はプロキシだと思っていた

接続できない原因を切り分けるため、AIエージェントの指示に従ってcurlnslookupで一つずつ確認していきました。

  • nslookupで対象サーバーの名前解決はできている
  • 直接IPアドレス指定のcurlはタイムアウト
  • 社内プロキシ経由のcurlは504 Gateway Timeout

「名前解決はできているのに繋がらない」「ブラウザでは見えるのにcurlやJMeterでは繋がらない」という矛盾した状況に、正直かなり混乱しました。ここでAIエージェントが提案したのが、ブラウザが参照しているPACファイル(プロキシ自動設定ファイル)の中身を確認することでした。

PACファイルを開いてみると、こんなロジックが書かれていました(内容は一般化して要約しています)。

// 社内ドメイン宛のリクエストは判定関数がDIRECT(プロキシなし直接接続)を返す
if (isPlainHostName(host) || isPrivate(host) || isSpecify(host))
    return "DIRECT";

// ただし、PCが属するネットワークによってはプロキシ経由になる分岐もある
if (isInNet(myIpAddress(), "社内サブネット", "サブネットマスク") && 条件)
    return "PROXY プロキシサーバー:ポート";

つまり「社内の対象システム宛は直接接続」というルールがある一方で、PCの所属ネットワークによっては別のプロキシ経由になる分岐も存在していました。ブラウザはPACファイルの指示どおり賢くルーティングしてくれるのに対し、curlやJMeterは何も考えずに直接接続を試みるため、経路がズレて失敗していたわけです。

ここでJMeterの起動オプションにプロキシ設定(ホスト・ポート・認証情報)を追加し、504 Gateway Timeoutを回避しようとタイムアウト値を伸ばしたりプロキシの有無を切り替えたり……何度も条件を変えて試行錯誤しました。プロキシ設定の変更点は、シナリオ記録実践ガイドで解説しているHTTPプロキシサーバの記録手順とは別物で、「記録用プロキシ」と「再生時にJMeterが使うプロキシ」を混同しないよう整理するのに時間がかかりました。

結果として一部のリクエストは成功するようになったものの、後続の特定のリクエストだけがどうしてもConnection resetで失敗し続けます。「プロキシの経路はもう合っているはずなのに、なぜこの1本だけ通らないのか」——この違和感が、次の展開への伏線でした。


第3話:本当の犯人はsessionKeyだった

行き詰まってAIエージェントに状況を整理してもらったところ、思いがけない指摘がありました。

今の設定(デフォルトのプロキシ状態)のままブラウザで対象システムに普通にアクセスできるなら、JMeterの再生にプロキシ設定変更は不要かもしれません。最初のConnection resetは、プロキシではなくsessionKeyの不整合が原因だった可能性があります。

半信半疑でプロキシ設定を元のデフォルト状態に戻し、ブラウザでアクセスを確認すると、あっさり繋がりました。つまり数時間かけて追いかけていたプロキシ問題は、そもそも本質的な原因ではなかったのです。

振り返ってみると、経緯はこうでした。

  1. 最初の実行(プロキシなし)→ Connection reset実はsessionKeyが原因だった
  2. プロキシ設定を試行錯誤 → 504 Gateway Timeout(プロキシが社内サーバーへの中継に対応していなかっただけ)
  3. プロキシ設定を調整 → 一部成功するも、特定のリクエストだけConnection resetが継続
  4. sessionKeyを動的化 → すべて成功

対象システムは、URLパラメータのsessionKeyとCookieのセッションIDの両方でセッションを検証する作りになっており、記録時に固定値として保存されたsessionKeyと、再生時に新しく発行されるセッションが食い違うことでサーバー側が接続を拒否していました。プロキシ関連のエラーメッセージ(504やConnection reset)に気を取られすぎて、アプリケーション層の問題を疑うのが遅れてしまったのが反省点です。

sessionKeyを動的に抽出してパラメータ化する具体的な手順は、sessionKey動的化のハマりどころにまとめています。

この体験から得た教訓: 「同じようなエラーメッセージが出ているから同じ原因だろう」と決めつけず、成功しているリクエストと失敗しているリクエストの違いを一つずつ洗い出す。AIエージェントに「これとこれの違いは何?」と聞き返すだけでも、思い込みに気づけることがあります。


第4話:見えないバックティックとの戦い

sessionKeyを動的化する対応として、正規表現抽出器でレスポンスからセッション値を取得し、JMXファイル(JMeterのテスト計画ファイル)内に散らばる固定値を${sessionKey}という変数参照に一括置換する作業に入りました。手作業で100箇所以上を書き換えるのは非現実的なので、PowerShellスクリプトでの一括置換を選択しました。

置換自体はあっさり完了したのですが、実行してみると再びエラーが発生します。ファイルの中身を確認してもらうと、AIエージェントが原因を特定してくれました。

loginParam= の後ろに、${sessionKey} ではなく **バックティック付きの `${sessionKey} ** が入り込んでいます。

cmd経由でPowerShellを実行する際、$記号がシェル側の変数展開として解釈されないようにバックティックでエスケープしたのですが、そのバックティック自体が文字としてファイルに書き込まれてしまっていたのです。画面表示ではほぼ気づけないレベルの見た目の違いで、「なぜ変数参照のはずなのに認識されないのか」と数十分悩みました。

さらにもう一つ落とし穴がありました。JMeterで置換後のJMXファイルを開いた際、「保存しますか?」というダイアログで「はい」を選ぶと、JMeterが内部処理でバックティック付きの表記をそのまま保存し直してしまうという挙動です。せっかく手直ししても、保存操作一つで元に戻ってしまう。

この一件から得た運用ルールは次の3点です。

  1. JMXファイルの一括置換は、JMeterを閉じた状態で行う
  2. 置換後にJMeterで開いて「保存しますか?」と聞かれたら、基本は「いいえ」を選ぶ
  3. cmd経由のエスケープ処理はトラブルの元になりやすいので、PS1ファイル(PowerShellスクリプトファイル)を直接実行する方式に切り替える

第5話:実はこれ2回目です——BOMなしUTF-8の文字化け

第4話の教訓を踏まえてPS1ファイル方式に切り替えたのですが、今度は日本語を含むPS1ファイルが文字化けし、パーサーエラーで止まるという別の問題にぶつかりました。しかも後で気づいたのですが、これは今回のプロジェクトで初めてではなく、実は2回目の再発でした。

AIエージェントに「なぜ何度も同じ問題が起きるのか」と聞いたところ、根本原因を整理してくれました。

Windows PowerShell 5.1が-FileパラメータでPS1ファイルを読み込む際、BOMなしUTF-8をUTF-8として認識しません。BOMがない場合、システムの既定エンコーディング(日本語環境ではShift-JIS / CP932)として解釈してしまいます。

  • BOMなしUTF-8 → Shift-JISとして読む → 日本語が文字化け → パーサーエラー
  • BOM付きUTF-8 → UTF-8として正しく読む → OK

AIエージェントのファイル書き込み機能は既定でBOMなしUTF-8で保存するため、日本語を含むPS1ファイルを作成するたびにこの問題が再発しています。

「UTF-8なのに日本語が含まれているのが問題」ではなく、「PowerShell 5.1がBOMの有無でエンコーディングを判定している」というのが本質でした。これは.ps1.batといったレガシーなインタプリタ特有の挙動で、.py.jsのような他の言語では起きません(実際、このブログのプロジェクトルールにも「Windows用スクリプト作成時の必須ルール」として明文化されるきっかけになった問題です)。

対処としては次の2段構えにしました。

  • 恒久対策: PS1ファイルに日本語を直書きしない。名前データなどはCSV/TSVなど外部ファイルから読み込む方式に変更する
  • 保険: それでも完全には避けきれない場合に備え、PS1ファイル作成後に必ずBOM付きUTF-8へ変換するコマンドを実行する運用ルールを追加する

さらに、この問題は一度で終わりませんでした。テストデータをCSVから読み込む方式に変えた後も、[char]コードでUnicode文字を組み立てる行がBOMなしUTF-8のまま解釈された結果、シングルクォートの対応が崩れて別の構文エラーが発生。「日本語リテラルさえ避ければ安全」という思い込みも通用しないケースがあることを学びました。最終的には「PS1ファイルを生成したら、内容に関わらず機械的にBOM付きUTF-8へ変換する」という一手間を運用フローに組み込むことで、この問題からようやく解放されました。

同じ失敗を繰り返さないために、AIエージェントとのやり取りの中でルール化・文書化しておく——これが地味ですが一番効いた対策でした。


第6話:スレッド数10→50、まさかの伏兵

sessionKeyの動的化まで終わり、10スレッドでの実行がようやく安定して成功するようになりました。次のステップは、本番のピーク時間帯の同時アクセス数に近づけるためのスケールアップです。本番アクセスログからのシナリオ設計で分析した実際のアクセスパターンをもとに、まずは50スレッドを目標に設定しました。

スケールアップにあたって必要だったのは、主に次の3つです。

  1. テストデータ(CSV/TSV)を10件分から50件分に拡張する
  2. DB側にも50件分のテストデータを投入する
  3. スレッドグループのRamp-Up期間(起動間隔)を見直す

Ramp-Upの設定では、AIエージェントとこんなやり取りがありました。10スレッド時はRamp-Up 30秒(3秒間隔で1スレッドずつ起動)にしていたので、単純に同じ比率で計算すると50スレッドでは150秒になります。しかし——

本番のピーク時間帯には多数のユーザーがほぼ同時に操作を開始していました。実態に近づけるなら、150秒(3秒間隔)ではなく、もっと短い間隔で起動する方が適切かもしれません。

比率をそのまま引き延ばすのではなく、「何のためにスレッド数を増やすのか」という目的(本番ピークの再現)に立ち返って間隔を決め直す。数字の整合性だけでなく目的に立ち返って判断する視点は、一人で黙々と設定変更していたら見落としていたかもしれません。

そして実際に50スレッドで実行してみると、10スレッドでは一度も出なかった新しいエラーが発生しました。エラーが起きたスレッドとタイミングをAIエージェントに集計してもらったところ、興味深いパターンが見えてきました。

  • 同じ10本のスレッドが、2回とも一貫してエラーになっている
  • エラーはランダムではなく特定のスレッドに偏っている

「ランダムに失敗するならデータの偶然、特定のスレッドだけが毎回失敗するなら処理ロジックか競合状態」——この切り分けの視点はAIエージェントに教わったものでした。サーバー側のアプリケーションログを確認すると、原因は「型キャスト例外」。同時アクセス数が増えたことで、セッションへの書き込みタイミングが競合し、一部のスレッドで想定と異なるオブジェクトがセッションに格納されてしまっていたのです。10スレッドでは発生しなかった競合状態が、50スレッドという同時実行数の増加によって初めて表面化した典型例でした。

あわせて、サーバーのCPU使用率も90%近くまで到達しており、「これ以上スレッド数を増やすなら、開発環境ではなく本番相当のスペックを持つ環境で検証すべき」という判断材料も得られました。スレッド数を上げること自体は成功しても、それによって初めて可視化される問題があるという点は、負荷テストの本来の目的を再確認させられる出来事でした。


まとめ:AIエージェントと二人三脚で進めて分かったこと

一連のトラブルシューティングを振り返って、AIエージェントと組んで進めたことで良かった点と、逆に自分自身が気をつけるべきだと感じた点をまとめます。

良かった点

  • エラーメッセージだけを見て思考停止せず、「成功しているリクエストと失敗しているリクエストの違いは何か」といった切り分けの視点を毎回言語化して示してくれる
  • 一度解決した問題(BOM文字化けなど)を「なぜ起きるのか」の根本原因まで説明してもらえるので、その場しのぎで終わらせず再発防止のルール化につなげやすい
  • プロキシ、DNS、sessionKey、セッション競合と、レイヤーの異なる複数の仮説を出してくれるので、自分一人では気づきにくい方向にも調査を広げられる

自分自身が気をつけたい点

  • AIエージェントが提示した仮説(今回で言えばプロキシ)に飛びつきすぎず、「本当にそれで説明がつくか」を都度立ち止まって検証する。第3話のように、実は的外れな仮説を数時間追いかけてしまうこともある
  • 再発した問題(BOM文字化け)に対しては、その場の対処だけでなく、プロジェクトのルールとして明文化しておく。次に同じ罠にハマる時間を減らせる
  • スレッド数を増やすといった変更は、「動くかどうか」だけでなく「何のためにその設定にするのか」という目的に立ち返って判断する

今後は、こうした試行錯誤の過程そのものを事後にまとめ直すのではなく、AIエージェントとの対話ログを都度メモ化しておき、次のプロジェクトで同じ壁にぶつかったときにすぐ参照できる形にしていきたいと考えています。


シリーズ記事一覧


AIエージェントとの協働をもっと学びたい方へ

今回の体験記で紹介したように、AIエージェント(Claude Code)とのペア作業は「原因の切り分け」「再発防止のルール化」といった場面で特に力を発揮します。Claude Codeを使ったハンズオン形式の入門講座もあるので、興味のある方はチェックしてみてください。

👉 講座の詳細を見る(Udemyで確認)

コメント