Performance & Scalabilityシリーズの一部
完全ガイドを読む負荷テストを行わずに製品を出荷するのはギャンブルです。ユーザーが限界点を見つけるまでは、通常は製品の発売時、バイラルな瞬間、売上の急増時に限界点が分かりません。 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 TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
関連記事
Shopify 速度の最適化: ウェブの重要な要素を実際に動かす技術的チェックリスト (2026)
実店舗での LCP、INP、CLS を実際に改善するもの、時間を無駄にするもの、アプリとテーマを監査する方法について、フィールドでテストされた 2026 年の Shopify スピード チェックリスト。
Odoo 19 HR: スキル マトリックス、キャリア プラン、パフォーマンス サイクル
Odoo 19 HR アップグレード: ネイティブ スキル マトリックス、キャリア パス計画、パフォーマンス レビュー サイクル、9 ボックス グリッド、後継者計画、HRIS 統合。
Odoo 19 パフォーマンス ベンチマーク: PostgreSQL 17 のチューニング数値
実際の Odoo 19 パフォーマンス ベンチマーク: Web クライアント速度、ORM スループット、PG17 チューニング設定、接続プーリング、ワーカー数、スケーリングしきい値。
Performance & Scalabilityのその他の記事
Shopify 速度の最適化: ウェブの重要な要素を実際に動かす技術的チェックリスト (2026)
実店舗での LCP、INP、CLS を実際に改善するもの、時間を無駄にするもの、アプリとテーマを監査する方法について、フィールドでテストされた 2026 年の Shopify スピード チェックリスト。
テクニカル SEO 監査チェックリスト 2026: すべてのクライアント サイトで実行する 47 のチェック
2026 年にすべてのクライアント サイトで実行する 47 項目の技術的な SEO 監査チェックリスト (クロール可能性、インデックス付け、正規化、hreflang、Core Web Vitals、ログ)。
Odoo 19 HR: スキル マトリックス、キャリア プラン、パフォーマンス サイクル
Odoo 19 HR アップグレード: ネイティブ スキル マトリックス、キャリア パス計画、パフォーマンス レビュー サイクル、9 ボックス グリッド、後継者計画、HRIS 統合。
Odoo 19 パフォーマンス ベンチマーク: PostgreSQL 17 のチューニング数値
実際の Odoo 19 パフォーマンス ベンチマーク: Web クライアント速度、ORM スループット、PG17 チューニング設定、接続プーリング、ワーカー数、スケーリングしきい値。
OpenClaw のコスト最適化と大規模なトークン効率
OpenClaw トークン コストの最適化: プロンプト キャッシュ、モデル ルーティング、応答キャッシュ、バッチ API、実稼働エージェントのテナントごとのコスト ガードレール。
1,000 万行を超えるテーブルの Power BI 増分更新
1,000 万行以上のテーブル用の Power BI 増分更新プレイブック: パーティション設計、RangeStart/RangeEnd、更新ポリシー、クエリの折りたたみ、DirectQuery ハイブリッド。