「k6 は JavaScript ベースで書けるから、フロントエンドエンジニアにも馴染みやすい」と佐藤CTOは言った。「Gatling は Scala ベースだが DSL が洗練されていて、複雑なシナリオが書きやすい。どちらを選ぶかより、正しいシナリオを書けるかが重要だ。」
1. k6 の基本
シンプルなテストスクリプト
// k6/basic-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// カスタムメトリクス
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
// テスト設定
export const options = {
// Load Test: 段階的に負荷を上げる
stages: [
{ duration: '2m', target: 50 }, // ウォームアップ: 2分で50VUまで
{ duration: '5m', target: 200 }, // ランプアップ: 5分で200VUまで
{ duration: '10m', target: 200 }, // 定常: 200VUを10分維持
{ duration: '3m', target: 0 }, // クールダウン: 3分で0VUまで
],
// 成功基準(Thresholds)
thresholds: {
http_req_duration: [
'p(95)<500', // p95 が 500ms 以下
'p(99)<1000', // p99 が 1000ms 以下
],
errors: ['rate<0.01'], // エラー率 1% 以下
http_req_failed: ['rate<0.01'], // HTTP エラー率 1% 以下
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
// シナリオ: 商品一覧 → 商品詳細 → カートに追加
const listRes = http.get(`${BASE_URL}/api/products?page=1&limit=20`);
check(listRes, {
'product list: status 200': (r) => r.status === 200,
'product list: has products': (r) => JSON.parse(r.body).length > 0,
});
responseTime.add(listRes.timings.duration);
errorRate.add(listRes.status !== 200);
sleep(Math.random() * 3 + 2); // 思考時間: 2-5秒
// 商品詳細
const productId = Math.floor(Math.random() * 10000) + 1;
const detailRes = http.get(`${BASE_URL}/api/products/${productId}`);
check(detailRes, {
'product detail: status 200': (r) => r.status === 200,
});
responseTime.add(detailRes.timings.duration);
errorRate.add(detailRes.status !== 200);
sleep(Math.random() * 5 + 3); // 思考時間: 3-8秒
// カートに追加(15%の確率)
if (Math.random() < 0.15) {
const cartRes = http.post(
`${BASE_URL}/api/cart/items`,
JSON.stringify({ productId, quantity: 1 }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(cartRes, {
'add to cart: status 201': (r) => r.status === 201,
});
errorRate.add(cartRes.status !== 201);
}
sleep(Math.random() * 2 + 1); // 思考時間: 1-3秒
}
シナリオベースのテスト
// k6/scenario-test.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
export const options = {
scenarios: {
// シナリオ1: ブラウジングユーザー(60%)
browsing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 120 },
{ duration: '10m', target: 120 },
{ duration: '2m', target: 0 },
],
exec: 'browsingScenario',
},
// シナリオ2: 検索ユーザー(25%)
searching: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 },
{ duration: '10m', target: 50 },
{ duration: '2m', target: 0 },
],
exec: 'searchingScenario',
},
// シナリオ3: 購入ユーザー(15%)
purchasing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 30 },
{ duration: '10m', target: 30 },
{ duration: '2m', target: 0 },
],
exec: 'purchasingScenario',
},
// シナリオ4: スパイクテスト(別タイミングで実行)
spike: {
executor: 'ramping-arrival-rate', // Open Model
startRate: 0,
timeUnit: '1s',
preAllocatedVUs: 500,
maxVUs: 1000,
stages: [
{ duration: '5m', target: 100 }, // 通常: 100 req/s
{ duration: '30s', target: 1000 }, // スパイク: 1000 req/s
{ duration: '5m', target: 100 }, // 回復: 100 req/s
],
exec: 'spikeScenario',
startTime: '15m', // 他のシナリオが安定してから開始
},
},
thresholds: {
'http_req_duration{scenario:browsing}': ['p(95)<300'],
'http_req_duration{scenario:searching}': ['p(95)<500'],
'http_req_duration{scenario:purchasing}': ['p(95)<800'],
'http_req_duration{scenario:spike}': ['p(95)<2000'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export function browsingScenario() {
group('browsing', function () {
http.get(`${BASE_URL}/`);
sleep(3);
http.get(`${BASE_URL}/api/products?category=electronics&page=1`);
sleep(5);
const pid = Math.floor(Math.random() * 10000) + 1;
http.get(`${BASE_URL}/api/products/${pid}`);
sleep(8);
});
}
export function searchingScenario() {
group('searching', function () {
const keywords = ['ワイヤレスイヤホン', 'ノートPC', 'キーボード', 'モニター'];
const keyword = keywords[Math.floor(Math.random() * keywords.length)];
http.get(`${BASE_URL}/api/search?q=${encodeURIComponent(keyword)}`);
sleep(4);
const pid = Math.floor(Math.random() * 10000) + 1;
http.get(`${BASE_URL}/api/products/${pid}`);
sleep(5);
});
}
export function purchasingScenario() {
group('purchasing', function () {
// ログイン
const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
email: `user${Math.floor(Math.random() * 10000)}@example.com`,
password: 'testpassword',
}), { headers: { 'Content-Type': 'application/json' } });
const token = loginRes.json('token');
const authHeaders = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
};
sleep(2);
// 商品閲覧 → カート追加 → チェックアウト
const pid = Math.floor(Math.random() * 10000) + 1;
http.get(`${BASE_URL}/api/products/${pid}`);
sleep(10);
http.post(`${BASE_URL}/api/cart/items`,
JSON.stringify({ productId: pid, quantity: 1 }),
authHeaders
);
sleep(3);
http.get(`${BASE_URL}/api/cart`, authHeaders);
sleep(5);
http.post(`${BASE_URL}/api/orders`, JSON.stringify({
paymentMethod: 'credit_card',
}), authHeaders);
});
}
export function spikeScenario() {
http.get(`${BASE_URL}/api/products?page=1&limit=20`);
}
2. Gatling の基本
Gatling シナリオ(Scala DSL)
// src/test/scala/simulations/ECSiteSimulation.scala
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class ECSiteSimulation extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:3000")
.acceptHeader("application/json")
.contentTypeHeader("application/json")
.shareConnections // コネクション共有でリアルなブラウザ挙動に
// テストデータのフィーダー
val productFeeder = csv("products.csv").random
val userFeeder = csv("users.csv").circular
// シナリオ1: ブラウジング
val browsingScenario = scenario("Browsing")
.feed(productFeeder)
.exec(
http("Product List")
.get("/api/products?page=1&limit=20")
.check(status.is(200))
.check(jsonPath("$[0].id").saveAs("productId"))
)
.pause(2, 5) // 2-5秒のランダム思考時間
.exec(
http("Product Detail")
.get("/api/products/${productId}")
.check(status.is(200))
)
.pause(3, 8)
// シナリオ2: 購入フロー
val purchaseScenario = scenario("Purchase")
.feed(userFeeder)
.feed(productFeeder)
.exec(
http("Login")
.post("/api/auth/login")
.body(StringBody("""{"email":"${email}","password":"${password}"}"""))
.check(status.is(200))
.check(jsonPath("$.token").saveAs("authToken"))
)
.pause(1, 3)
.exec(
http("Add to Cart")
.post("/api/cart/items")
.header("Authorization", "Bearer ${authToken}")
.body(StringBody("""{"productId":${productId},"quantity":1}"""))
.check(status.is(201))
)
.pause(2, 5)
.exec(
http("Checkout")
.post("/api/orders")
.header("Authorization", "Bearer ${authToken}")
.body(StringBody("""{"paymentMethod":"credit_card"}"""))
.check(status.is(201))
)
setUp(
browsingScenario.inject(
rampUsers(500).during(5.minutes), // 5分で500ユーザー
constantUsersPerSec(20).during(10.minutes), // 毎秒20ユーザー追加
),
purchaseScenario.inject(
rampUsers(100).during(5.minutes),
constantUsersPerSec(5).during(10.minutes),
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile3.lt(500), // p95 < 500ms
global.responseTime.percentile4.lt(1000), // p99 < 1000ms
global.successfulRequests.percent.gt(99), // 成功率 > 99%
)
}
3. 分散負荷テスト
// k6 の分散実行(k6 Cloud or 自前クラスタ)
// docker-compose.yml による分散構成
/*
version: '3'
services:
k6-master:
image: grafana/k6
command: run --out influxdb=http://influxdb:8086/k6 /scripts/test.js
volumes:
- ./scripts:/scripts
environment:
- K6_VUS=200
- BASE_URL=http://target-app:3000
k6-worker-1:
image: grafana/k6
command: run --out influxdb=http://influxdb:8086/k6 /scripts/test.js
volumes:
- ./scripts:/scripts
environment:
- K6_VUS=200
- BASE_URL=http://target-app:3000
k6-worker-2:
image: grafana/k6
command: run --out influxdb=http://influxdb:8086/k6 /scripts/test.js
volumes:
- ./scripts:/scripts
environment:
- K6_VUS=200
- BASE_URL=http://target-app:3000
influxdb:
image: influxdb:1.8
ports:
- "8086:8086"
grafana:
image: grafana/grafana
ports:
- "3001:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
*/
結果の可視化と分析
// k6 結果を InfluxDB + Grafana で可視化するパイプライン
// 実行コマンド: k6 run --out influxdb=http://localhost:8086/k6 test.js
// k6 の handleSummary でカスタムレポート生成
export function handleSummary(data: any): Record<string, string> {
const summary = {
totalRequests: data.metrics.http_reqs.values.count,
rps: data.metrics.http_reqs.values.rate.toFixed(1),
p50: data.metrics.http_req_duration.values['p(50)'].toFixed(1),
p95: data.metrics.http_req_duration.values['p(95)'].toFixed(1),
p99: data.metrics.http_req_duration.values['p(99)'].toFixed(1),
errorRate: (data.metrics.http_req_failed.values.rate * 100).toFixed(2),
};
const report = `
# Load Test Report
- Date: ${new Date().toISOString()}
- Total Requests: ${summary.totalRequests}
- RPS: ${summary.rps}
- Latency p50: ${summary.p50}ms
- Latency p95: ${summary.p95}ms
- Latency p99: ${summary.p99}ms
- Error Rate: ${summary.errorRate}%
`;
return {
'stdout': report,
'results/summary.json': JSON.stringify(data, null, 2),
'results/report.md': report,
};
}
4. k6 vs Gatling の選択基準
| 観点 | k6 | Gatling |
|---|---|---|
| 言語 | JavaScript/TypeScript | Scala (Java) |
| 学習コスト | 低(Web 開発者に馴染み) | 中(Scala DSL の習得が必要) |
| プロトコル | HTTP, WebSocket, gRPC | HTTP, WebSocket, JMS |
| レポート | テキスト + 外部ツール連携 | リッチな HTML レポート内蔵 |
| 分散実行 | k6 Cloud / 自前クラスタ | Enterprise 版 / 自前 |
| CI/CD 統合 | CLI ベースで容易 | Maven/Gradle プラグイン |
| リソース効率 | Go ランタイムで軽量 | JVM で重め |
| 推奨ケース | API テスト、CI/CD組込み | 複雑なシナリオ、エンタープライズ |
コラム: k6 拡張と xk6
k6 は Go で書かれており、xk6 でカスタム拡張を追加できる。
# SQL データベースへの直接負荷テスト
xk6 build --with github.com/grafana/xk6-sql
# Kafka への負荷テスト
xk6 build --with github.com/mostafa/xk6-kafka
# ブラウザテスト(k6 browser)
# k6 v0.46+ でブラウザベースの負荷テストが可能
# Playwright ベースで Core Web Vitals も測定
k6 browser は Lighthouse とは異なり、並列ブラウザでの負荷テストが可能。
まとめ
| トピック | 要点 |
|---|---|
| k6 基本 | JavaScript ベース、stages でVU制御、thresholds で合否判定 |
| シナリオ分離 | 複数の executor で異なるユーザー行動を同時再現 |
| Gatling | Scala DSL、feeder でデータ駆動、リッチレポート |
| 分散実行 | 大規模テストは複数マシンで負荷生成、結果は集約 |
| ツール選択 | チームスキルとCI/CD要件で判断 |
チェックリスト
- k6 のテストスクリプトを書ける(stages、check、thresholds)
- 複数シナリオの同時実行を設定できる
- Open Model(arrival-rate)と Closed Model(VU)の違いを理解した
- Gatling のシナリオ構造を理解した
- テスト結果を可視化する方法を知っている
次のステップへ
負荷テストツールの使い方を学んだ。次は 容量計画とスケーリング戦略 で、テスト結果を基にしたキャパシティプランニングを学ぼう。
推定読了時間: 40分