【ハンズオン】k6で始める負荷テスト(第1回/全5回)

梅木 和弥
【ハンズオン】k6で始める負荷テスト(第1回/全5回)

はじめに

こんにちは。アプリケーションエンジニアの梅木です。

みなさんは、負荷テストに対してどのような印象を持っていますか?

  • APIの仕様を隅々まで整理しなきゃいけない
  • 非機能要件の定義が大変そう
  • そもそも検証用の実行環境を準備するのが億劫...

私自身、これまで負荷テストの経験が少なかったこともあり、「意外と考えることが多くて、腰が重くなってしまうリリース前の一大イベント」というイメージを抱いていました。
しかし、先日担当したプロダクトのリリースを控えたタイミングで、k6というツールを使って負荷テストに挑戦してみたところ、その印象はガラリと変わりました。

本記事では、「面倒じゃない、怖くないk6負荷テスト」をテーマに、全5回にわたるハンズオン形式の連載をお届けします。

初回の今回は、「10分で動かせるAPIテスト」として、k6を使った負荷試験の第一歩を踏み出してみましょう。

1. 負荷試験の立ち位置について

従来であれば

負荷試験を専任チームや外部ベンダーに依頼する従来の形式だと、アプリエンジニアは「検証に携わらなくて済む」という利点はありました。
しかし、その分以下のような欠点も目立ちがちです。

例えば。

1. 仕様とかけ離れたシナリオ設計

シナリオ設計者が、仕様を一番理解している実装者(=アプリエンジニア)ではありません。

そのため、ユーザーの実際の挙動に即さない検証となるリスクがあります。

2. フィードバックループの遅延

「実装 → 外部での負荷測定 → ボトルネック発見 → 修正」というサイクルに時間がかかり、リリース直前に致命的な問題が発覚して慌てることになりかねません。

3. 「ブラックボックス」化

みなさんは、自分が書いたコードが実際のトラフィックでどう動くのか、限界点がどこなのかを説明出来ますか?
「インフラがよしなにしてくれるだろう」という前提でデプロイする不安が残ります。

アプリエンジニアが負荷試験に携わるべき理由

便利な検証ツールの台頭により、アプリエンジニアが積極的に負荷試験を行う理由は非常にポジティブなものになっています。

1. フィードバックの速度向上

開発終盤にまとめてやるのではなく、実装のついでに「ちょっと負荷をかける」ことが容易です。
負債が蓄積していない早い段階でボトルネックを見つけることで、修正コストを最小限に抑えられます。

2. パフォーマンスを「仕様」として定義できる

「レスポンスは速いほうがいい」という抽象的な目標を、「95%のリクエストが200ms以内に返ること」のようにコードで定義できます。
期待するレスポンスタイムを閾値(Thresholds)としてコードに記述することで、機能要件と同じように「テスト可能な仕様」として扱えるようになります。

3. 「依頼して待つ」がなくなる

他チームへの依頼や調整コストをかけず、自分のローカル環境や開発フローの中で「思い立ったときに」測定を始められます。

2. k6 とは

今回の連載は k6 にフォーカスを当てました。
k6は、Grafana社が開発している負荷テストツールです。主な特徴を挙げます。

  • JavaScript/TypeScriptで書ける:
    ユニットテストと同じ感覚でシナリオを構築でき、モジュール化やバージョン管理もそのまま使えます。
  • 多用なプロトコルに対応:
    ベーシックなHTTP通信だけでなく、WebSockets、gRPC、ブラウザ通信の負荷テストが実行できます。
  • コミュニティのエコシステムが大規模:
    k6を拡張して様々なニーズに対応が可能です。既に多くのユーザーが拡張機能をコミュニティで共有しているため、探索も可能です。
  • Go製で軽量:
    実行バイナリ1つで動作し、低スペックマシンでも大量のトラフィックをシミュレーション可能です。
  • モダンなエコシステムとの親和性: GitHub Actionsでの自動化 や、Grafanaでの可視化 が標準でサポートされている。

3. ハンズオン: 手元の環境で動かしてみる

今回、教材リポジトリを用意しました。クローン後すぐに検証できます。

教材: github.com/umekikazuya/k6-sandbox

Step 1: 開発環境のセットアップ

Docker Composeで検証用のモックサーバーを立ち上げます。

docker compose up -d mock-server

# ヘルスチェック確認
curl http://localhost:3000/health

Step 2: テストを実行する

リポジトリの scenarios/01-basics/01-simple-http.js を確認下さい。

k6の基本構造は非常にシンプルです。
「何人のユーザー(VUs)が」「何回(Iterations)実行するか」を options で定義し、default 関数にテスト内容を書きます。

以下のシナリオは、1ユーザーが1秒待機しながら10回リクエストを繰り返すといったシナリオ構成です。

export const options = {
  vus: 1,         // 仮想ユーザー数
  iterations: 10, // 実行回数
};

export default function () {
  http.get('http://localhost:3000/health');
  sleep(1);
}

では、上記のシナリオで負荷テストの実行をします。

docker run --rm -i --network=host grafana/k6 run - < scenarios/01-basics/01-simple-http.js

Step 3: 結果を読む

実行が完了すると、コンソールに統計レポートが表示されます。

メトリクス

まずは以下の3つの指標に注目しましょう。

  1. http_req_duration:
    リクエストの開始からレスポンス受信完了までの時間です。
    p(95)(95%のリクエストがこの時間内に完了した)の値を確認する習慣をつけましょう。
  2. http_req_failed:
    リクエストの失敗率です。
    最初のステップではここが 0.00% であることがゴールです。
  3. vus / iterations:
    意図した通りのユーザー数と回数で実行されたかを確認します。

4. レスポンスを検証する (check)

  リクエストが送れるようになったら、次は「正しく動いているか」を検証しましょう。
k6の check() 関数を使うと、ステータスコードやレスポンスボディの内容を検証できます。負荷試験中にサーバーがエラーを返していないかを確認するために必須の機能です。

check(response, {
  'ステータスは200': (r) => r.status === 200,
  'レスポンスタイムは200ms以下': (r) => r.timings.duration < 200,
  'statusフィールドは"ok"': (r) => {
    const body = JSON.parse(r.body);
    return body.status === 'ok';
  },
});

ステータスコードだけでなく、レスポンスボディのJSONフィールドまで検証できる点がポイントです。

5. ユーザーの「思考時間」を再現する (sleep)

実際のユーザーは、1秒間に何度もページをリロードしませんよね。

sleep() を入れないテストは、サーバーへの過度な集中を招き、非現実的な負荷になってしまいます。
「1秒待機」などの適切なインターバルを設けることで、リアリティのある負荷を再現できます。

export default function () {
  http.get('http://localhost:3000/health');
  sleep(1); // ユーザーの「思考時間」を再現
}

sleep() の値はシナリオによって変わります。
ページ閲覧なら1〜3秒、フォーム入力なら5〜10秒など、実際のユーザー行動に合わせて調整しましょう。

6. CRUD操作の網羅

GETだけでなく、POST・PUT・DELETEといったRESTful APIの一連の操作もテストできます。
シナリオの全体については02-http-methods.js を参照下さい。

シナリオの書き方

// POST - 新しいユーザーを作成
const payload = JSON.stringify({ name: '山田太郎', email: 'yamada@example.com' });
http.post(`${baseUrl}/users`, payload, {
  headers: { 'Content-Type': 'application/json' },
});

// PUT - ユーザー情報を更新
http.put(`${baseUrl}/users/1`, updatePayload, params);

// DELETE - ユーザーを削除
http.del(`${baseUrl}/users/1`);

7. 環境変数の外部化

ここまでのコードでは http://localhost:3000 をハードコーディングしていました。
実務では開発環境・ステージング環境・本番環境でURLが異なるため、環境変数で切り替えられるようにしておくと便利です。

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

export default function () {
  http.get(`${BASE_URL}/health`);
  sleep(1);
}

実行

# ローカル環境(デフォルト)
k6 run scenarios/01-basics/05-environment.js

# ステージング環境
BASE_URL=https://staging.example.com k6 run scenarios/01-basics/05-environment.js

# Docker環境
docker run --rm -i --network=host \
  -e BASE_URL=http://localhost:3000 \
  grafana/k6 run - < scenarios/01-basics/05-environment.js

|| 'http://localhost:3000' でデフォルト値を設定しているので、環境変数を渡さなくてもローカルで動作します。

まとめ

負荷試験は「リリースの直前に一度だけ行う特別な行事」ではなく、コードとして管理し、機能開発のサイクルの中で日常的に実行できる「継続的な検証」です。

k6を使えば、アプリエンジニアが慣れ親しんだJavaScriptで、すぐに始められます。

今回の内容を振り返ります。

  1. k6 run でテストを実行し、結果レポートを読む
  2. check() でレスポンスを検証する
  3. sleep() で現実的な負荷パターンを再現する
  4. CRUD操作でREST APIを網羅的にテストする
  5. 環境変数でURLを外部化する

教材リポジトリには、認証が必要なテストなど他にも多くのテンプレートを用意しています。
ぜひご活用ください。

まずは以下の2つに挑戦すると体系的に学べるかと思います。

  • リポジトリをクローンしてモックサーバーを立ててみる。
  • 自社のAPI(まずは1つだけ)に対して1VUでリクエストを投げてみる。

次回は、「通常負荷」から「システムの限界」までを把握する、負荷テストの型ついて解説します。

お楽しみに!

梅木 和弥/ アプリケーションエンジニア

Webのシステム開発における、設計・実装に携わっています。
業務ドメインを技術に翻訳する工程に注力しております。

最近はトムとジェリーにハマってます。

梅木 和弥 の書いた記事一覧

最新の関連記事

Download 資料ダウンロード

Drupalでの開発・運用、サーバー構築、Webサイト構築全般、制作費用などに関してお気軽にご相談ください。


Contact お問い合わせ

Drupalでの開発・運用、サーバー構築、Webサイト構築全般についてお気軽にご相談ください。専門スタッフによるDrupal無料相談も行なっております。