k6 面试高频问题全解析
本文档全面覆盖 k6 性能测试相关的面试问题,从基础概念到架构设计,从实战应用到性能优化,帮助你深入理解 k6 并在面试中脱颖而出。
基础概念与特性
什么是 k6?它解决了什么问题?
核心回答:
k6 是一款现代化的开源负载测试工具,由 Grafana Labs 开发并维护。它主要解决了传统性能测试工具的几大痛点:
-
开发者体验差:传统工具(如 JMeter)需要使用 GUI 操作或编写 XML 配置,k6 使用现代 JavaScript 编写脚本,符合开发者习惯
-
资源消耗高:JMeter 单机只能支持几百个并发线程,k6 单机可支持 10,000+ 虚拟用户,内存占用仅为传统工具的 1/3
-
CI/CD 集成困难:传统工具不是为自动化设计的,k6 天生支持命令行,可轻松集成到 CI/CD 流程
-
云原生支持不足:k6 支持容器化部署、Kubernetes 集成,非常适合微服务和云原生应用测试
架构特点:
JavaScript 测试脚本 Goja JS Engine
JavaScript 运行时 Go Runtime
核心执行引擎 goroutine Pool
高并发支持 HTTP Client
网络请求 Metrics Engine
指标收集
深度解析:
k6 采用双层架构设计:
- 用户层:使用 JavaScript(ES6+)编写测试脚本,提供良好的开发体验
- 执行层:使用 Go 语言实现核心引擎,保证高性能和低资源消耗
这种设计既保证了易用性,又不牺牲性能。
k6 的核心架构是怎样的?
技术架构:
实时上报 Init Context
初始化阶段 VU Context
虚拟用户上下文 Default Function
主测试逻辑 Teardown
清理阶段 Metrics Collector
指标收集器 Output
数据输出
生命周期说明:
-
Init 阶段(执行一次):
- 加载脚本和依赖模块
- 读取外部文件(CSV、JSON)
- 定义全局变量和配置
- 这个阶段的代码在所有 VU 之间共享
-
VU 阶段(每个 VU 独立执行):
- 创建 VU 实例
- 循环执行
default函数 - 每个 VU 有独立的执行上下文
-
Teardown 阶段(执行一次):
- 清理资源
- 生成测试报告
关键设计:
javascript
// Init 阶段(全局,所有 VU 共享)
import http from 'k6/http';
import { check } from 'k6';
// 这里的数据在所有 VU 之间共享,只加载一次
const testData = JSON.parse(open('./data.json'));
export const options = {
vus: 100,
duration: '10m',
};
// VU 阶段(每个 VU 独立执行)
export default function () {
// 这个函数会被每个 VU 循环执行
const response = http.get('https://api.example.com/users');
check(response, {
'status is 200': (r) => r.status === 200,
});
}
// Teardown 阶段(可选)
export function teardown(data) {
// 清理工作
}
VU(虚拟用户)的实现原理是什么?
深度解析:
VU 是 k6 中的并发执行单元,每个 VU 实际上是一个 Go goroutine。
实现机制:
k6 进程 Go Runtime goroutine 1
VU 1 goroutine 2
VU 2 goroutine 3
VU 3 goroutine N
VU N Goja VM Instance Goja VM Instance Goja VM Instance Goja VM Instance
技术细节:
-
Goroutine vs 传统线程:
- Java 线程:1MB+ 栈空间
- Go goroutine:2KB 初始栈空间
- 因此 k6 可以轻松创建 10,000+ 并发
-
M:N 调度模型:
- M 个 goroutine 映射到 N 个 OS 线程
- Go 运行时自动调度
- 当 goroutine 阻塞时(如等待 I/O),调度器会切换到其他 goroutine
-
内存占用对比:
| 工具 | 单个并发单元 | 内存占用 | 10,000 并发内存 |
|---|---|---|---|
| JMeter | Java 线程 | ~1-2MB | ~10-20GB |
| k6 | goroutine | ~0.5-1MB | ~5-10GB |
为什么 k6 性能高:
javascript
// 在 k6 中,这样的代码不会阻塞其他 VU
export default function () {
// 发起 HTTP 请求时,当前 goroutine 会让出 CPU
const response = http.get('https://api.example.com/users');
// 在等待响应期间,Go 调度器会运行其他 goroutine
// 响应返回后,当前 goroutine 继续执行
check(response, {
'status is 200': (r) => r.status === 200,
});
// 即使 sleep,也不会阻塞整个进程
sleep(1);
}
k6 的指标系统是如何设计的?
指标类型:
k6 提供四种核心指标类型,每种都有特定的应用场景:
-
Counter(计数器):
- 只增不减的累计值
- 适用场景:请求总数、错误总数
- 统计值:count、rate(每秒增量)
-
Rate(比率):
- 0-1 之间的百分比
- 适用场景:成功率、错误率
- 统计值:rate(平均比率)、count(符合条件的次数)
-
Trend(趋势):
- 数值序列,支持统计分析
- 适用场景:响应时间、数据大小
- 统计值:min、max、avg、med、p90、p95、p99
-
Gauge(仪表):
- 当前值,可增可减
- 适用场景:活跃 VU 数、并发连接数
- 统计值:value(最后一个值)、min、max
内置指标:
javascript
// HTTP 相关
http_reqs // Counter - 总请求数
http_req_duration // Trend - 请求耗时
http_req_blocked // Trend - 连接等待时间
http_req_connecting // Trend - TCP 连接建立时间
http_req_tls_handshaking // Trend - TLS 握手时间
http_req_sending // Trend - 发送请求时间
http_req_waiting // Trend - 等待响应时间(TTFB)
http_req_receiving // Trend - 接收响应时间
http_req_failed // Rate - 失败率
// VU 相关
vus // Gauge - 当前活跃 VU 数
vus_max // Gauge - VU 数峰值
// 迭代相关
iterations // Counter - 迭代总次数
iteration_duration // Trend - 单次迭代耗时
// 数据相关
data_sent // Counter - 发送数据量
data_received // Counter - 接收数据量
自定义指标实践:
javascript
import { Counter, Rate, Trend, Gauge } from 'k6/metrics';
// 业务指标定义
const loginAttempts = new Counter('login_attempts');
const loginSuccessRate = new Rate('login_success_rate');
const orderProcessingTime = new Trend('order_processing_time');
const activeCartItems = new Gauge('active_cart_items');
export default function () {
// 登录测试
loginAttempts.add(1);
const loginRes = http.post('https://api.example.com/login', {
username: 'user',
password: 'pass',
});
loginSuccessRate.add(loginRes.status === 200);
// 下单测试
const startTime = Date.now();
const orderRes = http.post('https://api.example.com/orders', orderData);
const duration = Date.now() - startTime;
orderProcessingTime.add(duration);
// 购物车商品数
const cartRes = http.get('https://api.example.com/cart');
const itemCount = JSON.parse(cartRes.body).items.length;
activeCartItems.add(itemCount);
}
指标聚合策略:
k6 在内存中实时计算统计值,而不是存储所有原始数据点,这是其高性能的关键:
javascript
// 对于 Trend 指标,k6 使用 reservoir sampling
// 只保存有限数量的样本(默认 1000 个)
// 但仍能准确计算分位数
export const options = {
// 自定义统计值
summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'p(99.99)'],
// 阈值定义
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
login_success_rate: ['rate>0.99'],
},
};
工具对比与选型
k6 与 JMeter 的深度对比
架构对比:
| 维度 | k6 | JMeter |
|---|---|---|
| 核心语言 | Go | Java |
| 脚本语言 | JavaScript | XML + Groovy/Beanshell |
| 并发模型 | goroutine(协程) | 线程 |
| 单机并发 | 10,000+ VU | 500-1,000 线程 |
| 内存占用 | ~80MB/100VU | ~350MB/100线程 |
| CPU 占用 | 低(非阻塞 I/O) | 高(阻塞 I/O) |
| 启动时间 | <1 秒 | 5-15 秒 |
| GUI | 无(CLI only) | 有(功能强大) |
| 学习曲线 | 平缓(熟悉 JS 即可) | 陡峭(需要学习 GUI) |
| CI/CD 集成 | 优秀(天生支持) | 良好(需要配置) |
| 分布式测试 | k6 Cloud | 原生支持 |
| 协议支持 | HTTP/WebSocket/gRPC | HTTP/FTP/LDAP/SMTP等 |
| 报告 | 简洁(需配合 Grafana) | 详细美观 |
| 扩展性 | JavaScript 生态 | 丰富的插件 |
性能实测对比:
测试场景:10,000 个并发用户,持续 10 分钟
k6:
- 内存占用:~8GB
- CPU 使用:60-70%
- 成功率:99.9%
- 测试机器:16 核 32GB
JMeter:
- 内存占用:~28GB(需要多台机器分布式)
- CPU 使用:90%+(单机无法支持)
- 成功率:95%(资源不足导致)
- 测试机器:16 核 32GB × 3
选型建议:
选择 k6 的场景:
- ✅ API 和微服务性能测试
- ✅ 持续集成/持续部署(CI/CD)
- ✅ 开发者驱动的性能测试
- ✅ 云原生和容器化环境
- ✅ 需要高并发低成本
- ✅ 团队熟悉 JavaScript
选择 JMeter 的场景:
- ✅ 需要 GUI 工具(测试团队非开发背景)
- ✅ 复杂的业务流程测试
- ✅ 需要测试多种协议(FTP、LDAP、SMTP)
- ✅ 需要详细的图形化报告
- ✅ 已有大量 JMeter 脚本资产
- ✅ 需要原生分布式测试
组合使用策略:
开发阶段:k6
- 快速迭代
- 本地验证
- PR 检查
测试阶段:k6 + JMeter
- k6 做日常回归测试
- JMeter 做复杂场景和详细分析
生产环境:k6
- 持续监控
- 容量验证
k6 与 Gatling 的对比
| 维度 | k6 | Gatling |
|---|---|---|
| 核心语言 | Go | Scala |
| 脚本语言 | JavaScript | Scala DSL |
| 学习曲线 | 平缓(JavaScript 通用) | 中等(需要学习 Scala) |
| 性能 | 优秀 | 优秀 |
| 并发模型 | goroutine | Akka Actor |
| 报告质量 | 简洁 | 详细美观(HTML) |
| 实时监控 | 需要配置 | 内置 |
| 分布式 | k6 Cloud | Enterprise 版本 |
| 社区 | 活跃 | 活跃 |
| 企业支持 | Grafana Labs | Gatling Corp |
选型建议:
- 团队熟悉 JavaScript → k6
- 团队熟悉 Scala/函数式编程 → Gatling
- 需要详细的内置报告 → Gatling
- 追求简洁和 CI/CD 集成 → k6
什么时候不应该使用 k6?
不适合的场景:
-
需要测试非 HTTP 协议:
- FTP、SMTP、LDAP 等传统协议
- 专有协议(除非自己扩展)
- 建议:使用 JMeter 或协议专用工具
-
需要 GUI 工具:
- 测试团队不熟悉编程
- 需要可视化操作界面
- 建议:使用 JMeter 或 LoadRunner
-
需要录制功能:
- 自动生成测试脚本
- 建议:使用 JMeter 的录制功能,然后转换为 k6
-
浏览器性能测试:
- 前端页面渲染性能
- JavaScript 执行性能
- 建议:使用 Lighthouse 或 WebPageTest
-
需要原生分布式测试:
- 不想依赖云服务
- 需要自建分布式集群
- 建议:使用 Gatling 或 JMeter
性能测试设计
如何设计负载测试场景?
测试类型与配置:
- 负载测试(Load Testing):
javascript
// 目标:验证系统在预期负载下的表现
export const options = {
// 固定负载
vus: 100,
duration: '30m',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
- 压力测试(Stress Testing):
javascript
// 目标:找到系统的性能极限
export const options = {
stages: [
{ duration: '5m', target: 100 }, // 预热
{ duration: '10m', target: 200 }, // 第一阶段
{ duration: '10m', target: 500 }, // 第二阶段
{ duration: '10m', target: 1000 }, // 压力阶段
{ duration: '10m', target: 2000 }, // 超压阶段
{ duration: '5m', target: 0 }, // 恢复
],
};
- 峰值测试(Spike Testing):
javascript
// 目标:测试系统应对突发流量的能力
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 正常负载
{ duration: '30s', target: 5000 }, // 突然爆发
{ duration: '2m', target: 5000 }, // 维持高峰
{ duration: '30s', target: 100 }, // 快速恢复
{ duration: '3m', target: 100 }, // 稳定观察
{ duration: '30s', target: 0 },
],
};
- 浸泡测试(Soak Testing):
javascript
// 目标:发现内存泄漏、资源耗尽等长期运行问题
export const options = {
vus: 200,
duration: '4h', // 长时间运行
thresholds: {
http_req_duration: ['p(95)<500'],
// 关注趋势,而不是绝对值
http_req_failed: ['rate<0.01'],
},
};
- 容量测试(Capacity Testing):
javascript
// 目标:确定系统的最大容量
export const options = {
scenarios: {
find_capacity: {
executor: 'ramping-arrival-rate',
startRate: 50,
timeUnit: '1s',
preAllocatedVUs: 500,
maxVUs: 10000,
stages: [
{ duration: '10m', target: 100 },
{ duration: '10m', target: 200 },
{ duration: '10m', target: 500 },
{ duration: '10m', target: 1000 },
],
},
},
};
场景设计模式:
javascript
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// 定义业务场景权重
const scenarios = {
browse: 0.6, // 60% 用户浏览
search: 0.25, // 25% 用户搜索
purchase: 0.15, // 15% 用户购买
};
const errorRate = new Rate('scenario_errors');
export const options = {
scenarios: {
// 浏览场景
browse_scenario: {
executor: 'constant-vus',
vus: 60,
duration: '10m',
exec: 'browseProducts',
tags: { scenario: 'browse' },
},
// 搜索场景
search_scenario: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 25 },
{ duration: '8m', target: 25 },
],
exec: 'searchProducts',
tags: { scenario: 'search' },
},
// 购买场景(最重要,单独监控)
purchase_scenario: {
executor: 'constant-arrival-rate',
rate: 15,
timeUnit: '1m',
duration: '10m',
preAllocatedVUs: 20,
exec: 'purchaseFlow',
tags: { scenario: 'purchase' },
},
},
thresholds: {
'http_req_duration{scenario:purchase}': ['p(95)<1000'],
'http_req_failed{scenario:purchase}': ['rate<0.001'],
'scenario_errors': ['rate<0.05'],
},
};
// 浏览商品场景
export function browseProducts() {
const res = http.get('https://api.example.com/products');
check(res, {
'browse: status 200': (r) => r.status === 200,
}) || errorRate.add(1);
sleep(Math.random() * 5 + 3); // 3-8秒思考时间
}
// 搜索商品场景
export function searchProducts() {
const keywords = ['phone', 'laptop', 'camera'];
const keyword = keywords[Math.floor(Math.random() * keywords.length)];
const res = http.get(`https://api.example.com/search?q=${keyword}`);
check(res, {
'search: status 200': (r) => r.status === 200,
'search: has results': (r) => JSON.parse(r.body).results.length > 0,
}) || errorRate.add(1);
sleep(Math.random() * 3 + 2); // 2-5秒
}
// 购买流程场景(完整业务流程)
export function purchaseFlow() {
// 1. 加入购物车
let res = http.post('https://api.example.com/cart', {
productId: 12345,
quantity: 1,
});
if (!check(res, { 'add to cart: 200': (r) => r.status === 200 })) {
errorRate.add(1);
return;
}
sleep(1);
// 2. 结算
res = http.post('https://api.example.com/checkout', {
paymentMethod: 'credit_card',
});
check(res, {
'checkout: status 200': (r) => r.status === 200,
'checkout: order created': (r) => JSON.parse(r.body).orderId !== undefined,
}) || errorRate.add(1);
sleep(2);
// 3. 支付
const orderId = JSON.parse(res.body).orderId;
res = http.post(`https://api.example.com/orders/${orderId}/pay`, {
cardNumber: '4111111111111111',
});
check(res, {
'payment: status 200': (r) => r.status === 200,
'payment: success': (r) => JSON.parse(r.body).status === 'paid',
}) || errorRate.add(1);
}
如何确定合理的性能指标阈值?
制定阈值的方法:
- 基于业务目标:
javascript
export const options = {
thresholds: {
// SLA 要求:95% 的请求在 500ms 内完成
http_req_duration: ['p(95)<500'],
// SLA 要求:99.9% 的可用性
http_req_failed: ['rate<0.001'],
// 吞吐量要求:至少 100 QPS
http_reqs: ['rate>100'],
},
};
- 基于历史数据:
javascript
// 分析历史测试数据,设置合理的回归阈值
export const options = {
thresholds: {
// 不能比历史均值慢 20%
http_req_duration: ['avg<600'], // 历史均值 500ms
// P99 不能超过历史 P99 的 10%
'http_req_duration{endpoint:/api/users}': ['p(99)<1100'], // 历史 P99: 1000ms
},
};
- 基于用户体验:
javascript
// 参考 Google 的研究:
// - 100ms:感觉即时
// - 300ms:感觉稍有延迟
// - 1000ms:用户开始失去专注
// - 10000ms:用户会放弃操作
export const options = {
thresholds: {
// 搜索要快(即时反馈)
'http_req_duration{endpoint:/api/search}': ['p(95)<200'],
// 列表页可以稍慢
'http_req_duration{endpoint:/api/products}': ['p(95)<500'],
// 复杂操作可以更慢
'http_req_duration{endpoint:/api/reports}': ['p(95)<3000'],
},
};
- 逐步收紧策略:
javascript
// 第一阶段:建立基线
// 不设置严格阈值,只观察
// 第二阶段:设置宽松阈值
export const options = {
thresholds: {
http_req_duration: ['p(95)<2000'],
http_req_failed: ['rate<0.05'],
},
};
// 第三阶段:优化后收紧
export const options = {
thresholds: {
http_req_duration: ['p(95)<1000'],
http_req_failed: ['rate<0.01'],
},
};
// 第四阶段:达到目标
export const options = {
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.001'],
},
};
如何模拟真实用户行为?
思考时间(Think Time):
javascript
import { sleep } from 'k6';
export default function () {
// 浏览首页
http.get('https://example.com/');
sleep(Math.random() * 5 + 3); // 3-8秒思考时间
// 点击商品
http.get('https://example.com/product/123');
sleep(Math.random() * 10 + 5); // 5-15秒阅读商品详情
// 加入购物车
http.post('https://example.com/cart/add', { productId: 123 });
sleep(2); // 2秒等待反馈
}
用户行为模拟:
javascript
import { randomSeed } from 'k6';
// 设置随机种子,保证可重现性
randomSeed(12345);
// 不同用户类型
const userTypes = {
browser: 0.7, // 70% 是浏览者
searcher: 0.2, // 20% 是搜索者
buyer: 0.1, // 10% 是购买者
};
export default function () {
const userType = getUserType();
switch (userType) {
case 'browser':
browseBehavior();
break;
case 'searcher':
searchBehavior();
break;
case 'buyer':
buyBehavior();
break;
}
}
function getUserType() {
const rand = Math.random();
if (rand < 0.7) return 'browser';
if (rand < 0.9) return 'searcher';
return 'buyer';
}
function browseBehavior() {
// 浏览者:随机浏览 3-5 个页面
const pages = Math.floor(Math.random() * 3) + 3;
for (let i = 0; i < pages; i++) {
http.get(`https://example.com/products?page=${i}`);
sleep(Math.random() * 8 + 2); // 2-10秒
}
}
function searchBehavior() {
// 搜索者:搜索 1-3 次
const searches = Math.floor(Math.random() * 3) + 1;
const keywords = ['phone', 'laptop', 'camera', 'headphone'];
for (let i = 0; i < searches; i++) {
const keyword = keywords[Math.floor(Math.random() * keywords.length)];
http.get(`https://example.com/search?q=${keyword}`);
sleep(Math.random() * 5 + 2);
}
}
function buyBehavior() {
// 购买者:完整购买流程
http.get('https://example.com/products');
sleep(3);
http.get('https://example.com/product/123');
sleep(8);
http.post('https://example.com/cart/add', { productId: 123 });
sleep(2);
http.post('https://example.com/checkout');
sleep(5);
http.post('https://example.com/payment', { method: 'credit_card' });
}
脚本开发与优化
如何优化 k6 脚本性能?
1. 使用 SharedArray 共享数据:
javascript
import { SharedArray } from 'k6/data';
// ❌ 错误:每个 VU 都会加载一份数据
const users = JSON.parse(open('./users.json')); // 1MB × 1000 VU = 1GB
// ✅ 正确:所有 VU 共享同一份数据
const users = new SharedArray('users', function () {
return JSON.parse(open('./users.json')); // 只占用 1MB
});
export default function () {
const user = users[__VU % users.length];
// 使用 user 数据
}
2. 使用批量请求(Batch Requests):
javascript
import http from 'k6/http';
// ❌ 效率低:串行发送请求
export default function () {
http.get('https://api.example.com/users');
http.get('https://api.example.com/products');
http.get('https://api.example.com/orders');
// 总耗时:300ms + 200ms + 250ms = 750ms
}
// ✅ 高效:并行发送请求
export default function () {
const responses = http.batch([
['GET', 'https://api.example.com/users'],
['GET', 'https://api.example.com/products'],
['GET', 'https://api.example.com/orders'],
]);
// 总耗时:max(300ms, 200ms, 250ms) = 300ms
}
3. 避免不必要的 JSON 解析:
javascript
import { check } from 'k6';
// ❌ 低效:多次解析同一个 JSON
export default function () {
const response = http.get('https://api.example.com/users');
check(response, {
'has users': (r) => JSON.parse(r.body).users !== undefined,
'user count > 0': (r) => JSON.parse(r.body).users.length > 0,
'first user has id': (r) => JSON.parse(r.body).users[0].id !== undefined,
});
}
// ✅ 高效:只解析一次
export default function () {
const response = http.get('https://api.example.com/users');
const data = JSON.parse(response.body);
check(data, {
'has users': (d) => d.users !== undefined,
'user count > 0': (d) => d.users.length > 0,
'first user has id': (d) => d.users[0].id !== undefined,
});
}
4. 使用连接复用:
javascript
export const options = {
// 启用 HTTP keep-alive
noConnectionReuse: false,
// 每个 VU 不复用连接(适用于测试连接建立性能)
// noVUConnectionReuse: true,
// 批量配置
batch: 10,
batchPerHost: 6,
};
5. 优化数据文件:
javascript
// ❌ 低效:大文件影响启动速度和内存
const largeData = JSON.parse(open('./10mb-data.json'));
// ✅ 高效:按需加载,减小文件
// 1. 压缩数据(移除不必要的字段)
// 2. 分片加载
// 3. 使用更紧凑的格式
const data = new SharedArray('data', function () {
const lines = open('./data.csv').split('\n');
return lines.map(line => {
const [id, name, email] = line.split(',');
return { id, name, email };
});
});
6. 合理设置指标统计:
javascript
export const options = {
// 只计算需要的统计值
summaryTrendStats: ['avg', 'p(95)', 'p(99)'],
// 不计算不需要的统计值(节省 CPU)
// summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'p(99.9)', 'p(99.99)'],
};
如何处理复杂的认证场景?
1. Session Cookie 认证:
javascript
import http from 'k6/http';
import { check } from 'k6';
export default function () {
// 登录获取 Session Cookie
const loginRes = http.post('https://api.example.com/login', {
username: 'user',
password: 'pass',
});
check(loginRes, {
'login successful': (r) => r.status === 200,
});
// k6 会自动保存和发送 Cookie
// 后续请求自动携带 Session Cookie
const profileRes = http.get('https://api.example.com/profile');
check(profileRes, {
'profile loaded': (r) => r.status === 200,
});
}
2. JWT Token 认证:
javascript
import http from 'k6/http';
let authToken = null;
export function setup() {
// 在 setup 阶段获取 token(只执行一次)
const loginRes = http.post('https://api.example.com/login', {
username: 'user',
password: 'pass',
});
return {
token: JSON.parse(loginRes.body).token,
};
}
export default function (data) {
// 使用 token 发送请求
const headers = {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json',
};
const response = http.get('https://api.example.com/protected', {
headers: headers,
});
}
3. OAuth2 认证:
javascript
import http from 'k6/http';
import { check } from 'k6';
// 每个 VU 独立的 token
let accessToken = null;
let tokenExpiry = 0;
export default function () {
// 检查 token 是否过期
if (!accessToken || Date.now() > tokenExpiry) {
refreshToken();
}
// 使用 token 发送请求
const response = http.get('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
// 如果 token 失效,刷新 token
if (response.status === 401) {
refreshToken();
// 重试请求
http.get('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
}
}
function refreshToken() {
const tokenRes = http.post('https://api.example.com/oauth/token', {
grant_type: 'client_credentials',
client_id: 'your_client_id',
client_secret: 'your_client_secret',
});
const data = JSON.parse(tokenRes.body);
accessToken = data.access_token;
tokenExpiry = Date.now() + (data.expires_in * 1000);
}
4. 多用户并发认证:
javascript
import { SharedArray } from 'k6/data';
import http from 'k6/http';
// 加载用户凭证
const credentials = new SharedArray('credentials', function () {
return JSON.parse(open('./users.json'));
});
export default function () {
// 每个 VU 使用不同的用户
const user = credentials[__VU % credentials.length];
// 登录
const loginRes = http.post('https://api.example.com/login', {
username: user.username,
password: user.password,
});
const token = JSON.parse(loginRes.body).token;
// 使用该用户的 token 进行测试
http.get('https://api.example.com/profile', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
}
如何进行数据关联和参数化?
动态数据提取:
javascript
import http from 'k6/http';
import { check } from 'k6';
export default function () {
// 1. 创建订单,提取订单 ID
const createOrderRes = http.post('https://api.example.com/orders', {
productId: 123,
quantity: 1,
});
const orderId = JSON.parse(createOrderRes.body).orderId;
check(createOrderRes, {
'order created': (r) => r.status === 201,
'orderId exists': () => orderId !== undefined,
});
// 2. 使用提取的订单 ID 进行支付
const paymentRes = http.post(`https://api.example.com/orders/${orderId}/pay`, {
paymentMethod: 'credit_card',
cardNumber: '4111111111111111',
});
check(paymentRes, {
'payment successful': (r) => r.status === 200,
});
// 3. 查询订单状态
const orderStatusRes = http.get(`https://api.example.com/orders/${orderId}`);
check(orderStatusRes, {
'order status is paid': (r) => JSON.parse(r.body).status === 'paid',
});
}
正则表达式提取:
javascript
import http from 'k6/http';
export default function () {
const response = http.get('https://example.com/form');
// 从 HTML 中提取 CSRF token
const csrfMatch = response.body.match(/csrf_token" value="([^"]+)"/);
const csrfToken = csrfMatch ? csrfMatch[1] : null;
// 提交表单时带上 CSRF token
http.post('https://example.com/submit', {
csrf_token: csrfToken,
data: 'test',
});
}
复杂数据关联:
javascript
import http from 'k6/http';
import { check } from 'k6';
export default function () {
// 场景:模拟完整的电商购物流程
// 1. 搜索商品
const searchRes = http.get('https://api.example.com/search?q=phone');
const products = JSON.parse(searchRes.body).products;
check(products, {
'has products': (p) => p.length > 0,
});
// 2. 选择第一个商品
const product = products[0];
const productId = product.id;
// 3. 查看商品详情
const productRes = http.get(`https://api.example.com/products/${productId}`);
const productDetails = JSON.parse(productRes.body);
// 4. 加入购物车
const cartRes = http.post('https://api.example.com/cart/items', {
productId: productId,
quantity: 1,
price: productDetails.price,
});
const cartItemId = JSON.parse(cartRes.body).itemId;
// 5. 查看购物车
const viewCartRes = http.get('https://api.example.com/cart');
const cart = JSON.parse(viewCartRes.body);
check(cart, {
'cart has items': (c) => c.items.length > 0,
'cart contains our product': (c) => c.items.some(item => item.id === cartItemId),
});
// 6. 结算
const checkoutRes = http.post('https://api.example.com/checkout', {
items: cart.items.map(item => ({
id: item.id,
quantity: item.quantity,
})),
});
const orderId = JSON.parse(checkoutRes.body).orderId;
// 7. 支付
const paymentRes = http.post(`https://api.example.com/orders/${orderId}/payment`, {
method: 'credit_card',
amount: cart.total,
});
check(paymentRes, {
'payment successful': (r) => r.status === 200,
'order completed': (r) => JSON.parse(r.body).status === 'completed',
});
}
监控与分析
如何集成监控系统?
InfluxDB + Grafana 集成:
bash
# 1. 运行测试并输出到 InfluxDB
k6 run --out influxdb=http://localhost:8086/k6 script.js
# 2. 使用环境变量配置
export K6_INFLUXDB_ORGANIZATION=my-org
export K6_INFLUXDB_BUCKET=k6
export K6_INFLUXDB_TOKEN=my-token
k6 run --out influxdb=http://localhost:8086 script.js
Prometheus Remote Write:
bash
# 配置环境变量
export K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write
export K6_PROMETHEUS_RW_PUSH_INTERVAL=1s
# 运行测试
k6 run --out experimental-prometheus-rw script.js
自定义输出格式:
javascript
import http from 'k6/http';
export function handleSummary(data) {
// 生成自定义报告
const summary = {
timestamp: new Date().toISOString(),
test_run_id: __ENV.TEST_RUN_ID,
metrics: {
http_reqs: {
count: data.metrics.http_reqs.values.count,
rate: data.metrics.http_reqs.values.rate,
},
http_req_duration: {
avg: data.metrics.http_req_duration.values.avg,
min: data.metrics.http_req_duration.values.min,
max: data.metrics.http_req_duration.values.max,
p95: data.metrics.http_req_duration.values['p(95)'],
p99: data.metrics.http_req_duration.values['p(99)'],
},
http_req_failed: {
rate: data.metrics.http_req_failed.values.rate,
count: data.metrics.http_req_failed.values.count,
},
},
};
// 发送到自定义 API
const response = http.post(
'https://metrics.example.com/api/k6-results',
JSON.stringify(summary),
{
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + __ENV.API_TOKEN,
},
}
);
// 返回多种格式的报告
return {
'stdout': JSON.stringify(summary, null, 2),
'summary.json': JSON.stringify(summary),
'summary.html': generateHTMLReport(data),
};
}
function generateHTMLReport(data) {
return `
<!DOCTYPE html>
<html>
<head><title>k6 Test Report</title></head>
<body>
<h1>Performance Test Report</h1>
<table>
<tr>
<th>Metric</th>
<th>Value</th>
</tr>
<tr>
<td>Total Requests</td>
<td>${data.metrics.http_reqs.values.count}</td>
</tr>
<tr>
<td>Avg Response Time</td>
<td>${data.metrics.http_req_duration.values.avg.toFixed(2)}ms</td>
</tr>
<tr>
<td>P95 Response Time</td>
<td>${data.metrics.http_req_duration.values['p(95)'].toFixed(2)}ms</td>
</tr>
<tr>
<td>Error Rate</td>
<td>${(data.metrics.http_req_failed.values.rate * 100).toFixed(2)}%</td>
</tr>
</table>
</body>
</html>
`;
}
如何分析性能测试结果?
1. 响应时间分析:
关注以下指标:
- avg(平均值):整体性能
- min(最小值):最佳情况
- max(最大值):最差情况(可能是异常)
- med(中位数):典型情况
- p90, p95, p99:长尾性能
分析方法:
javascript
// 在脚本中添加详细的时间分解
import http from 'k6/http';
import { Trend } from 'k6/metrics';
const dnsTime = new Trend('dns_lookup_time');
const tcpTime = new Trend('tcp_connection_time');
const tlsTime = new Trend('tls_handshake_time');
const serverTime = new Trend('server_processing_time');
const downloadTime = new Trend('content_download_time');
export default function () {
const response = http.get('https://api.example.com/users');
// 分解响应时间
dnsTime.add(response.timings.looking_up);
tcpTime.add(response.timings.connecting);
tlsTime.add(response.timings.tls_handshaking);
serverTime.add(response.timings.waiting);
downloadTime.add(response.timings.receiving);
// 总时间 = DNS + TCP + TLS + Sending + Waiting + Receiving
// 通过分析各部分时间,定位瓶颈
}
2. 错误率分析:
javascript
import http from 'k6/http';
import { Counter } from 'k6/metrics';
const errors = {
'4xx': new Counter('http_4xx_errors'),
'5xx': new Counter('http_5xx_errors'),
'timeout': new Counter('http_timeout_errors'),
'network': new Counter('http_network_errors'),
};
export default function () {
const response = http.get('https://api.example.com/users', {
timeout: '30s',
});
// 错误分类
if (response.status >= 400 && response.status < 500) {
errors['4xx'].add(1);
} else if (response.status >= 500) {
errors['5xx'].add(1);
} else if (response.error_code === 1050) {
errors['timeout'].add(1);
} else if (response.error) {
errors['network'].add(1);
}
}
3. 容量评估:
根据测试结果评估系统容量:
1. 确定性能目标(如 P95 < 500ms)
2. 逐步增加负载,观察何时突破阈值
3. 找到最大并发数(系统容量)
4. 留出安全余量(通常为 70-80%)
示例:
- 测试发现 1000 VU 时 P95 = 450ms(✓ 满足)
- 1500 VU 时 P95 = 600ms(✗ 超出)
- 因此系统容量约为 1000-1200 VU
- 建议生产环境设置为 800 VU(80% 容量)
4. 趋势分析:
javascript
// 长期监控,发现性能劣化趋势
export function handleSummary(data) {
const current = {
p95: data.metrics.http_req_duration.values['p(95)'],
errorRate: data.metrics.http_req_failed.values.rate,
};
// 与历史数据对比
const historical = {
p95: 450,
errorRate: 0.005,
};
const degradation = {
p95: ((current.p95 - historical.p95) / historical.p95 * 100).toFixed(2),
errorRate: ((current.errorRate - historical.errorRate) / historical.errorRate * 100).toFixed(2),
};
console.log(`Performance degradation: P95 ${degradation.p95}%, Error Rate ${degradation.errorRate}%`);
// 如果性能劣化超过 10%,发出告警
if (Math.abs(parseFloat(degradation.p95)) > 10) {
console.error('ALERT: Significant performance degradation detected!');
}
}
CI/CD 集成
如何将 k6 集成到 CI/CD 流程?
GitHub Actions 集成:
yaml
name: Performance Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
# 每天凌晨 2 点运行
- cron: '0 2 * * *'
jobs:
performance-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run k6 load test
uses: grafana/k6-action@v0.3.0
with:
filename: tests/load-test.js
flags: --out influxdb=http://influxdb:8086/k6
env:
K6_INFLUXDB_ORGANIZATION: ${{ secrets.INFLUXDB_ORG }}
K6_INFLUXDB_BUCKET: k6
K6_INFLUXDB_TOKEN: ${{ secrets.INFLUXDB_TOKEN }}
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: k6-results
path: summary.json
- name: Comment PR with results
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const summary = JSON.parse(fs.readFileSync('summary.json', 'utf8'));
const comment = `
## Performance Test Results
| Metric | Value |
|--------|-------|
| Avg Response Time | ${summary.metrics.http_req_duration.values.avg.toFixed(2)}ms |
| P95 Response Time | ${summary.metrics.http_req_duration.values['p(95)'].toFixed(2)}ms |
| Error Rate | ${(summary.metrics.http_req_failed.values.rate * 100).toFixed(2)}% |
| Total Requests | ${summary.metrics.http_reqs.values.count} |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
GitLab CI 集成:
yaml
# .gitlab-ci.yml
stages:
- test
- performance
performance-test:
stage: performance
image: grafana/k6:latest
script:
- k6 run --out influxdb=http://influxdb:8086/k6 tests/load-test.js
artifacts:
reports:
junit: summary.xml
paths:
- summary.json
expire_in: 30 days
only:
- main
- merge_requests
# 设置阈值,失败时中断 pipeline
allow_failure: false
Jenkins 集成:
groovy
// Jenkinsfile
pipeline {
agent any
stages {
stage('Performance Test') {
steps {
script {
docker.image('grafana/k6:latest').inside {
sh '''
k6 run \
--out influxdb=http://influxdb:8086/k6 \
--summary-export=summary.json \
tests/load-test.js
'''
}
}
}
}
stage('Analyze Results') {
steps {
script {
def summary = readJSON file: 'summary.json'
def p95 = summary.metrics.http_req_duration.values['p(95)']
def errorRate = summary.metrics.http_req_failed.values.rate
echo "P95 Response Time: ${p95}ms"
echo "Error Rate: ${errorRate * 100}%"
// 性能门禁
if (p95 > 500) {
error("Performance degradation: P95 ${p95}ms exceeds threshold 500ms")
}
if (errorRate > 0.01) {
error("High error rate: ${errorRate * 100}% exceeds threshold 1%")
}
}
}
}
stage('Publish Results') {
steps {
archiveArtifacts artifacts: 'summary.json', fingerprint: true
// 发送通知
slackSend(
color: 'good',
message: "Performance test passed for ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
post {
failure {
slackSend(
color: 'danger',
message: "Performance test failed for ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
如何实现性能回归测试?
建立性能基线:
javascript
// baseline-test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 100,
duration: '5m',
thresholds: {
// 基于历史数据设置阈值
http_req_duration: [
'avg<400', // 平均响应时间不超过历史均值 +10%
'p(95)<500', // P95 不超过历史 P95 +10%
'p(99)<800', // P99 不超过历史 P99 +10%
],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const response = http.get('https://api.example.com/users');
check(response, {
'status is 200': (r) => r.status === 200,
});
}
export function handleSummary(data) {
// 保存当前测试结果
const current = {
timestamp: new Date().toISOString(),
commit: __ENV.GIT_COMMIT,
branch: __ENV.GIT_BRANCH,
metrics: {
avg: data.metrics.http_req_duration.values.avg,
p95: data.metrics.http_req_duration.values['p(95)'],
p99: data.metrics.http_req_duration.values['p(99)'],
errorRate: data.metrics.http_req_failed.values.rate,
},
};
// 与基线对比
const baseline = loadBaseline();
const comparison = compareWithBaseline(current, baseline);
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'summary.json': JSON.stringify(current),
'comparison.json': JSON.stringify(comparison),
};
}
function compareWithBaseline(current, baseline) {
return {
avgChange: ((current.metrics.avg - baseline.avg) / baseline.avg * 100).toFixed(2) + '%',
p95Change: ((current.metrics.p95 - baseline.p95) / baseline.p95 * 100).toFixed(2) + '%',
p99Change: ((current.metrics.p99 - baseline.p99) / baseline.p99 * 100).toFixed(2) + '%',
passed: current.metrics.p95 < baseline.p95 * 1.1,
};
}
高级主题
k6 如何实现分布式测试?
使用 k6 Cloud:
bash
# 1. 登录 k6 Cloud
k6 login cloud
# 2. 运行云测试
k6 cloud script.js
# 3. 使用配置文件
k6 cloud --config cloud-config.json script.js
手动分布式实现:
javascript
// 思路:在多台机器上运行 k6,每台机器负责不同的 VU 范围
// machine-1: VU 1-1000
k6 run --vus 1000 --env MACHINE_ID=1 script.js
// machine-2: VU 1001-2000
k6 run --vus 1000 --env MACHINE_ID=2 script.js
// machine-3: VU 2001-3000
k6 run --vus 1000 --env MACHINE_ID=3 script.js
// 在脚本中区分不同机器
export default function () {
const machineId = __ENV.MACHINE_ID || 1;
const actualVU = (machineId - 1) * 1000 + __VU;
// 使用 actualVU 确保数据不重复
const userData = users[actualVU % users.length];
}
使用 Kubernetes 分布式:
yaml
# k6-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: k6-load-test
spec:
parallelism: 10 # 10 个 Pod 并行
completions: 10
template:
spec:
containers:
- name: k6
image: grafana/k6:latest
command:
- k6
- run
- --vus=1000
- --duration=10m
- --out=influxdb=http://influxdb:8086/k6
- /scripts/test.js
volumeMounts:
- name: k6-scripts
mountPath: /scripts
volumes:
- name: k6-scripts
configMap:
name: k6-scripts
restartPolicy: Never
如何扩展 k6 功能?
使用 xk6 创建自定义扩展:
bash
# 1. 安装 xk6
go install go.k6.io/xk6/cmd/xk6@latest
# 2. 构建自定义 k6 二进制(集成扩展)
xk6 build --with github.com/grafana/xk6-sql
xk6 build --with github.com/grafana/xk6-redis
# 3. 使用自定义扩展
./k6 run script-with-sql.js
自定义扩展示例(Redis):
javascript
import redis from 'k6/x/redis';
const client = new redis.Client('redis://localhost:6379');
export default function () {
// 使用 Redis 扩展
client.set('key', 'value');
const value = client.get('key');
console.log(value);
}
创建自己的扩展:
go
// extension.go
package myextension
import (
"go.k6.io/k6/js/modules"
)
func init() {
modules.Register("k6/x/myextension", new(RootModule))
}
type RootModule struct{}
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &ModuleInstance{vu: vu}
}
type ModuleInstance struct {
vu modules.VU
}
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.Exports{
Named: map[string]interface{}{
"myFunction": mi.myFunction,
},
}
}
func (mi *ModuleInstance) myFunction(input string) string {
// 实现自定义功能
return "processed: " + input
}
k6 的限制和替代方案
k6 的限制:
-
浏览器渲染测试:
- k6 无法执行 JavaScript、CSS、渲染页面
- 替代:Playwright、Puppeteer、Lighthouse
-
复杂协议支持:
- 原生只支持 HTTP/WebSocket/gRPC
- 替代:JMeter(支持更多协议)
-
GUI 界面:
- 没有图形界面
- 替代:JMeter、LoadRunner
-
原生分布式测试:
- 开源版不支持
- 替代:k6 Cloud(收费)或 Gatling
-
Node.js 生态:
- 不是真正的 Node.js,不支持所有 npm 包
- 替代:使用 Artillery(基于 Node.js)
实战经验问题
描述一个你实际使用 k6 的项目
回答模板(STAR 法则):
Situation(背景):
我在一个电商平台的微服务架构项目中使用了 k6。系统包含 20+ 个微服务,
日均 API 调用量达到 1000 万次。在一次大促活动前,需要进行全面的性能测试,
确保系统能够承受 10 倍于日常的流量。
Task(任务):
目标:
1. 验证系统能够承受 10 倍日常流量(1 亿次调用/天)
2. 识别性能瓶颈和单点故障
3. 制定容量规划和优化建议
4. 建立持续性能测试体系
Action(行动):
1. 测试准备:
- 分析生产环境流量模式,确定核心业务场景
- 设计测试场景:浏览(60%)、搜索(25%)、下单(15%)
- 准备测试数据:10万用户、100万商品、生产级数据分布
2. 脚本开发:
- 使用 JavaScript 编写测试脚本
- 实现完整的用户行为模拟(登录、浏览、搜索、加购、下单)
- 使用 SharedArray 优化内存使用
- 使用 Scenarios 实现多场景混合测试
3. 监控集成:
- 集成 InfluxDB + Grafana 实时监控
- 配置 Prometheus 收集应用指标
- 设置多维度告警(响应时间、错误率、系统资源)
4. 测试执行:
- 从 100 并发逐步增加到 10,000 并发
- 每个阶段运行 30 分钟,观察系统稳定性
- 记录性能指标和系统资源使用情况
5. 问题发现:
- 发现订单服务在 5000 并发时响应时间剧增
- 数据库连接池配置不足
- Redis 缓存击穿导致数据库压力过大
- 部分接口没有做好限流保护
Result(结果):
1. 性能优化:
- 优化数据库连接池:20 → 100
- 实现 Redis 二级缓存
- 添加接口限流和熔断
- 优化 SQL 查询,添加索引
2. 性能提升:
- P95 响应时间从 800ms 降至 300ms
- 系统容量从 3000 并发提升至 12000 并发
- 错误率从 2% 降至 0.1%
3. 持续集成:
- 将 k6 集成到 CI/CD 流程
- 每次发布前自动运行性能测试
- 建立性能回归测试机制
4. 大促成功:
- 活动期间系统稳定运行
- 实际峰值 8000 并发,系统运行正常
- 99.95% 可用性,超出预期
在 k6 测试中遇到过什么难题?如何解决?
问题1:测试环境与生产环境差异导致结果不准确
问题:
测试环境的性能测试结果无法反映生产环境的实际情况
解决方案:
1. 使用生产级别的硬件配置
2. 使用生产数据快照(脱敏后)
3. 模拟真实的网络延迟和丢包
4. 在低峰期进行生产环境测试(影子流量)
5. 使用容器技术保证环境一致性
问题2:测试数据准备困难
问题:
需要大量真实数据,但手工准备耗时且不现实
解决方案:
1. 使用数据生成工具(Faker.js)
2. 从生产环境导出数据(脱敏)
3. 使用 SharedArray 高效加载大数据文件
4. 按需动态生成数据
示例代码:
import { SharedArray } from 'k6/data';
import { faker } from 'https://cdn.skypack.dev/@faker-js/faker';
const users = new SharedArray('users', function () {
const userData = [];
for (let i = 0; i < 10000; i++) {
userData.push({
username: faker.internet.userName(),
email: faker.internet.email(),
phone: faker.phone.number(),
});
}
return userData;
});
问题3:复杂的业务流程难以模拟
问题:
真实的业务流程涉及多个步骤、条件分支、异步操作
解决方案:
1. 将复杂流程拆分为多个函数
2. 使用状态机模式管理流程状态
3. 实现数据关联和参数传递
4. 使用 setup/teardown 管理全局状态
示例代码:
export function setup() {
// 准备测试数据
return { baseUrl: 'https://api.example.com' };
}
export default function (data) {
const state = {
userId: null,
cartId: null,
orderId: null,
};
// 步骤1:登录
state.userId = login(data.baseUrl);
if (!state.userId) return;
// 步骤2:加购
state.cartId = addToCart(data.baseUrl, state.userId);
if (!state.cartId) return;
// 步骤3:下单
state.orderId = placeOrder(data.baseUrl, state.cartId);
if (!state.orderId) return;
// 步骤4:支付
payment(data.baseUrl, state.orderId);
}
问题4:测试结果分析困难
问题:
大量的测试数据难以分析和定位问题
解决方案:
1. 使用标签(Tags)区分不同的请求
2. 使用自定义指标跟踪业务指标
3. 集成 Grafana 实时可视化
4. 使用 handleSummary 生成自定义报告
5. 结合 APM 工具(如 Datadog)深入分析
示例:
const businessMetrics = {
orderCreated: new Counter('orders_created'),
orderFailed: new Counter('orders_failed'),
averageOrderValue: new Trend('average_order_value'),
};
export default function () {
const orderResponse = http.post(
'https://api.example.com/orders',
orderData,
{ tags: { operation: 'create_order', priority: 'high' } }
);
if (orderResponse.status === 201) {
businessMetrics.orderCreated.add(1);
const order = JSON.parse(orderResponse.body);
businessMetrics.averageOrderValue.add(order.total);
} else {
businessMetrics.orderFailed.add(1);
}
}