0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ワークロードモデリングとボトルネック分析で設計する実践パフォーマンステスト戦略

0
Last updated at Posted at 2026-04-17

ワークロードモデリングとボトルネック分析で設計する実践パフォーマンステスト戦略

「とりあえず1000ユーザーで5分回す」——根拠のない負荷テストは時間の無駄です。本記事では、本番トラフィックデータからワークロードを数理的にモデリングし、USE/REDメソッドでボトルネックを体系的に特定し、ML推論サービスへの応用まで含めた設計主導のパフォーマンステスト戦略を解説します。

この記事でわかること

  • Little's Lawを使った本番ワークロードの数理モデリング手法と仮想ユーザー数の算出方法
  • USE(Utilization/Saturation/Errors)メソッドとRED(Rate/Errors/Duration)メソッドによる体系的ボトルネック分析
  • k6・Locustを使った実践的なテストシナリオの実装とOpenTelemetry連携
  • ML推論サービス(vLLM等)のパフォーマンステストで押さえるべきメトリクスと設計パターン
  • テスト結果を次のアクションに繋げるための分析・レポーティング手法

対象読者

  • 想定読者: バックエンドAPIやML推論サービスの品質に責任を持つエンジニア
  • 必要な前提知識:
    • HTTP/RESTful APIの基本的な仕組み
    • PythonまたはJavaScript/TypeScriptの基礎文法
    • コマンドライン操作の基本(k6やLocustのインストール・実行)
    • MLエンジニアの方へ: ML推論の基本概念(推論レイテンシ、スループット)を理解していれば、負荷テスト未経験でもOKです

結論・成果

本番ワークロードデータに基づいてテストを設計することで、以下の効果が報告されています。

  • 障害検出の前倒し: Grafana Labsによると、Shift-Leftアプローチでパフォーマンス問題の検出を開発初期に移すことで、本番障害の修正コストを削減できると報告されている(Performance testing best practices - Grafana Labs)
  • リソース効率: k6はgoroutineあたり約100KBのメモリで動作し、JMeterのスレッドあたり約1MBと比較して約10倍のメモリ効率を実現。CPU使用量もJMeter比で約70%削減されると報告されている(Vervali 2026 comparison)
  • ML推論の最適化: vLLMのベンチマークでは、Llama 3.1 8BモデルがH100 80GB上で約12,500 tokens/秒のスループットとTTFT 72msを達成(vLLM Benchmarks - MorphLLM)。適切な負荷テストでこれらの指標を継続的に検証することが、本番品質の維持に不可欠です

本番トラフィックからワークロードをモデリングする

負荷テストの設計で最初にやるべきことは、本番の実際のトラフィックパターンを数値で把握することです。「たぶんこのくらいだろう」という推測ではなく、アクセスログ・APMデータを起点にした数理モデリングが信頼できるテスト設計の第一歩です。

Little's Lawで仮想ユーザー数を算出する

ワークロードモデリングの中核はLittle's Law(リトルの法則)です。これは待ち行列理論の基本定理で、ML分野でいうバッチサイズとスループットの関係に似ています。

Little's Law:

N = X × R
変数 意味 取得元
N 同時仮想ユーザー数(Concurrent VUs) 算出値
X スループット(TPS: Transactions Per Second) APMツール / アクセスログ
R 滞在時間 = レスポンスタイム + シンクタイム APMツール + セッション分析

たとえば、本番のAPIが平均レスポンスタイム200ms、ユーザーのシンクタイム(次のリクエストまでの待ち時間)が3秒、ピーク時のスループットが50 TPSの場合、必要な仮想ユーザー数は以下のように算出できます。

# ワークロードモデリング計算例
response_time_sec = 0.2      # 200ms
think_time_sec = 3.0          # ユーザーのシンクタイム
peak_tps = 50                 # 本番ピーク時TPS

residence_time = response_time_sec + think_time_sec  # R = 3.2秒
concurrent_vus = peak_tps * residence_time            # N = 50 × 3.2 = 160 VUs

growth_factor = 1.25  # 25%の成長マージン
test_vus = int(concurrent_vus * growth_factor)         # 200 VUs

print(f"ピーク時同時VU数: {concurrent_vus}")    # 160.0
print(f"テスト用VU数(成長マージン込み): {test_vus}")  # 200

ピーク時TPSを本番データから導出する

ピーク時TPSの算出にはPeak Hour Factor(PHF)を使います。PHFは1日のトラフィックのうち最も忙しい1時間の比率で、WebアプリケーションではPythonのログ分析等で確認すると、一般的に0.15〜0.25の範囲になります(Workload Modelling - Santhosh Kumar)。

# Peak Hour Factorからピーク時TPSを算出
daily_transactions = 500_000   # 1日の総トランザクション数(アクセスログから)
peak_hour_factor = 0.20        # PHF(ログ分析で算出)
growth_factor = 1.25           # 成長マージン

peak_hour_transactions = daily_transactions * peak_hour_factor  # 100,000
peak_tps = peak_hour_transactions / 3600                         # 27.8 TPS
test_tps = peak_tps * growth_factor                              # 34.7 TPS

print(f"ピーク時TPS: {peak_tps:.1f}")
print(f"テスト用TPS(成長マージン込み): {test_tps:.1f}")

注意点:

PHFは必ず本番のアクセスログから算出してください。推測値を使うと、テスト結果が本番の実態と乖離します。季節変動がある場合(ECサイトのセール時期など)は、年間の最大PHFを基準にするのが安全です。

トランザクションミックスを定義する

実際のシステムでは、ユーザーの操作は単一ではありません。API別のアクセス比率(トランザクションミックス)を定義することで、本番に近い負荷パターンを再現できます。

# トランザクションミックスの例(ECサイトAPI)
transaction_mix = {
    "GET /api/products":       {"ratio": 0.45, "requests_per_tx": 1},
    "GET /api/product/:id":    {"ratio": 0.25, "requests_per_tx": 1},
    "POST /api/cart":          {"ratio": 0.15, "requests_per_tx": 2},
    "POST /api/checkout":      {"ratio": 0.05, "requests_per_tx": 5},
    "GET /api/recommendations": {"ratio": 0.10, "requests_per_tx": 1},
}

test_tps = 34.7  # 前のステップで算出
total_rps = 0

for endpoint, config in transaction_mix.items():
    tx_tps = test_tps * config["ratio"]
    rps = tx_tps * config["requests_per_tx"]
    total_rps += rps
    print(f"{endpoint}: {tx_tps:.1f} TPS → {rps:.1f} RPS")

print(f"\n合計RPS: {total_rps:.1f}")
# GET /api/products:       15.6 TPS → 15.6 RPS
# GET /api/product/:id:     8.7 TPS →  8.7 RPS
# POST /api/cart:            5.2 TPS → 10.4 RPS
# POST /api/checkout:        1.7 TPS →  8.7 RPS
# GET /api/recommendations:  3.5 TPS →  3.5 RPS
# 合計RPS: 46.9

よくある間違い: トランザクションミックスを定義せず、全エンドポイントに均等に負荷をかけてしまうケースがあります。本番では商品一覧の参照(GET)が大半で、決済(POST)はごく一部です。均等配分のテストでは、決済エンドポイントへの過剰な負荷により本番では起きない問題を検出してしまい、逆に商品一覧の高負荷時の問題を見逃す可能性があります。

k6とLocustで実践的なテストシナリオを実装する

ワークロードモデリングの結果をもとに、実際のテストスクリプトを実装していきましょう。ここではk6(JavaScript/TypeScript)とLocust(Python)の両方の実装例を示します。

k6でワークロードモデルを実装する

k6 1.0ではネイティブTypeScript対応が実現し、型安全なテストスクリプトを記述できるようになりました(k6 documentation)。以下は、前セクションで設計したワークロードモデルをk6スクリプトに落とし込む例です。

// k6テストスクリプト: ワークロードモデル実装例
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';

const errorRate = new Rate('errors');
const productListDuration = new Trend('product_list_duration');

export const options = {
  scenarios: {
    // Average-Loadテスト: ピーク時の本番相当
    average_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '2m', target: 200 },  // Ramp-up(全体の10-20%)
        { duration: '10m', target: 200 }, // Steady-state
        { duration: '2m', target: 0 },    // Ramp-down
      ],
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
    errors: ['rate<0.01'],
    product_list_duration: ['p(95)<300'],
  },
};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';

export default function () {
  // トランザクションミックスを確率で再現
  const rand = Math.random();

  if (rand < 0.45) {
    // 商品一覧取得(45%)
    const res = http.get(`${BASE_URL}/api/products?page=1&limit=20`);
    productListDuration.add(res.timings.duration);
    check(res, { 'products status 200': (r) => r.status === 200 });
    errorRate.add(res.status !== 200);
  } else if (rand < 0.70) {
    // 商品詳細取得(25%)
    const productId = Math.floor(Math.random() * 1000) + 1;
    const res = http.get(`${BASE_URL}/api/product/${productId}`);
    check(res, { 'product detail status 200': (r) => r.status === 200 });
    errorRate.add(res.status !== 200);
  } else if (rand < 0.85) {
    // カート追加(15%)
    const payload = JSON.stringify({ product_id: Math.floor(Math.random() * 1000) + 1, quantity: 1 });
    const params = { headers: { 'Content-Type': 'application/json' } };
    const res = http.post(`${BASE_URL}/api/cart`, payload, params);
    check(res, { 'cart add status 2xx': (r) => r.status >= 200 && r.status < 300 });
    errorRate.add(res.status >= 400);
  } else if (rand < 0.90) {
    // チェックアウト(5%)
    const payload = JSON.stringify({ payment_method: 'card' });
    const params = { headers: { 'Content-Type': 'application/json' } };
    const res = http.post(`${BASE_URL}/api/checkout`, payload, params);
    check(res, { 'checkout status 2xx': (r) => r.status >= 200 && r.status < 300 });
    errorRate.add(res.status >= 400);
  } else {
    // レコメンデーション取得(10%)
    const res = http.get(`${BASE_URL}/api/recommendations?user_id=42`);
    check(res, { 'recommendations status 200': (r) => r.status === 200 });
    errorRate.add(res.status !== 200);
  }

  // シンクタイム: 本番ユーザー行動に基づく(2-5秒の正規分布近似)
  sleep(Math.random() * 3 + 2);
}

実行コマンド:

k6 run --out json=results.json load_test.js

なぜk6を選んだか:

  • JavaScript/TypeScriptでスクリプトを記述でき、フロントエンドチームとの知識共有がしやすい
  • goroutineベースで1VUあたり約100KBと軽量。200VUのテストでも約20MBのメモリで動作する
  • thresholdsでSLO違反を自動検出し、CIパイプラインの品質ゲートとして利用可能

注意点:

k6はHTTP/gRPC/WebSocket中心のプロトコル対応です。JDBC、JMS、LDAPなどのレガシープロトコルが必要な場合は、JMeter(20以上のプロトコルをネイティブサポート)の方が適しています。

Locustでワークロードモデルを実装する

Pythonチーム、特にMLエンジニアにとっては、Locustの方が馴染みやすい選択です。Locust 2.43+ではOpenTelemetry自動計装機能が追加され、負荷テスト結果と既存のObservability基盤を統合できるようになりました(Locust Observable Load Testing)。

# locustfile.py: ワークロードモデル実装例
from locust import HttpUser, task, between

class ECommerceUser(HttpUser):
    wait_time = between(2, 5)  # シンクタイム: 2-5秒
    host = "http://localhost:8080"

    @task(45)  # トランザクションミックス: 45%
    def get_products(self):
        with self.client.get(
            "/api/products?page=1&limit=20",
            name="/api/products",
            catch_response=True,
        ) as response:
            if response.status_code != 200:
                response.failure(f"Status {response.status_code}")

    @task(25)
    def get_product_detail(self):
        product_id = self.environment.runner.user_count % 1000 + 1
        self.client.get(
            f"/api/product/{product_id}",
            name="/api/product/:id",
        )

    @task(15)
    def add_to_cart(self):
        self.client.post(
            "/api/cart",
            json={"product_id": 42, "quantity": 1},
            name="/api/cart",
        )

    @task(5)
    def checkout(self):
        self.client.post(
            "/api/checkout",
            json={"payment_method": "card"},
            name="/api/checkout",
        )

    @task(10)
    def get_recommendations(self):
        self.client.get(
            "/api/recommendations?user_id=42",
            name="/api/recommendations",
        )

OpenTelemetry連携を有効にして実行する場合:

# LocustのOTel自動計装を有効化
pip install locust[otel]

export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
export OTEL_SERVICE_NAME="ecommerce-loadtest"

locust -f locustfile.py --otel --users 200 --spawn-rate 10 --run-time 14m

--otelフラグを付けるだけで、HTTP/Sリクエストが自動的にOpenTelemetryのメトリクスとトレースを出力します。W3C Trace Contextヘッダーも自動伝搬されるため、負荷テストのリクエストがバックエンドの分散トレーシングとシームレスに紐付きます。

USE/REDメソッドでボトルネックを体系的に特定する

テストを実行したら、次は結果の分析です。「レスポンスタイムが遅い」だけでは改善アクションに繋がりません。USEメソッドとREDメソッドを組み合わせることで、ボトルネックの所在をインフラ層とサービス層に切り分けて特定できます。

USEメソッド: インフラ層のボトルネックを特定する

USEメソッドはBrendan Gregg氏が考案した手法で、すべてのリソース(CPU、メモリ、ディスク、ネットワーク)に対して3つの指標を確認します(USE and RED Method - PagerTree)。MLエンジニア向けに説明すると、これはモデル学習時のGPU/CPU/メモリの使用状況監視と同じ考え方です。

指標 定義 確認対象の例 危険水準の目安
Utilization(使用率) リソースがビジー状態の時間割合 CPU使用率、メモリ使用率、ディスクI/O使用率 >80%
Saturation(飽和度) リソースの待ち行列の深さ CPU run queue、メモリswap、ディスクI/O wait >0が継続
Errors(エラー) リソースのエラーイベント数 NIC packet drops、disk errors、OOM kills >0

実践的なUSEチェックリスト(Linux環境):

# CPU: Utilization / Saturation / Errors
vmstat 1 5                    # us+sy=Utilization, r>CPU数=Saturation
mpstat -P ALL 1 5             # コアごとの偏りを確認

# メモリ: Utilization / Saturation
free -h                       # used/total=Utilization
vmstat 1 5                    # si/so>0=Swap Saturation(メモリ不足の兆候)

# ディスクI/O: Utilization / Saturation
iostat -xz 1 5               # %util=Utilization, avgqu-sz=Saturation

# ネットワーク: Utilization / Errors
sar -n DEV 1 5               # rxkB/s, txkB/s=Utilization
ip -s link show              # RX errors/drops=Errors

# GPU(ML推論サーバー用)
nvidia-smi --query-gpu=utilization.gpu,utilization.memory,memory.used,memory.total --format=csv -l 1

ハマりポイント: CPU使用率が80%を超えていなくても、特定のコアだけ100%に張り付いている場合があります。mpstat -P ALLでコアごとの使用率を確認しないと、シングルスレッドのボトルネックを見逃します。

REDメソッド: サービス層のボトルネックを特定する

REDメソッドはTom Wilkie氏が考案した手法で、マイクロサービスの各エンドポイントに対して3つの指標を確認します(RED Method Guide - SRE School)。

指標 定義 Prometheusメトリクス例
Rate(リクエスト率) 秒間リクエスト数 rate(http_requests_total[5m])
Errors(エラー率) 失敗リクエストの割合 rate(http_requests_total{status=~"5.."}[5m])
Duration(処理時間) リクエストの所要時間分布 histogram_quantile(0.95, http_request_duration_seconds_bucket)

以下は、PrometheusとGrafanaでREDダッシュボードを構築するためのクエリ例です。

# Grafanaダッシュボード: REDメトリクスパネル設定例
# Rate パネル
- title: "Request Rate"
  query: |
    sum(rate(http_requests_total{service="ecommerce-api"}[5m])) by (endpoint)

# Errors パネル
- title: "Error Rate (%)"
  query: |
    100 * sum(rate(http_requests_total{service="ecommerce-api",status=~"5.."}[5m]))
    / sum(rate(http_requests_total{service="ecommerce-api"}[5m]))

# Duration パネル(p95)
- title: "p95 Latency"
  query: |
    histogram_quantile(0.95,
      sum(rate(http_request_duration_seconds_bucket{service="ecommerce-api"}[5m])) by (le, endpoint)
    )

USEとREDを組み合わせた分析フロー

USEとREDは競合する手法ではなく、補完的に使い分けるものです。以下のフローで分析を進めます。

  1. REDでサービス層を確認: Duration(p95/p99)が閾値を超えているエンドポイントを特定
  2. 該当サービスのUSEを確認: そのサービスが動作するホストのCPU/メモリ/ディスク/ネットワークを確認
  3. リソースが飽和していれば: スケールアップ/スケールアウトを検討
  4. リソースに余裕があれば: アプリケーション層の問題(N+1クエリ、非効率なアルゴリズム、ロック競合など)を調査

よくある間違い: REDのDurationが悪化した時にすぐにサーバーを増やす判断をしがちですが、USEでリソースに余裕がある場合はスケールアウトしても改善しません。まずはアプリケーションプロファイリング(py-spy、async-profiler等)でコード上のボトルネックを特定しましょう。

ML推論サービスのパフォーマンステストを設計する

MLエンジニアが最も関心を持つ領域として、ML推論サービス特有のパフォーマンステストを解説します。REST APIの負荷テストとは異なるメトリクスと設計上の注意点があります。

ML推論サービス固有のメトリクス

通常のWebサービスではレスポンスタイムとスループットが主要メトリクスですが、ML推論サービス(特にLLM)では以下のメトリクスが重要です。

メトリクス 定義 目安値(2026年)
TTFT(Time to First Token) リクエストから最初のトークン生成までの時間 <200ms(対話型)、<500ms(バッチ型)
TPOT(Time Per Output Token) 出力トークンあたりの生成時間 <30ms(リアルタイム)、<100ms(バッチ)
Throughput(tokens/sec) 秒間生成トークン数 モデル・GPU依存(下表参照)
GPU Utilization GPU演算ユニットの使用率 70-90%が効率的
KV Cache Hit Rate キーバリューキャッシュのヒット率 >80%(プレフィックスキャッシュ有効時)

vLLMの2026年ベンチマークでは、以下のスループットが報告されています(vLLM Benchmarks - MorphLLM)。

モデル GPU スループット TTFT(平均)
Llama 3.1 8B(BF16) 1x H100 80GB 約12,500 tok/s 72ms
DeepSeek-R1 7B 1x A100 80GB 3,363 tok/s —
Llama 3.1 70B(FP8) 1x H100 80GB 460 tok/s —
GPT-OSS-120B 2x H100 80GB 4,741 tok/s 261ms

制約条件: これらの数値はバッチサイズ、入力/出力トークン比率、量子化方式、プレフィックスキャッシュのヒット率によって大きく変動します。自社の本番ワークロードに近い条件でベンチマークすることが不可欠です。

Locustを使ったLLM推論負荷テストの実装

LLM推論エンドポイント(OpenAI互換API)に対するLocust負荷テストの実装例を示します。

# locustfile_llm.py: LLM推論サービスの負荷テスト
import time
import json
from locust import HttpUser, task, between

class LLMInferenceUser(HttpUser):
    wait_time = between(1, 3)
    host = "http://localhost:8000"  # vLLMサーバー

    # 本番の入力長分布を再現するプロンプトセット
    prompts = [
        {"text": "Pythonでバブルソートを実装してください。", "max_tokens": 256},
        {"text": "機械学習モデルの過学習を防ぐ方法を3つ説明してください。", "max_tokens": 512},
        {"text": "以下のコードのバグを特定して修正してください。\n\ndef fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)\n\n# 大きなnで非常に遅い", "max_tokens": 1024},
    ]

    @task
    def chat_completion(self):
        prompt = self.prompts[self.environment.runner.user_count % len(self.prompts)]

        start_time = time.time()
        first_token_time = None

        with self.client.post(
            "/v1/completions",
            json={
                "model": "meta-llama/Llama-3.1-8B-Instruct",
                "prompt": prompt["text"],
                "max_tokens": prompt["max_tokens"],
                "temperature": 0.7,
                "stream": True,
            },
            stream=True,
            catch_response=True,
            name="/v1/completions",
        ) as response:
            if response.status_code != 200:
                response.failure(f"Status {response.status_code}")
                return

            token_count = 0
            for line in response.iter_lines():
                if line:
                    decoded = line.decode("utf-8")
                    if decoded.startswith("data: ") and decoded != "data: [DONE]":
                        if first_token_time is None:
                            first_token_time = time.time()
                        token_count += 1

            total_time = time.time() - start_time
            ttft = (first_token_time - start_time) if first_token_time else total_time
            tpot = (total_time - ttft) / max(token_count - 1, 1) if token_count > 1 else 0

            # カスタムメトリクスとして記録
            self.environment.events.request.fire(
                request_type="LLM",
                name="TTFT",
                response_time=ttft * 1000,  # ms
                response_length=0,
                exception=None,
                context={},
            )

            if ttft > 1.0:  # TTFT 1秒超は失敗扱い
                response.failure(f"TTFT too high: {ttft:.3f}s")

実行:

locust -f locustfile_llm.py --users 32 --spawn-rate 2 --run-time 10m

なぜストリーミングレスポンスを解析するか: LLMの負荷テストでは、通常のHTTPレスポンスタイムだけではTTFTとTPOTを正しく計測できません。Server-Sent Events(SSE)のストリームを逐次パースし、最初のトークン到達時刻とトークン生成速度を個別に計測する必要があります。

テストデータの設計

パフォーマンステストの信頼性は、テストデータの品質に大きく依存します。Gartnerは2026年にAIプロジェクトの約75%が合成データを使用すると予測しています(Synthetic Data Explosion - Cogent)。

テストデータ設計の3原則:

  1. 本番分布の再現: 入力データの分布(文字列長、数値範囲、NULL率)を本番と一致させる
  2. エッジケースの網羅: 最大入力長、空文字、特殊文字、同時アクセスによるデータ競合
  3. プライバシーの確保: 個人情報を含む本番データは使わない。合成データまたはマスキング済みデータを使用
# LLM向けテストプロンプト生成例: 本番の入力長分布を再現
import random
import numpy as np

def generate_test_prompts(n: int, seed: int = 42) -> list[dict]:
    """本番の入力トークン長分布に従うテストプロンプトを生成"""
    rng = np.random.default_rng(seed)
    # 本番ログから算出した入力トークン長の分布(対数正規分布で近似)
    token_lengths = rng.lognormal(mean=5.0, sigma=1.0, size=n).astype(int)
    token_lengths = np.clip(token_lengths, 10, 4096)

    base_prompts = [
        "以下の技術について詳しく説明してください: ",
        "次のコードをレビューしてください: ",
        "以下の問題の解決策を提案してください: ",
    ]

    prompts = []
    for length in token_lengths:
        base = random.choice(base_prompts)
        # 簡易的にトークン長を文字列長で近似(日本語: 1トークン≈1-2文字)
        padding = "あ" * max(0, int(length * 1.5) - len(base))
        prompts.append({
            "text": base + padding,
            "expected_tokens": int(length),
            "max_tokens": min(int(length * 2), 4096),
        })
    return prompts

テスト結果を分析してアクションに繋げる

テストを実行して数値を集めるだけでは不十分です。結果を構造的に分析し、具体的な改善アクションに落とし込むプロセスが必要です。

パフォーマンスバジェットを定義する

パフォーマンスバジェットとは、各指標の「許容上限値」を事前に定義したものです。これにより、テスト結果の合否判定が主観ではなく定量的になります(Performance Budgets - DebugBear)。

# performance_budget.yaml: パフォーマンスバジェット定義例
api_endpoints:
  product_list:
    p95_latency_ms: 300
    p99_latency_ms: 500
    error_rate_percent: 0.5
    min_throughput_rps: 100

  checkout:
    p95_latency_ms: 1000
    p99_latency_ms: 2000
    error_rate_percent: 0.1
    min_throughput_rps: 10

ml_inference:
  llm_completion:
    ttft_p95_ms: 500
    tpot_p95_ms: 50
    throughput_tokens_per_sec: 5000
    gpu_utilization_max_percent: 90

infrastructure:
  cpu_utilization_max_percent: 80
  memory_utilization_max_percent: 85
  disk_io_utilization_max_percent: 70

k6 thresholdsでパフォーマンスバジェットを自動検証する

k6のthresholds機能を使うと、パフォーマンスバジェットをコードとして管理し、CI/CDパイプラインで自動検証できます。閾値を超えるとk6が非ゼロの終了コードを返し、パイプラインを自動停止します。

// k6 thresholdsによるパフォーマンスバジェット自動検証
export const options = {
  thresholds: {
    // APIエンドポイント別のバジェット
    'http_req_duration{endpoint:product_list}': ['p(95)<300', 'p(99)<500'],
    'http_req_duration{endpoint:checkout}': ['p(95)<1000', 'p(99)<2000'],

    // 全体のエラー率
    'http_req_failed': ['rate<0.005'],

    // カスタムメトリクス(ML推論用)
    'ttft': ['p(95)<500'],
    'tpot': ['p(95)<50'],
  },
};

分析レポートのテンプレート

テスト結果を次のアクションに繋げるために、以下のテンプレートで分析レポートを作成することを推奨します。

## パフォーマンステスト分析レポート

### テスト概要
- 実行日時: 2026-04-17
- テスト種別: Average-Load
- VU数: 200 / 持続時間: 10分
- 対象環境: staging (k8s 3ノード, 4vCPU/16GB各)

### パフォーマンスバジェット判定

| エンドポイント | 指標 | バジェット | 実測値 | 判定 |
|--------------|------|----------|--------|------|
| /api/products | p95 | <300ms | 245ms | PASS |
| /api/checkout | p95 | <1000ms | 1,230ms | FAIL |

### ボトルネック分析

**REDメトリクス:**
- /api/checkout の Duration が 1,230ms(バジェット超過)
- Error Rate は 0.3%(バジェット内)

**USEメトリクス:**
- CPU Utilization: 45%(余裕あり)
- メモリ Utilization: 62%(余裕あり)
- DB接続プール Saturation: 95%(飽和!)

### 根本原因
DB接続プールが飽和。checkoutフローのトランザクションが
長時間接続を占有し、他のリクエストが待ち行列に入っている。

### 改善アクション
1. DB接続プールサイズを20→50に拡大(短期対策)
2. checkoutトランザクションの分割(長期対策)
3. 改善後に再テスト実施

よくある問題と解決方法

パフォーマンステストの実施でよく遭遇する問題と対処法を整理します。

問題 原因 解決方法
テスト結果が本番と乖離する シンクタイム未設定、テストデータが非現実的 本番ログからシンクタイム・データ分布を算出
負荷生成マシンがボトルネックに 1台で大量VUを生成しリソース枯渇 k6の場合--execution-segmentで分散、Locustの場合--master/--worker構成
毎回結果がばらつく 共有テスト環境で他のワークロードが干渉 専用テスト環境を確保、または深夜帯に実行
GPU推論のスループットが低い バッチサイズが小さく、GPU使用率が低い vLLMの--max-num-seqsを調整してバッチサイズを最適化
CI/CDでの実行時間が長い 全テスト種別を毎回実行 Smokeテストのみ毎回、Average-Loadはリリース前のみ

まとめと次のステップ

まとめ:

  • ワークロードモデリングはLittle's Lawを中核に、本番アクセスログからPHF・TPS・VU数を数理的に算出する
  • ボトルネック分析はUSE(インフラ層)とRED(サービス層)を組み合わせ、問題の所在を切り分ける
  • ML推論サービスではTTFT/TPOT/GPU Utilizationが主要メトリクス。ストリーミングレスポンスの逐次解析が必須
  • パフォーマンスバジェットを事前定義し、k6 thresholds等でCI/CDパイプラインに組み込むことで、主観を排した定量判定が可能

次にやるべきこと:

  • 自社の本番アクセスログからPHFを算出し、Little's LawでVU数を導出してみる
  • k6またはLocustで最もクリティカルなAPIの負荷テストスクリプトを1つ書いてみる
  • GrafanaダッシュボードにUSE/REDメトリクスパネルを追加し、負荷テスト実行時にリアルタイム観測できる環境を整える

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?