在没有进行负载测试的情况下交付产品是一场赌博。在用户为你找到突破点之前,你并不知道你的突破点——通常是在产品发布、病毒式传播或销售高峰期间。 k6 是现代负载测试工具,可让您用 JavaScript 编写测试,从 CI 运行它们,并在用户之前发现您的性能上限。它对开发人员友好、资源高效(k6 使用 goroutine,而不是线程),并与 Grafana 和 Prometheus 干净地集成以获取实时指标。
属于我们的性能与可扩展性系列
阅读完整指南在没有进行负载测试的情况下交付产品是一场赌博。在用户为你找到突破点之前,你并不知道你的突破点——通常是在产品发布、病毒式传播或销售高峰期间。 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 | p95 | p99 | 错误预算 |
|---|---|---|---|---|
GET /(主页) | 120 毫秒 | 400 毫秒 | 800 毫秒 | 0.1% |
GET /api/contacts | 50 毫秒 | 150 毫秒 | 300 毫秒 | 0.5% |
POST /api/contacts | 80 毫秒 | 200 毫秒 | 400 毫秒 | 0.5% |
POST /auth/login | 200 毫秒 | 500 毫秒 | 1000 毫秒 | 1% |
POST /billing/checkout | 500 毫秒 | 1500 毫秒 | 3000 毫秒 | 1% |
GET /blog/[slug] | 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 ERP、Shopify 电子商务、AI 智能体、Power BI 分析、GoHighLevel 自动化以及企业软件最佳实践。我们的指南帮助企业做出明智的技术决策。
相关文章
Hepsiburada API 与 Odoo 集成:完整设置指南
通过 API 将 Hepsiburada 与 Odoo ERP 集成的完整指南。在土耳其值得信赖的市场上实现订单、库存和履行的自动化。
Shopify 集成中心:2026 年如何将 Shopify 连接到任何系统
Shopify 集成的完整指南:API、Webhooks、中间件、iPaaS 方法。将 Shopify 连接到 ERP、会计、CRM、市场和 POS 系统。
API 速率限制:模式和最佳实践
通过令牌桶、滑动窗口和固定计数器模式掌握 API 速率限制。使用 NestJS 节流器、Redis 和实际配置示例保护您的后端。
更多来自性能与可扩展性
Webhook 调试和监控:完整的故障排除指南
通过这份涵盖故障模式、调试工具、重试策略、监控仪表板和安全最佳实践的完整指南掌握 Webhook 调试。
Nginx 生产配置:SSL、缓存和安全性
Nginx 生产配置指南:SSL 终止、HTTP/2、缓存标头、安全标头、速率限制、反向代理设置和 Cloudflare 集成模式。
Odoo 性能调优:PostgreSQL 和服务器优化
Odoo 19 性能调优专家指南。涵盖 PostgreSQL 配置、索引、查询优化、Nginx 缓存和企业部署的服务器大小调整。
在生产中测试和监控 AI 代理
在生产环境中测试和监控 AI 代理的完整指南。涵盖 OpenClaw 部署的评估框架、可观测性、漂移检测和事件响应。
使用 OpenClaw 进行合规性监控代理
部署 OpenClaw AI 代理以进行持续合规性监控。自动执行监管检查、策略执行、审计跟踪生成和合规性报告。
优化 AI 代理成本:令牌使用和缓存
通过令牌优化、缓存、模型路由和使用情况监控来降低 AI 代理运营成本的实用策略。生产 OpenClaw 部署真正节省了成本。