1. Home
  2. LoadTesting
  3. k6
  4. k6 を用いた API 負荷テストの導入とシナリオ実行の基礎

k6 を用いた API 負荷テストの導入とシナリオ実行の基礎

  • 公開日
  • カテゴリ:k6
  • タグ:k6
k6 を用いた API 負荷テストの導入とシナリオ実行の基礎

Webサービスを提供する上で、一定の負荷やスパイク的なアクセスがあっても安定して応答できるかどうかは非常に重要です。その確認に欠かせないのが「負荷テスト」です。

今回は、開発者フレンドリーで扱いやすいパフォーマンステストツール k6 を使って、API に対する負荷テストを実施する方法をご紹介します。

k6 とは

k6 は、JavaScript ベースで負荷テストが書ける OSS のパフォーマンステストツールです。

主な特徴は以下のとおりです。

  • JavaScript でテストスクリプトを記述できる
  • CLI で簡単に実行可能
  • スケーラブルで本番に近いシナリオ設計ができる
  • 実行結果がコンソールにリアルタイムで表示され、後から分析しやすい
  • Grafana Cloud や Datadog などと連携して可視化も可能

負荷テストを日常的な開発フローに取り入れやすくする設計になっており、CI/CD やパフォーマンスモニタリングとの統合もスムーズに行えます。

本記事では、ローカル環境で完結する形で基本機能を試していきます。

k6 インストール

k6 をインストールします。Mac なら Homebrew でインストール可能です。

brew install k6

// インストール確認
k6 version

Install k6

負荷テストスクリプト作成

実際にエンドポイントへリクエストを送信するスクリプトを作成します。

以下のスクリプトでは、10 人の仮想ユーザーが 10 秒間 /users にリクエストを送り続けるテストを行います。

// loadtest.js
import http from 'k6/http'
import { check } from 'k6'

export const options = {
    vus: 10,          // 仮想ユーザー数
    duration: '10s',  // 実行時間
}

export default function () {
    // エンドポイントへリクエスト
    const res = http.get('http://localhost:8080/users')

    check(res, {
        // ステータスコードが 200(成功)であることを確認
        'status is 200': (r) => r.status === 200,
        // レスポンスヘッダーに JSON を示す Content-Type が含まれていることを確認
        'response is JSON': (r) => r.headers['Content-Type'].includes('application/json'),
    })
}

負荷テスト実行

では実際にスクリプトを実行して負荷テストを実施してみます。

k6 run loadtest.js

出力された結果は以下になりました。

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: loadtest.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 40s max duration (incl. graceful stop):
              * default: 10 looping VUs for 10s (gracefulStop: 30s)

  █ TOTAL RESULTS 

    checks_total.......................: 458     42.734385/s
    checks_succeeded...................: 100.00% 458 out of 458
    checks_failed......................: 0.00%   0 out of 458

    ✓ status is 200
    ✓ response is JSON

    HTTP
    http_req_duration.......................................................: avg=451.44ms min=100.84ms med=456.57ms max=791.35ms p(90)=723.24ms p(95)=756.58ms
      { expected_response:true }............................................: avg=451.44ms min=100.84ms med=456.57ms max=791.35ms p(90)=723.24ms p(95)=756.58ms
    http_req_failed.........................................................: 0.00% 0 out of 229
    http_reqs...............................................................: 229   21.367192/s

    EXECUTION
    iteration_duration......................................................: avg=451.81ms min=100.89ms med=456.81ms max=791.53ms p(90)=723.45ms p(95)=756.86ms
    iterations..............................................................: 229   21.367192/s
    vus.....................................................................: 10    min=10       max=10
    vus_max.................................................................: 10    min=10       max=10

    NETWORK
    data_received...........................................................: 61 kB 5.7 kB/s
    data_sent...............................................................: 20 kB 1.9 kB/s

running (10.7s), 00/10 VUs, 229 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  10s

これらの出力はそれぞれ以下の指標を表しています。シンプルなスクリプトを実行するだけで、ここまで多くの情報が得られます。

指標名説明実際の数値注目ポイント考察
checks_total実行されたチェック(アサーション)の総数458 回(42.73 回/秒)スクリプト内 check() 実行回数リクエストごとに正しくアサーションが実行されていることが確認できる。
checks_succeeded成功したチェックの割合と件数100.00%(458/458)応答の正確さを示すすべてのレスポンスが期待通りであることを意味し、API の安定性が高いと判断できる。
checks_failed失敗したチェックの割合と件数0.00%(0/458)アサーション失敗がないか確認エラーなし。テストがすべて成功したことを示す。
http_req_durationHTTP リクエストにかかった時間の統計avg=451ms / min=100ms / max=791ms / p(90)=723ms / p(95)=757ms応答時間のばらつきが分かる。特に p(90), p(95) に注目高速なレスポンスも多いが、一部のリクエストでは応答時間が長くなる傾向も見られる。
http_req_failedHTTP リクエストのうち失敗した割合0.00%(0/229)通信・サーバーエラーの有無すべてのリクエストが正常に処理され、エラーは発生していない。
http_reqs総 HTTP リクエスト数と秒間リクエスト数229 回(21.36 回/秒)全体でどれくらいのリクエストが処理されたか仮想ユーザーによって継続的にリクエストが発生していることがわかる。
iteration_duration仮想ユーザーが1ループを完了するまでの所要時間avg=451ms / med=457ms / p(90)=723ms / p(95)=757mshttp_req_duration とほぼ同じ傾向を見ることができる各ループがすべて HTTP リクエスト処理に費やされており、処理時間の傾向が明確に反映されている。
iterations実行されたループ回数229 回(21.36 回/秒)総処理回数 = リクエスト数仮想ユーザーごとにループ処理が繰り返されていることが確認できる。
vus同時実行中の仮想ユーザー数(現在)10(固定)固定ユーザー数かどうか確認負荷を一定に保ったテストが実施されている。
vus_maxテスト中に使用された最大仮想ユーザー数10テスト構成の上限確認仮想ユーザー数は常に一定で、急激な負荷変化がない構成である。
data_receivedサーバーから受信した総データ量61 kB(5.7 kB/s)応答のサイズ感レスポンスは比較的軽量で、API の設計として扱いやすいことがわかる。
data_sentクライアントから送信した総データ量20 kB(1.9 kB/s)リクエストサイズリクエストボディが小さく、負荷テスト時のネットワーク帯域に対する影響が少ない。

duration はパーセンタイル値を見るとユーザーの体感値を把握しやすい

結果出力の中の duration 項目に p(90)p(95) とあるのは、パーセンタイル値です。

  • p(90):90% のリクエストが、この時間以内に応答されたことを意味します。
  • p(95):95% のリクエストが、この時間以内に応答されたことを意味します。

つまり、たとえば p(90) = 723ms と出た場合、全体のうち上位 10% の遅いリクエストを除いた 90% が、723ms 以内に終わっているということを意味します。

  • 平均(avg) は1つの極端な遅いリクエストによって大きくズレてしまうことがあります。
  • 中央値(med) は全体のちょうど真ん中の値なので、ばらつきの「端の方」が見えません。

「ちょっともたつくこともある」を加味する場合、これらのパーセンタイル値も見ておくと「ほとんどのユーザーが体験する実際のレスポンスタイム」を把握できます。

「サイトの快適さ」は、一部の速いユーザー体験ではなく、大多数のユーザー体験で決まるため、これらの指標も把握するとテスト結果からの考察と実際のユーザー体感にズレがなく見られます。

シナリオテスト作成

テスト実行までの基本的な点に触れたところで、シナリオテストを作成してみます。

シナリオテストとは、異なる条件や実行パターンごとに複数のテストを同時に、あるいは段階的に実行できる機能です。

たとえば「短時間に一定のユーザーがアクセスし続けるテスト」と「ユーザー数を徐々に増やすテスト」を並行して実行する、といったことが可能になります。

これにより、単純な負荷だけでなく、スパイク(瞬間的な高負荷)やスケーリング(段階的な増加)に対する挙動も一括して検証できるようになります。

今回は、3 種類の代表的なシナリオを組み合わせたシナリオテストを作成してみます。

以下 k6 スクリプトでは、option にシナリオを渡して、それぞれ異なる負荷のかけ方をするテストを定義しています。

// scenarios-test.js
import http from 'k6/http'
import { check } from 'k6'

export const options = {
    scenarios: {
        fast_users: { // 短時間に一定の同時ユーザー数でリクエストを連続送信するベーシックな負荷テスト
            executor: 'constant-vus', // 仮想ユーザー数(VUs)を常に一定に保ち、指定時間ループを実行し続けるモード
            exec: 'fastUser',         // 実行する関数
            vus: 5,                   // 常時アクティブな仮想ユーザー数(5人)
            duration: '1m',           // このシナリオを実行する期間(1分間)
        },
        ramp_users: { // ユーザー数を段階的に増減させながら負荷を変化させるテスト(スケーラビリティ確認)
            executor: 'ramping-vus',             // 仮想ユーザー数(VUs)を段階的に変化させるモード
            exec: 'rampUser',                    // 実行する関数
            startVUs: 0,                         // テスト開始時の VU 数(最初は 0 人からスタート)
            stages: [
                { duration: '30s', target: 10 }, // 最初の30秒間で、0人 → 10人まで増やす
                { duration: '30s', target: 20 }, // 次の30秒間で、10人 → 20人までさらに増やす
                { duration: '30s', target: 0 },  // 最後の30秒間で、20人 → 0人に減らして終了
            ],
            startTime: '10s',                    // テスト全体の開始から10秒後に開始
        },
        spike_test: { // 一瞬だけ大量のアクセスを発生させ、システムのスパイク耐性を確認するテスト
            executor: 'per-vu-iterations', // 各仮想ユーザー(VU)が指定された回数だけ処理して終了する。負荷が瞬間的に集中するスパイクテストに向いている。
            exec: 'spikeUser',             // 実行する関数
            vus: 50,                       // 同時に起動する仮想ユーザー数。50人が一斉に開始される。
            iterations: 1,                 // 各ユーザーが1回だけリクエストを送って終了する。
            startTime: '40s',              // テスト全体の開始から40秒後にこのシナリオを実行開始する。
        },
    },
}

export function fastUser() {
    const res = http.get('http://localhost:8080/users')
    check(res, { 'status is 200': (r) => r.status === 200 })
}

export function rampUser() {
    const res = http.get('http://localhost:8080/users')
    check(res, { 'status is 200': (r) => r.status === 200 })
}

export function spikeUser() {
    const res = http.get('http://localhost:8080/users')
    check(res, { 'status is 200': (r) => r.status === 200 })
}

このスクリプトでは、それぞれのシナリオが異なる exec 関数(テスト内容)を持っており、タイミングや実行方法を個別に制御できます。

  • fast_users: 5 人の仮想ユーザーが 60 秒間リクエストを送り続ける基本的なテスト
  • ramp_users: ユーザー数を段階的に増減させ、スケーラビリティを確認
  • spike_test: 50 人が一斉に 1 回だけアクセスするスパイク的な負荷

それでは負荷テストを実行します。

k6 run scenarios-test.js

結果は以下になりました。

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: scenarios-test.js
        output: -

     scenarios: (100.00%) 3 scenarios, 75 max VUs, 11m10s max duration (incl. graceful stop):
              * fast_users: 5 looping VUs for 1m0s (exec: fastUser, gracefulStop: 30s)
              * ramp_users: Up to 20 looping VUs for 1m30s over 3 stages (gracefulRampDown: 30s, exec: rampUser, startTime: 10s, gracefulStop: 30s)
              * spike_test: 1 iterations for each of 50 VUs (maxDuration: 10m0s, exec: spikeUser, startTime: 40s, gracefulStop: 30s)

  █ TOTAL RESULTS 

    checks_total.......................: 2661    26.419984/s
    checks_succeeded...................: 100.00% 2661 out of 2661
    checks_failed......................: 0.00%   0 out of 2661

    ✓ status is 200

    HTTP
    http_req_duration.......................................................: avg=455.34ms min=100.75ms med=457.37ms max=800.93ms p(90)=737.28ms p(95)=768.39ms
      { expected_response:true }............................................: avg=455.34ms min=100.75ms med=457.37ms max=800.93ms p(90)=737.28ms p(95)=768.39ms
    http_req_failed.........................................................: 0.00%  0 out of 2661
    http_reqs...............................................................: 2661   26.419984/s

    EXECUTION
    iteration_duration......................................................: avg=455.61ms min=101.03ms med=457.52ms max=801.26ms p(90)=737.35ms p(95)=768.61ms
    iterations..............................................................: 2661   26.419984/s
    vus.....................................................................: 1      min=1         max=21
    vus_max.................................................................: 75     min=75        max=75

    NETWORK
    data_received...........................................................: 711 kB 7.1 kB/s
    data_sent...............................................................: 234 kB 2.3 kB/s

running (01m40.7s), 00/75 VUs, 2661 complete and 0 interrupted iterations
fast_users ✓ [======================================] 5 VUs      1m0s           
ramp_users ✓ [======================================] 00/20 VUs  1m30s          
spike_test ✓ [======================================] 50 VUs     00m00.8s/10m0s  50/50 iters, 1 per VU

このように、複数のシナリオを同時に動かすことで、短期的な通常アクセス・段階的なスケーリング・突発的なスパイクといった複数のケースを 1 つのテストで網羅的に検証できます。

また、各シナリオの実行時間や仮想ユーザー数、実行タイミングを個別に制御できるため、本番に近いリアルな負荷モデルを柔軟に設計できるのが k6 の強みです。

今回実装した負荷テストでは、それぞれが以下のタイミングで実行されていきます。

時間(sec)   0          10         20         30         40         50         60
           |----------|----------|----------|----------|----------|----------|

fast_users ├────────────────────────────────────────────────────────────────▶(1分間継続)

ramp_users           ├───────────────┬───────────────┬───────────────▶(10秒後に開始、3ステージ)
                     ↑               ↑               ↑
                   VU=10          VU=20            VU=0(終了)

spike_test                                ├▶(40秒後に開始、全50VUが一斉に1回ずつ実行)
                                          ↑
                                   一瞬のスパイク負荷

まとめ

今回は、k6 を使った基本的な負荷テストの流れから、複数の実行条件を組み合わせたシナリオテストまでをご紹介しました。

  • vusduration の指定だけで、シンプルに並列リクエストの負荷を確認できる
  • 出力結果に含まれる p(90)p(95) などのパーセンタイル指標を活用することで、ユーザー体感に近いパフォーマンスを把握できる
  • scenarios を使えば、一定負荷・段階的増加・スパイク的なアクセスを一括でテストできる

パフォーマンスのボトルネックやスパイク時の挙動を事前に把握しておくことは、サービスの安定運用に直結します。

私が携わるサービスでも、本番リリース前の品質チェックの一環として、k6 を用いた負荷試験(TypeScriptで実装)を導入しています。

そこでは、可用性やスケーラビリティの確認に加え、将来的な拡張を見据えた「現状の性能の棚卸し」にも役立っています。

実際のパフォーマンスを把握しておくことで、「現時点では要件を満たしているが、今後アクセスが増えた場合、どこを改善すべきか?」といった改善余地を先回りで考察できるのも、負荷テストの大きな意義です。

ぜひ一度、k6 を活用して、パフォーマンスの健全性を可視化してみてください。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級