属于我们的Performance & Scalability系列
阅读完整指南k6 负载测试:在发布前对您的 API 进行压力测试
在没有进行负载测试的情况下交付产品是一场赌博。在用户为你找到突破点之前,你并不知道你的突破点——通常是在产品发布、病毒式传播或销售高峰期间。 k6 是现代负载测试工具,可让您用 JavaScript 编写测试,从 CI 运行它们,并在用户之前发现您的性能上限。它对开发人员友好、资源高效(k6 使用 goroutine,而不是线程),并与 Grafana 和 Prometheus 干净地集成以获取实时指标。
本指南涵盖了 k6,从第一个脚本到复杂的多场景负载测试、自定义指标、阈值、CI 集成以及 Node.js/NestJS API 的生产安全测试模式。
要点
- k6 脚本是 JavaScript,但在 Go 运行时中运行 - 没有 Node.js API(没有
require、没有fs、没有setTimeout)- 虚拟用户(VU)是并发模拟用户;迭代是单独的脚本执行
- 运行前始终设置阈值 - 失败的阈值会停止测试并导致 CI 失败
- 使用场景(constant-vus、ramping-vus、constant-arrival-rate)来模拟真实流量模式
- 切勿在未与运营人员协调的情况下加载测试生产 - 针对登台或生产克隆进行测试
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 | p50 | p95 | p95 | p99 | 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 数和每秒请求数有什么区别?
VU(虚拟用户)是并发模拟用户。每个 VU 在循环中运行默认函数。每秒请求数取决于每次迭代的运行速度。如果每个 VU 迭代需要 2 秒(包括睡眠时间)并且您有 100 个 VU,则您将获得大约 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 都非常轻量级(一个 Goroutine)——对于同等的 VU 数量,k6 使用的资源比 JMeter 或 Gadling 少得多。
后续步骤
在用户发现应用程序处于压力下之前,负载测试可以揭示应用程序的真实性能概况。本指南中的模式(斜坡场景、自定义阈值、CI 集成和 Grafana 仪表板)为您提供持续查找和修复性能瓶颈的基础架构。
ECOSIRE 通过 k6 负载测试构建经过性能验证的 NestJS API,涵盖主页、API 端点、结帐流程和完整的站点地图爬网。 探索我们的后端工程服务,了解我们如何从第一天开始就提高性能。
作者
ECOSIRE Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 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.