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

Webサービスを提供する上で、一定の負荷やスパイク的なアクセスがあっても安定して応答できるかどうかは非常に重要です。その確認に欠かせないのが「負荷テスト」です。
今回は、開発者フレンドリーで扱いやすいパフォーマンステストツール k6 を使って、API に対する負荷テストを実施する方法をご紹介します。
k6 とは
k6 は、JavaScript ベースで負荷テストが書ける OSS のパフォーマンステストツールです。
主な特徴は以下のとおりです。
- JavaScript でテストスクリプトを記述できる
- CLI で簡単に実行可能
- スケーラブルで本番に近いシナリオ設計ができる
- 実行結果がコンソールにリアルタイムで表示され、後から分析しやすい
- Grafana Cloud や Datadog などと連携して可視化も可能
負荷テストを日常的な開発フローに取り入れやすくする設計になっており、CI/CD やパフォーマンスモニタリングとの統合もスムーズに行えます。
本記事では、ローカル環境で完結する形で基本機能を試していきます。
k6 インストール
k6 をインストールします。Mac なら Homebrew でインストール可能です。
brew install k6
// インストール確認
k6 version
負荷テストスクリプト作成
実際にエンドポイントへリクエストを送信するスクリプトを作成します。
以下のスクリプトでは、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_duration | HTTP リクエストにかかった時間の統計 | avg=451ms / min=100ms / max=791ms / p(90)=723ms / p(95)=757ms | 応答時間のばらつきが分かる。特に p(90), p(95) に注目 | 高速なレスポンスも多いが、一部のリクエストでは応答時間が長くなる傾向も見られる。 |
http_req_failed | HTTP リクエストのうち失敗した割合 | 0.00%(0/229) | 通信・サーバーエラーの有無 | すべてのリクエストが正常に処理され、エラーは発生していない。 |
http_reqs | 総 HTTP リクエスト数と秒間リクエスト数 | 229 回(21.36 回/秒) | 全体でどれくらいのリクエストが処理されたか | 仮想ユーザーによって継続的にリクエストが発生していることがわかる。 |
iteration_duration | 仮想ユーザーが1ループを完了するまでの所要時間 | avg=451ms / med=457ms / p(90)=723ms / p(95)=757ms | http_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 を使った基本的な負荷テストの流れから、複数の実行条件を組み合わせたシナリオテストまでをご紹介しました。
vus
とduration
の指定だけで、シンプルに並列リクエストの負荷を確認できる- 出力結果に含まれる
p(90)
やp(95)
などのパーセンタイル指標を活用することで、ユーザー体感に近いパフォーマンスを把握できる scenarios
を使えば、一定負荷・段階的増加・スパイク的なアクセスを一括でテストできる
パフォーマンスのボトルネックやスパイク時の挙動を事前に把握しておくことは、サービスの安定運用に直結します。
私が携わるサービスでも、本番リリース前の品質チェックの一環として、k6 を用いた負荷試験(TypeScriptで実装)を導入しています。
そこでは、可用性やスケーラビリティの確認に加え、将来的な拡張を見据えた「現状の性能の棚卸し」にも役立っています。
実際のパフォーマンスを把握しておくことで、「現時点では要件を満たしているが、今後アクセスが増えた場合、どこを改善すべきか?」といった改善余地を先回りで考察できるのも、負荷テストの大きな意義です。
ぜひ一度、k6 を活用して、パフォーマンスの健全性を可視化してみてください。