属于我们的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 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.
相关文章
Odoo 19 HR:技能矩阵、职业规划、绩效周期
Odoo 19 HR 升级:本地技能矩阵、职业道路规划、绩效评估周期、9 框网格、继任计划、HRIS 集成。
Odoo 19 性能基准:PostgreSQL 17 调整数字
真实的 Odoo 19 性能基准:Web 客户端速度、ORM 吞吐量、PG17 调整设置、连接池、工作线程数、扩展阈值。
Odoo ORM API Cheat Sheet 2026:搜索、读取、写入、创建
实用的 Odoo ORM 备忘单,包含示例:搜索、浏览、读取、写入、创建、取消链接、记录集、域、计算字段、性能提示。
更多来自Performance & Scalability
Odoo 19 HR:技能矩阵、职业规划、绩效周期
Odoo 19 HR 升级:本地技能矩阵、职业道路规划、绩效评估周期、9 框网格、继任计划、HRIS 集成。
Odoo 19 性能基准:PostgreSQL 17 调整数字
真实的 Odoo 19 性能基准:Web 客户端速度、ORM 吞吐量、PG17 调整设置、连接池、工作线程数、扩展阈值。
OpenClaw 大规模成本优化和代币效率
OpenClaw 令牌成本优化:提示缓存、模型路由、响应缓存、批处理 API 和生产代理的每租户成本护栏。
Power BI 增量刷新超过 1000 万行的表
适用于 10M 以上行表的 Power BI 增量刷新手册:分区设计、RangeStart/RangeEnd、刷新策略、查询折叠和 DirectQuery 混合。
Webhook 调试和监控:完整的故障排除指南
通过这份涵盖故障模式、调试工具、重试策略、监控仪表板和安全最佳实践的完整指南掌握 Webhook 调试。
Nginx 生产配置:SSL、缓存和安全性
Nginx 生产配置指南:SSL 终止、HTTP/2、缓存标头、安全标头、速率限制、反向代理设置和 Cloudflare 集成模式。