Performance & Scalabilityシリーズの一部
完全ガイドを読むk6 負荷テスト: 起動前に API のストレス テストを行う
負荷テストを行わずに製品を出荷するのはギャンブルです。ユーザーが限界点を見つけるまでは、通常は製品の発売時、バイラルな瞬間、売上の急増時に限界点が分かりません。 k6 は、JavaScript でテストを作成し、CI から実行して、ユーザーが行う前にパフォーマンスの上限を発見できる最新の負荷テスト ツールです。開発者にとって使いやすく、リソース効率が高く (k6 はスレッドではなくゴルーチンを使用します)、リアルタイム メトリクスのために Grafana および Prometheus ときれいに統合されます。
このガイドでは、最初のスクリプトから複雑なマルチシナリオ負荷テスト、カスタム メトリクス、しきい値、CI 統合、Node.js/NestJS API の実稼働環境に安全なテスト パターンまで、k6 について説明します。
重要なポイント
- k6 スクリプトは JavaScript ですが、Go ランタイムで実行されます — Node.js API はありません (
require、fs、setTimeoutはありません)- 仮想ユーザー (VU) は、シミュレートされた同時ユーザーです。反復は個々のスクリプトの実行です
- 実行前に常にしきい値を設定します。しきい値に失敗するとテストが停止し、CI が失敗します。
- シナリオ (一定対、ランピング対、一定到着率) を使用して実際のトラフィック パターンをモデル化する
- 運用と調整せずにテスト本番環境をロードしないでください。ステージングまたは本番環境のクローンに対してテストします。
http_req_durationメトリックは主要な SLA メトリックです。p95 と p99 は平均よりも重要です- k6 ランナーのレート制限 — 負荷テストのトラフィックがレート制限の予算を使い果たさないようにする必要があります
- 地理的領域にわたる分散負荷テストには k6 Cloud または Grafana k6 を使用します
インストールと最初のスクリプト
# macOS
brew install k6
# Ubuntu/Debian
sudo gpg -k && sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
# Windows (via Chocolatey)
choco install k6
最初の k6 スクリプト:
// k6/homepage-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// Custom metric: error rate
const errorRate = new Rate('error_rate');
export const options = {
// Ramp up to 50 VUs over 1 minute, hold 2 minutes, ramp down
stages: [
{ duration: '1m', target: 50 },
{ duration: '2m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th pct < 500ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
error_rate: ['rate<0.01'],
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
const res = http.get(`${BASE_URL}/`);
const ok = check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains ECOSIRE': (r) => r.body.includes('ECOSIRE'),
});
errorRate.add(!ok);
sleep(1); // 1 second think time between iterations
}
実行してください:
k6 run k6/homepage-load.js
k6 run --env BASE_URL=https://staging.ecosire.com k6/homepage-load.js
k6 run --vus 100 --duration 30s k6/homepage-load.js # Quick smoke test
認証を使用した API 負荷テスト
// k6/api-load.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Counter, Trend } from 'k6/metrics';
const apiCalls = new Counter('api_calls');
const authDuration = new Trend('auth_duration');
export const options = {
scenarios: {
// Constant arrival rate: 100 requests/second regardless of VU count
constant_load: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '3m',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
thresholds: {
http_req_duration: ['p(95)<300', 'p(99)<500'],
'http_req_duration{type:api}': ['p(95)<200'],
http_req_failed: ['rate<0.005'], // 0.5% error budget
},
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001';
// setup() runs once before VUs start
export function setup() {
// Authenticate and return tokens for VUs to use
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: __ENV.TEST_EMAIL,
password: __ENV.TEST_PASSWORD,
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, { 'login successful': (r) => r.status === 200 });
const { accessToken } = loginRes.json();
return { accessToken };
}
export default function (data) {
const { accessToken } = data;
const headers = {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
};
group('Contacts API', () => {
// List contacts
const listRes = http.get(`${BASE_URL}/contacts?page=1&limit=20`, {
headers,
tags: { type: 'api', endpoint: 'contacts-list' },
});
check(listRes, {
'list contacts: 200': (r) => r.status === 200,
'list contacts: has items': (r) => r.json('data') !== null,
});
apiCalls.add(1);
sleep(0.5);
// Create a contact
const createRes = http.post(`${BASE_URL}/contacts`, JSON.stringify({
name: `Load Test User ${Date.now()}`,
email: `load-test-${Date.now()}@example.com`,
}), {
headers,
tags: { type: 'api', endpoint: 'contacts-create' },
});
check(createRes, {
'create contact: 201': (r) => r.status === 201,
});
apiCalls.add(1);
});
sleep(1);
}
// teardown() runs once after all VUs finish
export function teardown(data) {
// Clean up test data if needed
console.log(`Test complete. Data: ${JSON.stringify(data)}`);
}
VU のランピング シナリオ
複数の段階とシナリオを使用して実際のトラフィック パターンをモデル化します。
// k6/stress-test.js
export const options = {
scenarios: {
// Scenario 1: Normal traffic baseline
normal_traffic: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 20 },
{ duration: '5m', target: 20 },
],
gracefulRampDown: '30s',
},
// Scenario 2: Sudden spike (marketing campaign goes live)
traffic_spike: {
executor: 'ramping-vus',
startVUs: 0,
startTime: '7m', // Starts after baseline is stable
stages: [
{ duration: '30s', target: 200 }, // Rapid spike
{ duration: '2m', target: 200 }, // Hold spike
{ duration: '30s', target: 20 }, // Fall back
],
},
// Scenario 3: Soak test for memory leaks
soak_test: {
executor: 'constant-vus',
vus: 10,
duration: '30m', // Run for 30 minutes
startTime: '10m',
},
},
thresholds: {
'http_req_duration{scenario:normal_traffic}': ['p(95)<300'],
'http_req_duration{scenario:traffic_spike}': ['p(95)<800'],
http_req_failed: ['rate<0.02'],
},
};
ブログ/サイトマップのクロール テスト
// k6/sitemap-crawl.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
// Load URLs from sitemap (pre-fetched and saved as JSON)
const urls = new SharedArray('sitemap urls', function () {
// In real usage, fetch sitemap XML and parse URLs beforehand
// SharedArray is loaded once and shared across all VUs (memory efficient)
return JSON.parse(open('./sitemap-urls.json'));
});
export const options = {
vus: 20,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<2000'], // Public pages can be slower
http_req_failed: ['rate<0.01'],
},
};
export default function () {
// Each VU picks a random URL from the sitemap
const url = urls[Math.floor(Math.random() * urls.length)];
const res = http.get(url, {
headers: {
// Identify as load tester in logs
'User-Agent': 'k6-load-tester/1.0',
},
timeout: '10s',
});
check(res, {
'status is 200': (r) => r.status === 200,
'no error page': (r) => !r.body.includes('Error'),
'has canonical tag': (r) => r.body.includes('rel="canonical"'),
'response time < 2000ms': (r) => r.timings.duration < 2000,
});
sleep(Math.random() * 2 + 1); // Random 1-3 second think time
}
カスタムメトリクスとしきい値
// k6/checkout-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter, Rate, Trend, Gauge } from 'k6/metrics';
// Custom metrics for business-specific tracking
const checkoutSuccesses = new Counter('checkout_successes');
const checkoutFailures = new Counter('checkout_failures');
const checkoutDuration = new Trend('checkout_duration');
const activeCheckouts = new Gauge('active_checkouts');
export const options = {
stages: [
{ duration: '1m', target: 10 },
{ duration: '3m', target: 30 },
{ duration: '1m', target: 0 },
],
thresholds: {
checkout_duration: ['p(95)<3000'], // Checkout < 3s at p95
checkout_failures: ['count<10'], // Max 10 absolute failures
http_req_duration: ['p(99)<2000'],
},
};
export default function (data) {
activeCheckouts.add(1);
const start = Date.now();
// Step 1: Add to cart
const cartRes = http.post('/api/cart/add', JSON.stringify({
productId: 'prod_odoo_customization',
quantity: 1,
}), { headers: { 'Content-Type': 'application/json' } });
if (!check(cartRes, { 'add to cart: 200': (r) => r.status === 200 })) {
checkoutFailures.add(1);
activeCheckouts.add(-1);
return;
}
sleep(2);
// Step 2: Create checkout session
const checkoutRes = http.post('/api/billing/checkout', JSON.stringify({
cartId: cartRes.json('cartId'),
}), { headers: { 'Content-Type': 'application/json' } });
const success = check(checkoutRes, {
'checkout session created': (r) => r.status === 200,
'has session URL': (r) => r.json('url') !== null,
});
if (success) {
checkoutSuccesses.add(1);
checkoutDuration.add(Date.now() - start);
} else {
checkoutFailures.add(1);
}
activeCheckouts.add(-1);
sleep(1);
}
Grafana 出力の統合
k6 メトリクスを InfluxDB にストリーミングし、テスト中に Grafana で視覚化します。
# Run with InfluxDB output
k6 run --out influxdb=http://localhost:8086/k6 k6/api-load.js
# Or use the k6 Grafana integration (k6 v0.45+)
k6 run --out experimental-prometheus-rw k6/api-load.js
# docker-compose.monitoring.yml
services:
influxdb:
image: influxdb:2.7
ports: ["8086:8086"]
environment:
INFLUXDB_DB: k6
INFLUXDB_ADMIN_USER: admin
INFLUXDB_ADMIN_PASSWORD: password
grafana:
image: grafana/grafana:latest
ports: ["3030:3000"]
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
volumes:
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/datasources:/etc/grafana/provisioning/datasources
CI の統合
# .github/workflows/ci.yml (load test section)
load-tests:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Only on main branch
needs: [deploy-staging]
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \
| sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
- name: Run smoke test (30 VUs, 60s)
run: |
k6 run --vus 30 --duration 60s \
--env BASE_URL=${{ secrets.STAGING_URL }} \
--env TEST_EMAIL=${{ secrets.TEST_EMAIL }} \
--env TEST_PASSWORD=${{ secrets.TEST_PASSWORD }} \
apps/api/k6/api-load.js
- name: Upload k6 results
uses: actions/upload-artifact@v4
if: always()
with:
name: k6-results
path: k6-results/
パフォーマンスのベースライン
ベースラインを確立し、回帰を警告します。
| エンドポイント | p50 | p95 | p99 | エラーバジェット |
|---|---|---|---|---|
GET / (ホームページ) | 120ミリ秒 | 400ミリ秒 | 800ミリ秒 | 0.1% |
| コード0 | 50ミリ秒 | 150ミリ秒 | 300ミリ秒 | 0.5% |
| コード0 | 80ミリ秒 | 200ミリ秒 | 400ミリ秒 | 0.5% |
| コード0 | 200ミリ秒 | 500ミリ秒 | 1000ミリ秒 | 1% |
| コード0 | 500ミリ秒 | 1500ミリ秒 | 3000ミリ秒 | 1% |
| コード0 | 100ミリ秒 | 300ミリ秒 | 600ミリ秒 | 0.1% |
よくある質問
VU と 1 秒あたりのリクエストの違いは何ですか?
VU (仮想ユーザー) は、シミュレートされた同時ユーザーです。各 VU はデフォルトの関数をループで実行します。 1 秒あたりのリクエスト数は、各反復の実行速度によって異なります。各 VU の反復に 2 秒かかり (スリープ時間を含む)、VU が 100 個ある場合、約 50 RPS が得られます。反復期間に関係なく、特定の RPS をターゲットにする必要がある場合は、constant-arrival-rate エグゼキュータを使用します。
本番環境またはステージングに対して負荷テストを行うべきですか?
常にステージングを好みます。実稼働負荷テストでは、実際のユーザーに影響を与え、レート制限を使い果たし、テストデータから実際の注文を作成し、実際の Webhook イベントをトリガーするリスクがあります。運用環境をテストする必要がある場合は、トラフィックが少ない時間帯にテストし、テスト支払いトークンを使用し、ロールバック計画を立ててください。 k6 Cloud は地理的に分散された負荷生成をサポートしているため、オリジンに触れることなく CDN エッジのパフォーマンスをテストできます。
長いテスト中に認証トークンの有効期限が切れた場合、どのように処理すればよいですか?
短いテスト (15 分未満) の場合は、setup() でトークンを取得し、data 経由ですべての VU に渡します。長いテスト (15 分以上) の場合は、テスト スクリプトにトークンの更新を実装します。デフォルトの関数でトークンの有効期間を確認し、有効期限が近づいたら更新します。トークンを各 VU のローカルな JavaScript 変数に保存します。
stages と scenarios の違いは何ですか?
stages は単一の ramping-vus シナリオの短縮形であり、単純なランプアップ/ホールド/ランプダウン パターンに適しています。 scenarios を使用すると、複数の同時トラフィック パターン、シナリオごとの異なるエグゼキュータ、シナリオごとのしきい値、メトリクスでのシナリオのタグ付けなどを完全に制御できます。現実的なマルチパターン テスト (ベースライン + スパイク + ソークを同時に行う) には、scenarios を使用します。
単一の k6 プロセスはいくつの VU を処理できますか?
最新のハードウェア上の単一の k6 プロセスは、スクリプトの複雑さと応答サイズに応じて、5,000 ~ 10,000 の VU を維持し、50,000 ~ 100,000 の RPS を生成できます。負荷が高い場合は、k6 cloud を使用するか、負荷ディストリビューターの背後で複数の k6 インスタンスを実行します。各 VU は非常に軽量 (ゴルーチン) です。k6 は、同等の VU 数に対して JMeter や Gatling よりもはるかに少ないリソースを使用します。
次のステップ
負荷テストにより、ユーザーがアプリケーションにストレスを感じる前に、アプリケーションの真のパフォーマンス プロファイルが明らかになります。このガイドのパターン (ランピング シナリオ、カスタムしきい値、CI 統合、Grafana ダッシュボード) は、パフォーマンスのボトルネックを継続的に見つけて修正するためのインフラストラクチャを提供します。
ECOSIRE は、ホームページ、API エンドポイント、チェックアウト フロー、完全なサイトマップ クロールをカバーする k6 負荷テストを使用して、パフォーマンスが検証された NestJS API を構築します。 当社のバックエンド エンジニアリング サービスをご覧ください して、初日からパフォーマンスを向上させるために当社がどのように構築しているかをご覧ください。
執筆者
ECOSIRE Research and Development Team
ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。
関連記事
API Rate Limiting: Patterns and Best Practices
Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.
GoHighLevel API and Webhooks: Custom Integrations
Complete developer guide to GoHighLevel API and webhooks. Build custom integrations, sync data with external systems, and extend GHL capabilities with REST API and webhook automation.
NestJS 11 Enterprise API Patterns
Master NestJS 11 enterprise patterns: guards, interceptors, pipes, multi-tenancy, and production-ready API design for scalable backend systems.
Performance & Scalabilityのその他の記事
Nginx Production Configuration: SSL, Caching, and Security
Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Odoo vs Acumatica: Cloud ERP for Growing Businesses
Odoo vs Acumatica compared for 2026: unique pricing models, scalability, manufacturing depth, and which cloud ERP fits your growth trajectory.
Testing and Monitoring AI Agents in Production
A complete guide to testing and monitoring AI agents in production environments. Covers evaluation frameworks, observability, drift detection, and incident response for OpenClaw deployments.
Compliance Monitoring Agents with OpenClaw
Deploy OpenClaw AI agents for continuous compliance monitoring. Automate regulatory checks, policy enforcement, audit trail generation, and compliance reporting.
Optimizing AI Agent Costs: Token Usage and Caching
Practical strategies for reducing AI agent operational costs through token optimization, caching, model routing, and usage monitoring. Real savings from production OpenClaw deployments.