k6 实战——现代化性能测试工具的完整上手指南(JavaScript DSL)
k6 实战——现代化性能测试工具的完整上手指南(JavaScript DSL)
适读人群:前端/全栈工程师、DevOps工程师、想用JavaScript写压测脚本的工程师 | 阅读时长:约14分钟 | 核心价值:从零上手k6,掌握JavaScript DSL写压测脚本,理解k6的核心设计理念
遇见k6的那个下午
2022年我们开始把基础设施往Kubernetes迁移,测试工程师小李提出要重新选一个压测工具。她的需求很具体:
"JMeter的XML我看不懂也改不了,Gatling要学Scala我不会,我想要一个能用JavaScript写压测脚本的工具,最好能在CI里很方便地跑。"
我推荐了k6。
那个下午,小李装好k6,对照文档写出第一个脚本,成功压测了登录接口,全程不到两小时。
k6是Grafana Labs(就是做监控大盘的那家公司)开源的现代化压测工具,核心设计理念是:
- 开发者友好:用JavaScript写脚本,前端/后端都懂
- CLI First:命令行运行,天然CI友好
- 轻量高效:Go语言实现,单机并发性能接近Gatling
- 可观测性:原生集成Grafana,实时监控零配置
安装 k6
# macOS
brew install k6
# Linux (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
winget install k6 --source winget
# Docker
docker pull grafana/k6
docker run --rm -i grafana/k6 run - <script.js验证安装:
k6 version
# k6 v0.54.0 (go1.22.9, linux/amd64)第一个 k6 脚本
// hello_k6.js
import http from 'k6/http';
import { check, sleep } from 'k6';
// 压测配置
export const options = {
vus: 10, // 10个虚拟用户
duration: '30s', // 运行30秒
};
// 默认函数:每个虚拟用户循环执行
export default function () {
const res = http.get('https://test-api.example.com/api/products/list');
check(res, {
'状态码是200': (r) => r.status === 200,
'响应时间<500ms': (r) => r.timings.duration < 500,
'返回了数据': (r) => JSON.parse(r.body).data.length > 0,
});
sleep(1); // 等待1秒,模拟用户思考时间
}运行:
k6 run hello_k6.js输出:
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: hello_k6.js
output: -
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration:
* default: 10 looping VUs for 30s (gracefulStop: 30s)
✓ 状态码是200
✓ 响应时间<500ms
✓ 返回了数据
checks.........................: 100.00% ✓ 2400 ✗ 0
data_received..................: 8.2 MB 273 kB/s
data_sent......................: 312 kB 10 kB/s
http_req_blocked...............: avg=1.2ms min=1µs med=3µs max=245ms p(90)=5µs p(95)=8µs
http_req_connecting............: avg=389µs min=0s med=0s max=85ms p(90)=0s p(95)=0s
http_req_duration..............: avg=142ms min=89ms med=135ms max=892ms p(90)=198ms p(95)=245ms
{ expected_response:true }...: avg=142ms min=89ms med=135ms max=892ms p(90)=198ms p(95)=245ms
http_req_failed................: 0.00% ✓ 0 ✗ 2400
http_req_receiving.............: avg=4.2ms min=125µs med=2.8ms max=124ms p(90)=8.9ms p(95)=12ms
http_req_sending...............: avg=24µs min=6µs med=20µs max=1.2ms p(90)=40µs p(95)=51µs
http_req_tls_handshaking.......: avg=812µs min=0s med=0s max=178ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=138ms min=87ms med=131ms max=869ms p(90)=193ms p(95)=239ms
http_reqs......................: 2400 79.99/s
iteration_duration.............: avg=1.14s min=1.09s med=1.14s max=2.1s p(90)=1.2s p(95)=1.25s
iterations.....................: 2400 79.99/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10http_req_duration里的p(90)和p(95)就是P90和P95响应时间。
核心 API 完整介绍
HTTP 请求
import http from 'k6/http';
// GET请求
const res = http.get('https://api.example.com/users/123');
// POST请求(JSON)
const res = http.post(
'https://api.example.com/order',
JSON.stringify({ userId: 123, productId: 456 }),
{ headers: { 'Content-Type': 'application/json' } }
);
// 带认证的请求
const params = {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'X-Request-ID': `req_${Date.now()}_${__VU}_${__ITER}`,
},
timeout: '10s', // 请求超时时间
};
const res = http.post('https://api.example.com/order', body, params);
// 批量并发请求(同一用户并发多个请求)
const responses = http.batch([
['GET', 'https://api.example.com/product/1'],
['GET', 'https://api.example.com/product/2'],
['GET', 'https://api.example.com/product/3'],
]);内置变量
__VU // 当前虚拟用户编号(1-based)
__ITER // 当前迭代次数(0-based)
__ENV // 环境变量,通过 k6 run --env KEY=VALUE 注入check 与 fail
import { check, fail } from 'k6';
const res = http.post('/api/order', body, params);
// check:记录通过/失败,但不中断脚本
const passed = check(res, {
'状态码200': (r) => r.status === 200,
'code字段为200': (r) => r.json('code') === 200,
'orderId不为空': (r) => r.json('data.orderId') !== null,
});
// 如果关键断言失败,中断当前迭代
if (!passed) {
console.error(`下单失败: ${res.body}`);
fail('下单接口断言失败');
}从文件读取测试数据
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
// SharedArray:所有VU共享同一份数据,不重复加载
const users = new SharedArray('users', function() {
const csv = open('/data/users.csv');
return papaparse.parse(csv, { header: true }).data;
});
export default function() {
// 随机选一个用户
const user = users[Math.floor(Math.random() * users.length)];
const res = http.post('/api/auth/login', JSON.stringify({
username: user.username,
password: user.password,
}), { headers: { 'Content-Type': 'application/json' } });
check(res, { 'login success': (r) => r.status === 200 });
}完整压测场景:电商下单链路
// order_flow.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { SharedArray } from 'k6/data';
import { Counter, Rate, Trend } from 'k6/metrics';
// 自定义指标
const orderCreated = new Counter('orders_created');
const orderFailed = new Rate('order_fail_rate');
const orderDuration = new Trend('order_create_duration', true); // true = ms单位
// 测试数据
const users = new SharedArray('users', function() {
return JSON.parse(open('/data/users.json'));
});
const products = new SharedArray('products', function() {
return JSON.parse(open('/data/products.json'));
});
// 压测配置
export const options = {
scenarios: {
// 阶梯加压场景
stress_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 50 }, // 1分钟内爬升到50
{ duration: '3m', target: 50 }, // 保持50,持续3分钟
{ duration: '1m', target: 200 }, // 继续加压到200
{ duration: '5m', target: 200 }, // 保持200,持续5分钟
{ duration: '1m', target: 0 }, // 1分钟内降到0
],
},
},
thresholds: {
'http_req_duration': ['p(99)<500'], // P99 < 500ms
'http_req_failed': ['rate<0.01'], // 失败率 < 1%
'order_fail_rate': ['rate<0.005'], // 下单失败率 < 0.5%
'order_create_duration': ['p(99)<1000'], // 下单P99 < 1000ms
},
};
const BASE_URL = __ENV.BASE_URL || 'https://test-api.example.com';
export default function() {
const user = users[Math.floor(Math.random() * users.length)];
const product = products[Math.floor(Math.random() * products.length)];
let token = '';
let userId = '';
// ============ 登录 ============
group('登录', function() {
const res = http.post(
`${BASE_URL}/api/auth/login`,
JSON.stringify({ username: user.username, password: user.password }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(res, {
'登录成功': (r) => r.status === 200 && r.json('code') === 200,
});
if (res.status === 200) {
token = res.json('data.accessToken');
userId = res.json('data.userId');
}
});
if (!token) {
console.error('登录失败,跳过后续步骤');
orderFailed.add(1);
return;
}
sleep(Math.random() * 2 + 1); // 1-3秒思考时间
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
// ============ 查询商品 ============
group('查询商品', function() {
const res = http.get(
`${BASE_URL}/api/product/${product.id}`,
{ headers }
);
check(res, {
'商品查询成功': (r) => r.status === 200,
'商品有库存': (r) => r.json('data.stock') > 0,
});
});
sleep(Math.random() * 3 + 2); // 2-5秒浏览时间
// ============ 创建订单 ============
group('创建订单', function() {
const startTime = Date.now();
const res = http.post(
`${BASE_URL}/api/order/create`,
JSON.stringify({
userId: userId,
productId: product.id,
quantity: Math.floor(Math.random() * 3) + 1,
addressId: user.addressId,
}),
{ headers }
);
const duration = Date.now() - startTime;
orderDuration.add(duration);
const success = check(res, {
'下单成功': (r) => r.status === 200 && r.json('code') === 200,
'有订单ID': (r) => r.json('data.orderId') !== undefined,
});
if (success) {
orderCreated.add(1);
} else {
orderFailed.add(1);
console.error(`下单失败 [VU:${__VU}]: status=${res.status}, body=${res.body}`);
}
});
sleep(1);
}运行与输出
# 基本运行
k6 run order_flow.js
# 传入环境变量
k6 run --env BASE_URL=https://test-api.example.com order_flow.js
# 导出结果为JSON
k6 run --out json=/tmp/result.json order_flow.js
# 实时输出到InfluxDB
k6 run --out influxdb=http://localhost:8086/k6 order_flow.js
# 实时输出到Grafana Cloud
k6 run --out cloud order_flow.js踩坑实录
坑1:k6的JavaScript不是完整的Node.js
现象: 想用axios发请求,安装后报错。想用fs.readFileSync()读文件,报错fs is not defined。
原因: k6的JavaScript运行时是基于Goja(一个Go实现的JS引擎),不是V8,也不是Node.js。它只支持ES6+ JavaScript语法,不支持Node.js的内置模块(fs、path等)和npm包(除非被专门移植)。
解法: 读文件用k6内置的open()函数;HTTP请求用k6内置的k6/http。k6提供了jslib(https://jslib.k6.io/),里面有一些常用工具库的移植版,比如papaparse、uuid等。
坑2:SharedArray的数据加载在某些版本里很慢
现象: 压测开始前有一段30秒的"准备时间",100万行CSV加载非常慢,CI里等得很久。
原因: SharedArray在初始化时把整个文件读入内存,大文件会比较慢。
解法: 控制测试数据规模。如果数据文件超过100MB,先截取其中一部分用于压测。另外可以预处理成JSON格式,比CSV解析更快。
坑3:thresholds阈值检查的时机问题
现象: 配置了'http_req_duration': ['p(99)<500'],但压测开始后P99一直超过500ms,脚本没有报错,继续跑了整个duration。
原因: k6的thresholds默认在压测结束后才判断。如果想在运行过程中就终止,需要配置abortOnFail: true:
thresholds: {
'http_req_duration': [{ threshold: 'p(99)<500', abortOnFail: true }],
},解法: 明确需要"超阈值就停止"的场景时,加上abortOnFail: true。CI场景里用这个选项可以在发现问题时立刻停止,不浪费时间。
总结
k6的核心优势:JavaScript脚本对开发者友好,CLI First天然适合CI,Go语言实现的高性能,原生Grafana集成。
主要限制:不是完整Node.js,npm包不能直接用,需要适应k6的模块体系。
如果你的团队有JavaScript背景或者在做云原生架构,k6是很好的选择。下一篇进入k6进阶,场景模拟、自定义指标、Grafana可视化全讲。
