【ハンズオン】k6で始める負荷テスト(第2回/全5回)
梅木 和弥
はじめに
こんにちは、アプリケーションエンジニアの梅木です。
前回は、「10分で動かせるAPIテスト」として、k6の導入と最小限のチュートリアルを行いました。
手元で k6 run が動くと、「とりあえず VU(ユーザー数)を1000にして回してみよう!」とやりたくなりませんか?
ただ、闇雲に負荷をかけても得られる知見はそう多くありません。
※ VU(ユーザー数)について
「リリース後の安心」を得るためには、「何を知りたくて負荷をかけるのか」 を明確にし、それに適した「負荷の波形(パターン)」 を選ぶ必要があります。
今回は、提供しているリポジトリの scenarios/02-load-patterns に含まれるシナリオを使って、実務で使い分けるべき 「負荷テストの型」 を解説します。
負荷テストの「型」
一口に負荷テストと言っても、その目的は様々です。
k6ではパラメータを設定することで、あらゆる負荷パターンをコードで表現できます。
代表的なパターンは以下の6つです。
| テスト種類 | 目的 |
|---|---|
| Smoke Test | 基本動作確認 スクリプトが合ってるか、サーバーが死んでないかの確認(前回やったのがこれ) |
| Load Test | 通常運転確認 平常時のアクセス数で、パフォーマンス目標を達成できるか |
| Stress Test | 限界値の把握 負荷を上げ続けて、システムのボトルネックを探る |
| Spike Test | 急激な負荷変動の挙動確認 |
| Soak Test | 長時間稼働の安定性確認 メモリリークなどがないか確認する |
| Breakpoint Test | 破綻点の特定 |
今回は、開発サイクルの中で特によく使う Load Test と Stress Test、そして特徴的な Spike Test を実際に動かして見ようと思います。
1. Load Test: 通常運転確認
まずは基本となるロードテストです。
実運用で想定される「ピーク時のトラフィック」をシミュレートするものです。
ご紹介するものはあくまで例ですが、
今回は「段階的に負荷を上げ、一定時間維持し、下げる」 という台形のようなステージ構成です。
設定
- VU: 10 → 20(段階的)
- 実行時間: 16分
閾値
- 失敗率: 5%未満
- レスポンスタイム(p95): 500ms未満
- レスポンスタイム(p99): 1000ms未満
- Check成功率: 95%以上
シナリオの書き方
// scenarios/02-load-patterns/02-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 10 }, // 2分かけて10 VUにランプアップ
{ duration: '5m', target: 10 }, // 5分間10 VUを維持(ピーク)
{ duration: '2m', target: 20 }, // 2分かけて20 VUに増加
{ duration: '5m', target: 20 }, // 5分間20 VUを維持
{ duration: '2m', target: 0 }, // 2分かけて0 VUにランプダウン
],
thresholds: {
http_req_failed: ['rate<0.05'], // 失敗率5%未満
http_req_duration: [
'p(95)<500', // 95%のリクエストが500ms未満
'p(99)<1000', // 99%のリクエストが1000ms未満
],
checks: ['rate>0.95'], // チェック成功率95%以上
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
// 典型的なユーザー行動をシミュレート
// 1. トップページ(ユーザー一覧)
let response = http.get(`${BASE_URL}/api/users`);
check(response, {
'ユーザー一覧取得成功': (r) => r.status === 200,
});
sleep(1);
// 2. ユーザー詳細を表示
const userId = Math.floor(Math.random() * 10) + 1;
response = http.get(`${BASE_URL}/api/users/${userId}`);
check(response, {
'ユーザー詳細取得成功': (r) => r.status === 200,
});
sleep(2);
// 3. 新しいユーザーを作成(10%の確率)
if (Math.random() < 0.1) {
const payload = JSON.stringify({
name: `テストユーザー${Date.now()}`,
email: `test${Date.now()}@example.com`,
});
response = http.post(`${BASE_URL}/api/users`, payload, {
headers: { 'Content-Type': 'application/json' },
});
check(response, {
'ユーザー作成成功': (r) => r.status === 201,
});
sleep(1);
}
// 4. ランダムな遅延エンドポイント(実際のAPIの遅延をシミュレート)
response = http.get(`${BASE_URL}/api/random-delay`);
check(response, {
'ランダム遅延エンドポイント成功': (r) => r.status === 200,
});
sleep(1);
}
実行
k6 run scenarios/02-load-patterns/02-load-test.js
# BASE_URL=http://{your-api}.com k6 run scenarios/02-load-patterns/02-load-test.js
分析ポイント
- http_req_duration: レスポンスタイムが許容範囲内か
- http_req_failed: エラー率が低いか
- iterations: 完了したイテレーション数
- vus: 仮想ユーザー数の推移
このテストが通れば(閾値をクリアすれば)、「今のシステム構成なら、予想されるアクセス数までは問題なくさばける」 という実績を得ることが出来ます。
2. Stress Test: 限界値の把握
ロードテストが通ったら、システムの限界点を見極めるテストを実施します。
「システムの耐久性の確認」「キャパシティプランニング」「ピーク時の挙動の予測をしたい」が実行する意図です。
段階的に負荷を高めていこうと思います。最終的には「5分間100VU」の負荷をかけようかと。
設定
- VU: 20 → 50 → 100(段階的増加)
- 実行時間: 26分
補足
"100VUを5分間"がしっくり来ない方へ
VUは「同時ユーザー」です。1秒あたりのリクエスト数と同義ではありません。
今回は、各ユーザーが「リクエスト→レスポンス→1秒休憩(sleep(1))」という動作をする前提です。
「この1サイクルを5分間行います。それを100ユーザーで行います。」が今回の構成です。
たとえば、
「リクエスト→レスポンス」が約100msで返ってくるシステムの場合ですと、
- 1VU(各VU)は 約1.1秒に1リクエスト
- つまり100VUなら 1秒間に約90〜100リクエスト程度
となり、かなりの高負荷を演出出来ます。
閾値
- 失敗率: 10%未満
- レスポンスタイム(p95): 2000ms未満
シナリオの書き方
// scenarios/02-load-patterns/03-stress-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 20 }, // 2分で20 VUへランプアップ(ウォームアップ)
{ duration: '5m', target: 20 }, // 5分間20 VUを維持
{ duration: '2m', target: 50 }, // 2分で50 VUへ増加
{ duration: '5m', target: 50 }, // 5分間50 VUを維持
{ duration: '2m', target: 100 }, // 2分で100 VUへ増加
{ duration: '5m', target: 100 }, // 5分間100 VUを維持(ストレス状態)
{ duration: '5m', target: 0 }, // 5分かけて0 VUへランプダウン(回復確認)
],
thresholds: {
// ストレステストでは閾値を緩めに設定
http_req_failed: ['rate<0.1'], // 失敗率10%未満
http_req_duration: ['p(95)<2000'], // 95%のリクエストが2秒未満
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
// シンプルなリクエストパターン
const response = http.get(`${BASE_URL}/api/users`);
check(response, {
'ステータスは200または503': (r) => r.status === 200 || r.status === 503,
'レスポンスタイムは5秒以内': (r) => r.timings.duration < 5000,
});
sleep(1);
}
実行
※ 本番環境では実行しないこと
k6 run scenarios/02-load-patterns/03-stress-test.js
# # BASE_URL=http://{your-api}.com k6 run scenarios/02-load-patterns/03-stress-test.js
分析ポイント
以下に注目するとよいレポートがかけるかと。
- どの時点でレスポンスタイムが劣化し始めるか
- どの時点でエラー率が上昇するか
- 負荷が下がった後、システムが正常に回復するか
閾値(Thresholds)の考え方
Load TestとStress Testでは「合格基準」が異なります。
Stress Testではシステムが高負荷状態になるため、多少のエラーや遅延は許容しつつ、「復旧できるか」「データ整合性が保たれるか」などを重視します。
thresholds: {
// ロードテストより緩めに設定(失敗率10%未満など)
http_req_failed: ['rate<0.1'],
http_req_duration: ['p(95)<2000'], // 2秒以内なら許容
},
このテストを行うことで、「アクセスが急増したとき、次にスケールアウトすべきなのはWebサーバーなのかDBなのか」 というボトルネックの特定に役立ちます。
障害発生時の原因切り分けの役に立つことが多く、ドキュメント化しておくことで、どのレイヤーを優先調査すべきかの目印になることも多々あります。
3. Spike Test: 突発的なアクセス増
最後に、急激な負荷変動に対するシステムの反応を確認することを目的としたスパイクテストを実施してみましょう。
イベント時の対策やSNSバズリといったなんらかの突発的なアクセス増をシミュレーションするものです。
また、オートスケーリングのテストにも使います。
構成は以下のようなイメージです。通常時の想定から一気に「10秒 で 200VU 状態」に持っていきます。
export const options = {
stages: [
{ duration: '10s', target: 10 }, // 通常状態
{ duration: '1m', target: 10 },
{ duration: '10s', target: 200 }, // 10秒で一気に200 VUへ!(スパイク)
{ duration: '3m', target: 200 }, // 耐える
{ duration: '10s', target: 10 }, // 急減
// ...
],
};
設定
- VU: 10 → 200(急増)→ 10
- スパイク時間: 3分
- 実行時間: 8分
閾値
- 失敗率: 15%未満
- レスポンスタイム(p99): 5000ms未満
シナリオの書き方
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '10s', target: 10 }, // 10秒で10 VUへランプアップ(通常状態)
{ duration: '1m', target: 10 }, // 1分間10 VUを維持
{ duration: '10s', target: 200 }, // 🚀 10秒で200 VUへ急増(スパイク)
{ duration: '3m', target: 200 }, // 3分間200 VUを維持(スパイク継続)
{ duration: '10s', target: 10 }, // 10秒で10 VUへ急減
{ duration: '3m', target: 10 }, // 3分間10 VUを維持(回復確認)
{ duration: '10s', target: 0 }, // 10秒で0 VUへ
],
thresholds: {
http_req_failed: ['rate<0.15'], // 失敗率15%未満(スパイク時は許容)
http_req_duration: ['p(99)<5000'], // 99%のリクエストが5秒未満
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const response = http.get(`${BASE_URL}/api/users`);
check(response, {
'リクエスト成功': (r) => r.status === 200 || r.status === 429 || r.status === 503,
});
// スパイク時は待機時間を短くして、より多くのリクエストを送信
sleep(0.5);
}
実行
※ 本番環境では実行しないこと
k6 run scenarios/02-load-patterns/04-spike-test.js
# BASE_URL=http://{your-api}.com k6 run scenarios/02-load-patterns/04-spike-test.js
分析ポイント
- スパイク発生時のレスポンスタイム
- エラー率の変化
- オートスケーリングが発動するか
- レートリミットやキューイングの動作
- スパイク後の回復時間
先程も記載しましたが、オートスケーリングの確認などに最適です。
「負荷が増えてからコンテナが増えるまでの間に、どれくらいエラーが出るか」を測定できます。
まとめ
今回は6つの負荷パターンを紹介し、実際に3つの負荷テストのハンズオンを行いました。
アプリエンジニアとして開発サイクルに組み込むなら、まずは以下のフローを意識すると良いかなと。
- Smoke Test
CI/CDで毎回実行。デプロイごとの健全性確認。 - Load Test
スプリントごと、あるいは大きな機能追加時。パフォーマンス要件を守れているか確認。 - Stress Test
リリース前やインフラ構成変更時。システムの限界容量を把握する。
リポジトリの scenarios/02-load-patterns/ には、これらすべてのテンプレートが入っています。
まずは 02-load-test.js の target を自社の規模に合わせて調整し、実行してみてもよいかと。
※ 実施する環境は充分確認を行ったうえでご実施ください。
次回はより実践的な「認証が必要なAPI」や「ファイルアップロード」など、複雑なシナリオの書き方に踏み込んでいきます!
お楽しみに!
梅木 和弥/ アプリケーションエンジニア
Webのシステム開発における、設計・実装に携わっています。
業務ドメインを技術に翻訳する工程に注力しております。SOLID原則が僕の物差しです。

