Apache JMeterによる複雑なシナリオの負荷テスト実装
単一のHTTPエンドポイントに対する軽量な負荷テストにはwrkなどのツールが適していますが、gRPCプロトコルのサポート、複数のAPI呼び出しを組み合わせたトランザクション処理、複数サーバーへの負荷分散など、複雑なシナリオにおける検証にはApache JMeterが最適です。本記事では、gRPCサーバーおよびHTTPサーバーをターゲットとした環境を構築し、JMeterを使用して複合的な負荷テストを行う手順を解説します。
検証用サーバーアプリケーションの実装
まず、JMeterからのリクエストを受け付けるためのターゲットアプリケーションをGo言語で作成します。このアプリケーションは、計算負荷の異なる2つのHTTPエンドポイントと、gRPCサービスを提供します。
プロジェクト構成と初期化
新しいディレクトリstress_test_labを作成し、モジュールを初期化します。
mkdir stress_test_lab
cd stress_test_lab
go mod init stress_test_lab
必要な依存関係を管理するためのgo.modを用意し、以下のディレクトリ構造を作成します。
main.go: サーバーのエントリーポイントproto/: Protocol Buffersの定義ファイルserver/: gRPCおよびHTTPのハンドラー実装
Protocol Buffers定義 (proto/api.proto)
gRPCサービスのインターフェースを定義します。
syntax = "proto3";
package api;
option go_package = "./proto";
service NotificationService {
rpc SendNotification (NotificationRequest) returns (NotificationResponse) {}
}
message NotificationRequest {
string message_body = 1;
}
message NotificationResponse {
string status = 1;
string details = 2;
}
定義ファイルを作成したら、以下のコマンドでGoのコードを生成します。
protoc --go_out=. --go-grpc_out=. proto/api.proto
サーバー実装 (main.go)
HTTPサーバーとgRPCサーバーを同時に起動し、異なる計算負荷を与えるハンドラーを実装します。コードの構造や変数名を変更し、ロジックを整理しています。
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"net/http"
"stress_test_lab/proto"
"time"
)
// GRPCServer implements the proto server interface
type GRPCServer struct {
proto.UnimplementedNotificationServiceServer
}
func main() {
// Start gRPC Server
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen on port 50051: %v", err)
}
grpcSrv := &GRPCServer{}
s := grpc.NewServer()
proto.RegisterNotificationServiceServer(s, grpcSrv)
go func() {
log.Println("Starting gRPC server on :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve gRPC: %v", err)
}
}()
// Start HTTP Server
mux := http.NewServeMux()
mux.HandleFunc("/heavy", heavyLoadHandler)
mux.HandleFunc("/light", lightLoadHandler)
go func() {
log.Println("Starting HTTP server on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatalf("Failed to serve HTTP: %v", err)
}
}()
// Keep program running
select {}
}
// gRPC Handler
func (s *GRPCServer) SendNotification(ctx context.Context, req *proto.NotificationRequest) (*proto.NotificationResponse, error) {
log.Printf("Received gRPC message: %s", req.MessageBody)
return &proto.NotificationResponse{
Status: "SUCCESS",
Details: "Processed by gRPC Server",
}, nil
}
// HTTP Handler: High Load (Fibonacci calculation)
func heavyLoadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, _ = io.ReadAll(r.Body) // Consume body
// Simulate heavy processing
performIntensiveTask(35)
w.Write([]byte("Processed heavy task"))
}
// HTTP Handler: Low Load
func lightLoadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
_, _ = io.ReadAll(r.Body)
// Simulate light processing
performIntensiveTask(0)
w.Write([]byte("Processed light task"))
}
// Helper function for calculation
func performIntensiveTask(n int) int {
if n <= 1 {
return n
}
return performIntensiveTask(n-1) + performIntensiveTask(n-2)
}
JMeterによるテスト計画の作成
環境セットアップ
- JMeterの実行にはJava SDKが必要です。インストール後、公式サイトから最新のJMeterをダウンロードして展開します。
- JMeterはデフォルトではgRPCをサポートしていないため、
jmeter-grpc-requestプラグインをダウンロードし、lib/extディレクトリに配置します。 - グラフ描画などの拡張機能を使用するために、
JMeterPlugins-StandardなどのPlugin Managerもインストールすることを推奨します。 - プラグイン導入後、JMeterを再起動してください。
テスト計画のコンポーネント構成
JMeterのGUIを起動し、以下の要素を追加・設定していきます。
1. テスト計画 (Test Plan)
ルート要素です。ここでテスト全体で使用する変数を定義できます。例えば、サーバーのアドレスなどを変数化しておくと管理が容易です。
2. スレッドグループ (Thread Group)
「追加 > Threads (Users) > Thread Group」から追加します。ここで同時接続ユーザー数(スレッド数)、ループ回数、テスト期間などを設定します。負荷テストの実行リソースを制御する中枢です。
3. トランザクションコントローラ (Transaction Controller)
「追加 > Logic Controller > Transaction Controller」を選択します。これにより、複数のリクエスト(例:HTTPリクエスト + gRPCリクエスト)を1つのトランザクションとしてグループ化できます。すべてのリクエストが成功した場合のみ、そのトランザクションを成功とみなすように設定できます。
4. サンプラー (Samplers)
実際にリクエストを送信するコンポーネントです。
- HTTPリクエスト (HTTP Request Sampler):
トランザクションコントローラ配下に追加します。「サーバー名またはIP」欄に
localhost、「ポート番号」に8080を設定します。 負荷分散の実装: パス(Path)設定において、JMeterの関数${__chooseRandom(/heavy,/light)}を使用することで、2つのエンドポイントに対してランダムにアクセスし、擬似的な負荷分散を行うことができます。 - gRPCリクエスト (gRPC Request Sampler):
同様に追加します。事前に作成した
.protoファイルを指定し、Full MethodからNotificationService/SendNotificationを選択します。リクエストデータはJSON形式で入力します(例:{"message_body": "Test Data"})。
5. アサーション (Assertions)
「追加 > アサーション > レスポンスアサーション」で追加します。レスポンスに特定の文字列(例: "SUCCESS" や "Processed")が含まれているかを検証するルールを設定し、リクエストの正否を判定します。
6. リスナー (Listeners)
結果を可視化およびログ出力するためのコンポーネントです。
- 結果をツリーで表示 (View Results Tree): デバッグ時にリクエスト/レスポンスの詳細を確認するために使用します。
- 集計レポート (Aggregate Report): スループット、平均応答時間、エラー率などの統計データを表形式で表示します。
- レスポンス時間グラフ (Response Time Graph): 時間経過に伴う応答時間の変動をグラフで描画します。
テストの実行と結果の分析
設定が完了したら、ツールバーの「掃除」アイコンで過去の結果をクリアし、「開始」ボタンをクリックしてテストを実行します。
テスト実行中、レスポンス時間グラフを確認すると、計算負荷の高い/heavyエンドポイントにアクセスした際にスパイクが発生し、gRPCリクエストや/lightエンドポイントへのアクセス時は応答が高速であることが視覚的に確認できます。これにより、システム全体のパフォーマンス特性を把握することが可能です。
トラブルシューティング
JMeterは安定したツールですが、GUIの起動やテスト計画の保存時にNoClassDefFoundErrorなどのエラーが発生する場合があります。これはJavaのGUI設定(Look and Feel)とOSの互換性問題によるものです。「オプション > Look and Feel」から「System」や「CrossPlatform」など別のテーマに変更することで解決する場合があります。