PythonとLocustによる負荷テスト(Load Testing)実践ガイド:実用シナリオ作成とAPIメトリクス分析

Python tutorial - IT technology blog
Python tutorial - IT technology blog

主要な負荷テスト(Load Testing)ツールの比較:Apache JMeter、k6、Locust

大規模セールや新機能のリリースを控えている際、バックエンドシステムはどの程度の同時アクセストラフィックに耐えられるでしょうか。事前に負荷測定を行っていないと、トラフィックがピークに達した瞬間にシステムがダウンしてしまう危険性があります。適切なテストツールを選定することで、開発チームは長時間のデバッグ作業を大幅に削減できます。

現在、バックエンドエンジニアが主に検討する代表的なソリューションは以下の3つです。

  • Apache JMeter: Java製の実績豊富な定番ツール。ドラッグ&ドロップのGUIや充実したプラグイン群が強みですが、XMLファイルによる設定は肥大化しやすく、Gitでの変更管理が困難です。
  • k6 (Grafana): Go言語で開発されたモダンなツール。JavaScript (ES6) でテストスクリプトを記述でき、非常に軽量でCI/CDパイプラインとの統合に最適化されています。
  • Locust: 100% Pythonで記述されたオープンソースフレームワーク。通常のPythonコードでユーザーの振る舞いを柔軟に定義でき、リアルタイムに指標を確認できる直感的なWeb UIが標準装備されています。

各ツールのメリット・デメリットの比較分析

インフラ構成やチームの開発スタイルに応じて、各ツールにはそれぞれ強みと弱みがあります。

1. Apache JMeter

  • メリット: ほぼすべての主要プロトコル(HTTP、JDBC、FTP、TCP、LDAP)をサポート。ドキュメントが豊富でコミュニティ規模も大きい。
  • デメリット: 1ユーザーあたり1スレッドを割り当てるモデルのため、2,000〜5,000ユーザー以上をシミュレートする際に大量のCPU/メモリを消費する。XMLファイルが複雑で、チーム開発時のマージ作業が困難。

2. k6

  • メリット: 最適化されたGoランタイムにより、負荷生成速度が極めて高速。JavaScriptスクリプトは可読性が高く、PrometheusやGrafanaへメトリクスを直接スムーズに転送可能。
  • デメリット: 完全なNode.jsランタイム上で動作するわけではないため、任意の外部ライブラリを自由に npm install することはできない。オープンソース版にはローカルのWeb UIが付属していない。

3. Locust

  • メリット: 純粋なPythonコードで記述可能。requests、faker、redis などPythonエコシステムの任意のパッケージを自在に活用できる。gevent によるコルーチンアーキテクチャのおかげで、4コアの開発マシン1台でも5,000〜10,000の仮想ユーザー(Virtual Users)を容易に生成可能。追加のインフラ構築なしでリアルタイムチャートを表示できるWeb UIが標準搭載されている。
  • デメリット: 単一コアあたりの生のスループット(raw throughput)はk6より低い。ただし、LocustはCLIオプション1つでMaster-Workerの分散実行モードを起動できるため、この課題を容易に解決できる。

Python開発チームがLocustを選ぶべき理由

メインの技術スタックがPythonである場合、Locustは最も快適な開発体験を提供します。複雑なGUI操作に慣れる必要も、独自のドメイン特化言語(DSL)の構文を新たに学習する必要もありません。

ログインによるJWTトークンの取得から、テストデータ準備のためのデータベース検索、Fakerを用いたモックデータの生成まで、複雑なシナリオもすべてコードで直接処理できます。for ループ、if/else による条件分岐、例外処理など、すべて慣れ親しんだPythonで完結します。

Locustを使用したAPI負荷テストの実践手順

ステップ1:Locustのインストール

仮想環境(virtual environment)を作成し、pipでLocustをインストールします。

# 仮想環境の作成と有効化
python3 -m venv venv
source venv/bin/activate

# Locustのインストール
pip install locust

# インストールバージョンの確認
locust --version

ステップ2:テストシナリオの作成(locustfile.py)

プロジェクトディレクトリに locustfile.py を作成します。ここではECサイトの一連のフロー(ログインしてJWTトークンを取得し、商品一覧の閲覧およびユーザープロフィールの確認を行う流れ)をシミュレートします。

import json
from locust import HttpUser, task, between

class EcommerceUser(HttpUser):
    # リクエスト間の待機時間(1〜3秒のランダムな待ち時間)
    wait_time = between(1, 3)
    token = None

    def on_start(self):
        """各仮想ユーザーの起動時に1回実行(ログイン処理)"""
        payload = {
            "username": "test_user",
            "password": "Secret@123"
        }
        headers = {"Content-Type": "application/json"}
        
        response = self.client.post("/api/v1/auth/login", json=payload, headers=headers)
        if response.status_code == 200:
            self.token = response.json().get("access_token")
        else:
            response.failure("ログインに失敗しました。トークンを取得できません。")

    @task(3)
    def get_products(self):
        """商品一覧取得タスク(重み3:プロフィール取得タスクの3倍実行)"""
        headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
        with self.client.get("/api/v1/products?page=1&limit=20", headers=headers, catch_response=True) as res:
            if res.status_code == 200:
                res.success()
            else:
                res.failure(f"商品一覧の取得エラー: {res.status_code}")

    @task(1)
    def get_user_profile(self):
        """ユーザープロフィール取得タスク(重み1)"""
        headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
        self.client.get("/api/v1/users/me", headers=headers, name="/api/v1/users/me")

上記のスクリプトにおける重要ポイント:

  • HttpUser: 仮想ユーザーを表すクラス。セッションやCookieを管理するHTTPクライアントが組み込まれています。
  • wait_time = between(1, 3): 実際のユーザーの思考時間(シンクタイム)をシミュレートし、仮想ユーザーによる非現実的な連続リクエストのスパムを防ぎます。
  • on_start: タスク開始前に実行されるライフサイクルフック。アクセストークンを取得するログイン処理などに適しています。
  • @task(weight): リクエストの実行比率(重み付け)を定義します。@task(3) は @task(1) に対して3倍(全体の75%対25%)の割合で実行されます。
  • catch_response=True: ステータスコードが200 OKであっても、レスポンス内容がビジネスロジックに合致しない場合に明示的に失敗(failure)として判定できます。

ステップ3:テストの実行とWeb UIによる操作

ターミナルで以下のコマンドを実行してLocustを起動します。

locust -f locustfile.py --host http://localhost:8000

ブラウザで http://localhost:8089 にアクセスし、以下のパラメータを設定します。

  • Number of users: 200(シミュレートする仮想ユーザーの総数)。
  • Ramp-up (users started/second): 10(目標の200ユーザーに達するまで毎秒10ユーザーずつ増加)。
  • Host: 負荷テスト対象となるバックエンドのベースURL。

Start swarming をクリックして負荷生成を開始し、リアルタイムチャートを監視します。

ステップ4:CI/CDでのヘッドレス(Headless)モード実行

GitHub ActionsやGitLab CIなどのパイプラインに統合する場合、UIを使用しないヘッドレスモードで実行します。

locust -f locustfile.py \
    --headless \
    --users 100 \
    --spawn-rate 10 \
    --run-time 3m \
    --host http://localhost:8000 \
    --html report.html

上記のコマンドは100ユーザーで3分間テストを実行し、視覚的なレポートを report.html ファイルに自動出力します。

APIパフォーマンス指標の分析方法

テスト実行中は、以下の3つの主要指標に注目する必要があります。

1. Requests Per Second (RPS / スループット)

バックエンドが1秒あたりに正常処理できたリクエスト数を測定する指標です。ユーザー数が増加しているにもかかわらずRPSが横ばいまたは低下した場合、バックエンドがボトルネックに達していることを示します(主にデータベースのコネクションプール枯渇やサーバーCPU使用率100%への到達などが原因)。

2. レスポンスタイム(レイテンシのパーセンタイル:50%、95%、99%)

平均応答時間(Average Response Time)だけに依存しないようにしましょう。平均値は、10ms程度の高速なリクエストによって数値が引き下げられ、5秒以上停滞している重いリクエストの存在を見落とす原因になります。

  • 50th Percentile (中央値): 一般的なユーザーの50%が体験する応答時間。
  • 95th Percentile: 全リクエストの95%がこの時間内に完了。SLAを評価する際の標準的な指標(例:p95 < 250msの保証など)。
  • 99th Percentile: 最悪ケースの挙動を把握する指標。データベースのテーブルロックやガベージコレクション(GC)の発生時などに跳ね上がることが多い。

3. エラー率(Failure Rate / %)

5xxエラーコードが返された、あるいはタイムアウトしたリクエストの割合です。標準的な負荷テストにおいて許容されるエラー率は通常1%未満です。この数値が5〜10%に跳ね上がった場合、システムの限界点(ブレーキングポイント)に達したと判断できます。

負荷テスト実施前のチェックリスト

  • テスト実行環境とサーバーの分離: Locustとバックエンドサーバーは別々のホストに配置してください。同一マシンで実行すると、Locust自身がサーバーのCPUリソースを奪い合い、正確な測定結果が得られなくなります。
  • 本番同等環境での実行: 可能な限り本番環境に近いCPU、メモリ、データベースデータ量を備えたステージング/UAT環境でテストします。ローカルマシンのlocalhost経由では仮想的なI/Oボトルネックが発生するため推奨されません。
  • サーバーリソースのモニタリング: 負荷生成中は監視ツール(htop、Prometheus、Datadogなど)を常時稼働させ、CPU、メモリ、ディスクIOPS、データベースのコネクションプール使用率を監視してください。
Share: