1. 项目概述为什么我们需要关注k6中的stage如果你正在或计划使用Grafana k6进行性能测试那么“stage”这个概念绝对是你绕不开的核心。它不像一个简单的“运行5分钟”的指令那么直白而是k6测试脚本中用于定义负载模型的“指挥中枢”。简单来说stage决定了你的虚拟用户VUs如何随时间变化是模拟真实用户行为、验证系统弹性的关键。想象一下你要测试一个电商网站的秒杀活动。用户行为绝对不是瞬间涌入然后瞬间消失。更真实的场景是活动开始前少量用户预热、刷新页面活动开始时用户量在短时间内急剧攀升达到峰值并持续一段时间活动结束后用户量缓慢回落。这种“爬坡-保持-下坡”的负载模式就是stage的用武之地。它让你能精细地控制并发用户数随时间变化的曲线从而模拟出贴近生产环境的压力场景而不仅仅是给出一个平均负载。在性能测试领域一个常见的误区是只关注“最大并发数”和“平均响应时间”。但系统的瓶颈往往出现在负载变化的瞬间——比如用户量突然激增时数据库连接池是否够用服务自动扩缩容是否及时stage通过定义这些变化阶段帮助你精准地发现这些“瞬态”问题。因此深入理解stage意味着你能设计出更具杀伤力、也更贴近现实的测试场景让性能测试的价值最大化。2. Stage核心概念深度解析不仅仅是“阶段”2.1 Stage的本质负载变化的时间函数在k6的语境下一个stage本质上是一个定义了虚拟用户数量随时间线性变化的规则。它不是一个静态的“步骤”而是一个动态的“过程”。每个stage由三个核心属性构成duration: 这个阶段持续的时长例如“5m”代表5分钟。target: 这个阶段结束时期望达到的虚拟用户数。可选startVUs: 这个阶段开始时已有的虚拟用户数。如果不指定k6会根据上一个stage的结束状态或全局配置智能处理。它的工作逻辑是在duration指定的时间内k6会平滑地默认是线性地将活跃的虚拟用户数从起始值调整到目标值。例如一个stage配置为{ duration: ‘2m’ target: 100 }意味着在2分钟内k6会从当前用户数可能是0开始均匀地增加VUs直到2分钟结束时正好有100个虚拟用户在并发执行你的测试脚本。2.2 Stage与options.executor的关联与区别这是初学者最容易混淆的地方。k6提供了多种executor执行器如ramping-vus、constant-vstages、shared-iterations等。stage是ramping-vus这个执行器的专属配置项。ramping-vus执行器专为复杂的、多阶段的负载模型设计。它通过一个stages数组来定义整个测试过程中负载的完整变化轨迹。你可以把它想象成绘制一条负载曲线每个stage就是曲线上的一段线段。其他执行器例如constant-vus它只维持一个固定数量的VUs没有“阶段”变化的概念。shared-iterations则关注总迭代次数的共享完成。所以当你决定要使用stage来设计负载时你实际上已经选择了ramping-vus这种执行策略。理解这一点能帮助你在设计测试场景时做出正确的架构选择。2.3 一个stage配置的完整示例与拆解让我们看一个模拟典型工作日流量模式的配置import http from ‘k6/http’; import { check sleep } from ‘k6’; export const options { scenarios: { typical_workday: { executor: ‘ramping-vus’ // 关键指定使用支持stage的执行器 startVUs: 10 // 测试开始时立即启动的VU数量 stages: [ // stages数组定义了完整的负载变化序列 // 阶段1早高峰爬坡 (30分钟内从10个用户增加到300个) { duration: ‘30m’ target: 300 } // 阶段2早高峰保持 (保持300用户2小时) { duration: ‘2h’ target: 300 } // 阶段3午间回落 (1小时内从300用户减少到100用户) { duration: ‘1h’ target: 100 } // 阶段4午后平稳期 (保持100用户3小时) { duration: ‘3h’ target: 100 } // 阶段5晚高峰爬坡 (45分钟内从100用户增加到500用户) { duration: ‘45m’ target: 500 } // 阶段6晚高峰峰值压力测试 (保持500用户30分钟) { duration: ‘30m’ target: 500 } // 阶段7收尾下降 (1小时内从500用户平滑降为0) { duration: ‘1h’ target: 0 } ] gracefulRampDown: ‘5m’ // 优雅关闭时间给正在运行的迭代完成的机会 } } thresholds: { ‘http_req_duration’: [‘p(95)500’] // 定义性能阈值95%的请求响应时间应小于500ms } }; export default function () { const response http.get(‘https://test-api.mycompany.com/v1/health’); check(response { ‘status is 200’: (r) r.status 200 }); sleep(1); // 每次迭代后思考1秒模拟用户操作间隔 }配置拆解与逻辑分析startVUs: 10测试一开始立刻就有10个VUs开始执行default函数。这模拟了系统已有的基线负载。阶段1 (30m-300)在接下来的30分钟内k6会以(300-10)/30 ≈ 9.67 VUs/分钟的速率线性增加VUs。这是模拟用户陆续登录系统进入工作状态。阶段2 (2h-300)负载达到300后在2小时内维持不变。这是测试系统在稳定高负载下的表现观察是否有内存泄漏、响应时间是否稳定。阶段5和6这是压力测试的核心。负载再次攀升至更高的峰值500并持续一段时间。目的是找到系统的性能拐点或容量上限。阶段7 (1h-0)将用户数线性降为0。配合gracefulRampDown确保活跃的请求能正常完成而不是被强行中断这能更准确地统计最终的成功率。thresholds在整个多阶段的测试过程中我们持续监控响应时间。如果任何一个阶段比如晚高峰峰值期的P95响应时间超过500ms测试就会被标记为失败。这让我们能将性能要求与具体的业务场景如高峰时段绑定。注意duration的字符串格式非常灵活支持‘10s’10秒、‘5m’5分钟、‘2h’2小时甚至‘1h30m’1小时30分钟。务必确保格式正确否则k6会解析失败。3. Stage的高级应用与实战策略3.1 设计贴合业务场景的负载模型仅仅理解语法是不够的关键是利用stage来建模。以下是一些常见场景的stage设计思路上线/扩容验证[{duration: ‘5m’ target: 50} {duration: ‘10m’ target: 50} {duration: ‘5m’ target: 0}]。快速上量稳定观察然后下量。用于验证新服务启动或扩容后能否立即承接流量。稳定性与耐力测试[{duration: ‘30m’ target: 200} {duration: ‘8h’ target: 200} {duration: ‘30m’ target: 0}]。用长时间如8小时的稳定负载观察系统在持续压力下的表现如内存增长、GC情况、中间件连接池状态。尖峰冲击测试[{duration: ‘1m’ target: 1000} {duration: ‘5m’ target: 1000} {duration: ‘1m’ target: 0}]。用极短的时间冲到极高负载测试系统的瞬时抗压能力和弹性伸缩组的反应速度。波浪形负载测试设计多个连续的、目标值起伏的stage模拟流量周期性波动的场景如每半小时一次的促销抢券。实操心得设计stage前一定要去分析生产环境的监控数据如Prometheus中的QPS、活跃用户数图表。尝试用一系列stage去“拟合”真实的流量曲线这样的测试结果才有说服力。我通常会先用一个简单的、时长短的stage组合做快速验证确保脚本和基础架构没问题再运行长时间、复杂的正式测试。3.2 结合Scenarios实现更复杂的测试编排单个ramping-vus场景已经很强大了但k6的scenarios允许你同时运行多个独立的负载模型这打开了更复杂的测试大门。例如你可以模拟一个混合场景场景A (后台API)使用constant-vus执行器始终维持50个VUs持续调用一些低频但重要的管理后台API。场景B (用户前端)使用ramping-vus执行器定义包含多个stage的负载模拟前端用户的高峰浏览和下单行为。export const options { scenarios: { background_api: { executor: ‘constant-vus’ vus: 50 duration: ‘1h’ exec: ‘backgroundApiTest’ // 指定执行另一个函数 } frontend_traffic: { executor: ‘ramping-vus’ startVUs: 10 stages: [ { duration: ‘10m’ target: 200 } { duration: ‘40m’ target: 200 } { duration: ‘10m’ target: 0 } ] exec: ‘frontendUserTest’ // 指定执行前端测试函数 } } }; // 不同的测试逻辑可以分开编写 export function backgroundApiTest() { /* ... */ } export function frontendUserTest() { /* ... */ }这样你就能在一个测试中同时观察核心业务流量和背景任务对系统资源的综合影响更真实地反映生产环境状况。3.3 性能阈值与Stage的联动监控阈值Thresholds是k6的另一大杀器与stage结合能实现场景化的断言。你不仅可以设置全局阈值还可以为特定的度量指标添加标签然后针对标签设置阈值。假设你的API有/fast和/slow两个端点你想在高峰阶段对/slow端点的要求放宽些但平峰期要求严格。虽然k6的阈值不能直接绑定到某个stage但你可以通过分析不同stage时间窗口内的结果来间接实现。更实用的做法是为不同优先级的请求打上不同的标签然后设置不同的阈值import http from ‘k6/http’; import { check } from ‘k6’; export const options { stages: [ /* ... 定义高峰平峰阶段 ... */ ] thresholds: { ‘http_req_duration{type:fast}’: [‘p(95)200’] // 快速接口要求高 ‘http_req_duration{type:slow}’: [‘p(95)1000’] // 慢速接口要求稍低 ‘http_req_failed{type:fast}’: [‘rate0.01’] // 快速接口错误率要求极高 } }; export default function () { // 打上标签 let fastResp http.get(‘https://api.example.com/fast’ { tags: { type: ‘fast’ } }); let slowResp http.get(‘https://api.example.com/slow’ { tags: { type: ‘slow’ } }); check(fastResp { ‘status is 200’: (r) r.status 200 }); check(slowResp { ‘status is 200’: (r) r.status 200 }); }在Grafana看板中你可以轻松地按type标签过滤观察不同阶段、不同类型请求的性能表现。4. 常见问题、调试技巧与避坑指南4.1 Stage配置不生效或行为异常问题现象定义了stages但运行后发现VU数量没有变化或者变化规律不符合预期。排查思路检查执行器Executor这是最常犯的错误确认options中配置的executor是‘ramping-vus’。如果你写成了‘constant-vus’那么stages配置会被完全忽略。检查作用域确保stages数组是正确嵌套在scenarios的某个场景下的。如果是单场景直接放在options下是旧写法建议统一使用scenarios结构。验证JSON格式stages是一个数组每个stage对象属性名要用双引号虽然在JS中单引号也可但保持规范。确保没有缺少逗号或括号。理解startVUsstartVUs是测试开始时立即启动的VU数量。第一个stage的“起始用户数”默认就是这个值。如果你设置startVUs: 100第一个stage是{duration: ‘5m’ target: 200}那么k6会在5分钟内从100个VU增加到200个。4.2 负载未按预期线性增长问题现象理论上应该平滑增长的VU曲线在监控中看到的是阶梯状或锯齿状。原因与解决资源瓶颈你的负载生成机器运行k6的机器CPU或内存不足无法按需快速启动新的VU实例。使用k6 run --out statsd或查看k6自身的监控输出检查vus和vus_max指标。如果vus一直达不到target很可能是生成器性能不够。解决方案使用分布式执行如k6 Cloud或自建k6集群或者优化测试脚本降低单个VU的资源消耗。gracefulRampDown影响在stage下降期target减少k6不会强制停止VU而是等待它们完成当前的迭代gracefulRampDown参数指定了最大等待时间。这可能导致VU数量下降的速度慢于预期。这是符合预期的行为目的是保证测试的准确性。如果你需要更精确的控制可以缩短gracefulRampDown时间但需承担提前终止请求的风险。4.3 如何精准分析不同Stage的性能数据k6输出的结果是一个聚合报告。要分析单个stage的表现你需要借助外部工具或更细致的标签。最佳实践使用--out参数输出到时序数据库这是最推荐的方式。将结果输出到InfluxDB或Prometheus然后使用Grafana进行可视化。k6 run --out influxdbhttp://localhost:8086/k6 script.js在Grafana中你可以绘制vus指标曲线清晰地看到整个测试过程中VU数量随时间的变化这就是你定义的stage曲线。将http_req_duration等性能指标与vus曲线放在同一个时间轴上。通过时间范围选择器框选出一个特定的stage时间段如从第30分钟到第90分钟然后单独查看这个时间段内的P95响应时间、错误率等。这能精确评估系统在“爬坡期”、“峰值期”等不同压力阶段的表现。为请求打上业务标签如api:loginapi:checkout可以在Grafana中按标签和阶段进行下钻分析定位到具体是哪个接口在哪个阶段出现了性能退化。实操心得我习惯在测试脚本中为每个主要的业务操作或每个http.batch请求都打上具有业务意义的标签。在Grafana中我创建了一个仪表板上半部分是vus和http_reqs_rate请求速率曲线下半部分是一组按标签过滤的http_req_duration百分位图P50 P95 P99。这样负载变化与性能响应的因果关系一目了然。一次我们通过这种对比发现系统在VU达到300时P99延迟突然飙升但P95还很平稳最终定位到是某个数据库分片的热点查询问题这是只看聚合报告无法发现的。4.4 脚本逻辑与Stage的配合陷阱问题在default函数中使用了固定的sleep()时间导致在高VU阶段迭代速度变慢无法产生足够的请求压力。示例与改进// 不太理想的写法 export default function () { http.get(‘https://api.example.com’); sleep(10); // 无论有多少VU每个迭代都固定休眠10秒 }在这种写法下即使你有500个VU每秒能发出的请求数RPS上限也只有500 / 10 50。这无法在高峰阶段对服务器产生足够的压力。更好的做法是使用动态的思考时间或 pacingimport { sleep } from ‘k6’; import { randomIntBetween } from ‘https://jslib.k6.io/k6-utils/1.2.0/index.js’; export default function () { http.get(‘https://api.example.com’); // 模拟更真实的用户行为思考时间在一个范围内随机 sleep(randomIntBetween(1 5)); // 休眠1-5秒之间的随机时间 }或者如果你需要精确控制每个VU的请求速率可以使用scenarios中的executor如constant-arrival-rate它会以固定的频率发起新的迭代不受sleep时间影响。最后一个小技巧在编写复杂的多stage测试脚本时我总是在本地先用极短的duration如‘3s’‘5s’和很小的target值跑一遍。这能快速验证脚本逻辑、阶段衔接和阈值配置是否正确避免浪费大量时间在一个存在基础错误的长时间测试上。确认无误后再将duration和target调整到正式测试的规模。