k6 进阶实战——场景模拟、阈值、自定义指标、Grafana 可视化
k6 进阶实战——场景模拟、阈值、自定义指标、Grafana 可视化
适读人群:有k6基础的工程师、需要搭建性能监控大盘的DevOps | 阅读时长:约15分钟 | 核心价值:掌握k6的高级用法,把压测数据接入Grafana,实现实时可视化监控
压测大盘救了我们的一次发版
2023年双十一前一个月,我们在测试环境做全链路压测。用的是k6,结果实时推送到InfluxDB,Grafana做监控大盘。
压测跑到第23分钟,Grafana大盘上P99曲线开始缓慢上扬——从280ms慢慢爬到了450ms,但TPS没有变化,错误率也是0。
如果只看压测结束后的报告,这个问题可能被平均掉了。但因为有实时曲线,我当场叫停了压测,让研发查原因。
最后定位到:从第20分钟开始,后端有一个缓存定时刷新任务启动了,它在查询全量商品数据写入Redis,和压测流量竞争数据库连接,导致查询变慢。
这个任务生产上也是定时启动的,如果不发现,双十一期间每次缓存刷新都会造成一波P99飙升。
这次实时监控帮我们提前发现了一个生产隐患。
Scenarios(场景)深入
k6的scenarios是比vus + duration更强大的配置方式,支持多种并发模式。
六种 Executor 类型
export const options = {
scenarios: {
// 1. shared-iterations:所有VU共享固定迭代次数
fixed_requests: {
executor: 'shared-iterations',
vus: 10,
iterations: 1000, // 10个VU合计跑1000次
maxDuration: '5m',
},
// 2. per-vu-iterations:每个VU跑固定次数
each_vu_fixed: {
executor: 'per-vu-iterations',
vus: 50,
iterations: 100, // 每个VU跑100次,共5000次
},
// 3. constant-vus:固定VU数跑固定时长
steady_load: {
executor: 'constant-vus',
vus: 200,
duration: '10m',
},
// 4. ramping-vus:VU数随时间变化(最常用)
ramp_up_down: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '1m', target: 0 },
],
},
// 5. constant-arrival-rate:固定到达率(RPS控制,不依赖响应时间)
fixed_rps: {
executor: 'constant-arrival-rate',
rate: 100, // 每秒100个请求
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 200,
maxVUs: 500,
},
// 6. ramping-arrival-rate:到达率随时间变化
ramp_rps: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 200 },
],
preAllocatedVUs: 300,
},
},
};最关键的选择:ramping-vus vs constant-arrival-rate
ramping-vus:控制并发用户数。响应时间变慢时,RPS会自动降低(每个VU在等待响应)constant-arrival-rate:控制到达率(RPS)。响应时间变慢时,k6会自动增加VU来维持RPS
大多数场景用ramping-vus。但如果你要测"系统在固定QPS下的稳定性",用constant-arrival-rate。
多场景混合压测
模拟真实的流量混合:
export const options = {
scenarios: {
// 70%流量:浏览商品
browse_products: {
executor: 'constant-arrival-rate',
rate: 70,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 100,
exec: 'browseScenario', // 指定执行函数
},
// 20%流量:搜索
search: {
executor: 'constant-arrival-rate',
rate: 20,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 40,
exec: 'searchScenario',
},
// 10%流量:下单
create_order: {
executor: 'constant-arrival-rate',
rate: 10,
timeUnit: '1s',
duration: '10m',
preAllocatedVUs: 30,
exec: 'orderScenario',
},
},
};
export function browseScenario() { /* ... */ }
export function searchScenario() { /* ... */ }
export function orderScenario() { /* ... */ }自定义指标
k6内置了http_req_duration等指标,但业务层面的指标(比如"下单耗时"、"支付成功率")需要自己定义。
四种指标类型
import { Counter, Gauge, Rate, Trend } from 'k6/metrics';
// Counter:只增不减的计数器
const ordersCreated = new Counter('orders_created_total');
const paymentsFailed = new Counter('payments_failed_total');
// Gauge:当前值(可增可减)
const activeOrders = new Gauge('active_orders');
// Rate:比率(值必须是boolean)
const loginSuccessRate = new Rate('login_success_rate');
// Trend:统计分布(P50/P95/P99等),单位可选
const orderProcessTime = new Trend('order_process_ms', true); // true表示单位是ms
export default function() {
// 使用自定义指标
const startTime = Date.now();
const loginRes = http.post('/api/login', body, params);
loginSuccessRate.add(loginRes.status === 200); // 传boolean
if (loginRes.status === 200) {
const orderRes = http.post('/api/order', orderBody, orderParams);
if (orderRes.status === 200) {
ordersCreated.add(1); // 计数加1
orderProcessTime.add(Date.now() - startTime); // 记录耗时
} else {
paymentsFailed.add(1);
}
}
}对自定义指标设置阈值
export const options = {
thresholds: {
'orders_created_total': ['count>1000'], // 压测期间至少创建1000个订单
'login_success_rate': ['rate>0.99'], // 登录成功率>99%
'order_process_ms': ['p(99)<2000', 'med<800'], // P99<2s,中位数<800ms
'payments_failed_total': ['count<10'], // 支付失败不超过10次
},
};实时输出到 InfluxDB + Grafana
这是k6实战中最有价值的部分——把压测数据实时可视化。
启动 InfluxDB + Grafana
# docker-compose.yml
version: '3'
services:
influxdb:
image: influxdb:1.8
ports:
- "8086:8086"
environment:
INFLUXDB_DB: k6
INFLUXDB_HTTP_AUTH_ENABLED: false
volumes:
- influxdb_data:/var/lib/influxdb
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
GF_AUTH_ANONYMOUS_ENABLED: true
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
volumes:
- grafana_data:/var/lib/grafana
volumes:
influxdb_data:
grafana_data:docker-compose up -d运行 k6 并输出到 InfluxDB
k6 run \
--out influxdb=http://localhost:8086/k6 \
--tag testrun=order_stress_20241121 \
order_flow.js--tag很重要,它给这次压测打上标签,Grafana里可以按标签过滤,对比不同次压测的结果。
配置 Grafana 数据源
- 访问
http://localhost:3000 - Configuration → Data Sources → Add data source
- 选择InfluxDB
- URL:
http://influxdb:8086 - Database:
k6 - 保存测试
导入 k6 官方 Dashboard
k6官方提供了现成的Grafana Dashboard,ID是2587:
Dashboards → Import → Dashboard ID: 2587 → Load → 选择InfluxDB数据源 → Import
导入后就有完整的压测监控大盘,包括:
- VU数量随时间变化
- RPS趋势
- P50/P90/P95/P99响应时间曲线
- 错误率
- 网络吞吐量
高级特性:Lifecycle Hooks
k6提供了四个生命周期函数,用于压测前后的准备和清理:
// setup:在压测开始前执行一次,返回值传给所有VU和teardown
export function setup() {
console.log('压测开始前准备...');
// 比如:先调接口获取全局配置
const res = http.get('https://api.example.com/config');
const config = res.json();
// 比如:预创建一批测试账号
const tokens = [];
for (let i = 0; i < 100; i++) {
const loginRes = http.post('/api/login', JSON.stringify({
username: `bench_user_${i}`,
password: 'bench_pass'
}), { headers: { 'Content-Type': 'application/json' } });
tokens.push(loginRes.json('data.token'));
}
return { config, tokens }; // 这个对象会传给default函数的data参数
}
// default:每个VU每次迭代执行
export default function(data) {
// data就是setup()的返回值
const token = data.tokens[__VU % data.tokens.length];
const config = data.config;
// 使用预创建的token发请求
http.get(`${config.apiBaseUrl}/api/products`, {
headers: { 'Authorization': `Bearer ${token}` }
});
}
// handleSummary:压测结束后处理汇总数据
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'/tmp/summary.json': JSON.stringify(data), // 输出JSON格式
};
}
// teardown:压测结束后执行一次,清理资源
export function teardown(data) {
console.log('压测结束,清理测试数据...');
// 比如:删除压测期间创建的测试订单
http.delete('/api/test/cleanup', JSON.stringify({
tag: 'load_test',
}), { headers: { 'Content-Type': 'application/json' } });
}踩坑实录
坑1:constant-arrival-rate下VU数量暴增
现象: 设置rate: 100(100 RPS),preAllocatedVUs: 200,压测跑了5分钟后,k6报警"VU数量不够",自动把VU增加到maxVUs: 500,但TPS还是上不去100。
原因: 被测服务响应变慢(P99从200ms涨到3000ms),每个VU需要等3秒才能发下一个请求。要维持100 RPS,需要300个VU(100 RPS × 3秒 = 300并发)。
解法: preAllocatedVUs要设置得足够大:preAllocatedVUs = 目标RPS × 预期最坏响应时间(秒) × 1.5。如果被测服务P99是1秒,100 RPS需要预分配150个VU。
坑2:InfluxDB数据丢失导致Grafana大盘不完整
现象: Grafana大盘里有几段时间的数据是空的,但压测日志里显示一直在跑。
原因: k6默认批量写InfluxDB,批量间隔是1秒。但如果InfluxDB负载高或者网络抖动,写入会失败且没有重试,导致数据丢失。
解法: 压测机和InfluxDB部署在同一台机器或同一个内网里,减少网络延迟。同时配置:
K6_INFLUXDB_PUSH_INTERVAL=2s # 增加写入间隔,减少写入频率
K6_INFLUXDB_CONCURRENT_WRITES=4 # 增加并发写入坑3:handleSummary里的自定义逻辑出错导致无报告
现象: 压测正常结束,但没有生成summary.json,也没有终端输出。
原因: handleSummary里的JavaScript代码有异常(比如访问了undefined属性),k6捕获了异常但不打印详细错误信息。
解法: 在handleSummary里加try-catch:
export function handleSummary(data) {
try {
const report = generateReport(data); // 你的自定义逻辑
return {
'/tmp/summary.json': JSON.stringify(report),
};
} catch (e) {
console.error('handleSummary error:', e.message);
return { 'stdout': JSON.stringify(data) }; // fallback
}
}总结
k6进阶的核心:Scenarios提供灵活的负载模型,自定义指标捕获业务层面的数据,InfluxDB+Grafana实现实时可视化监控。
实时监控大盘是压测工程化的重要一步——不是压完才看报告,而是压测过程中就能看到问题,及时干预。
下一篇我们离开工具层面,聊性能测试场景设计:基准测试、负载测试、压力测试、稳定性测试,每种怎么设计、测什么、看什么指标。
