3.4 配套代码:三种负载曲线的 k6 脚本
对应小节:3.4 负载曲线 三份可直接运行的脚本:阶梯加压、恒定到达率、尖峰测试。
一、阶梯加压:找拐点
// loadtest/profile-staircase.js
import http from 'k6/http';
import { check } from 'k6';
const LEVELS = [100, 200, 400, 700, 1000, 1500, 2000];
const stages = [];
for (const target of LEVELS) {
stages.push({ target, duration: '30s' }); // 爬升:让系统升到该级
stages.push({ target, duration: '2m' }); // ⚠️ 稳态:采数据必须在这里,至少 2 分钟
}
export const options = {
scenarios: {
staircase: {
executor: 'ramping-arrival-rate',
startRate: 50,
timeUnit: '1s',
preAllocatedVUs: 500,
maxVUs: 5000,
stages,
},
},
// 故意不设 thresholds:容量测试的目的是找拐点,不是判定通过/失败
discardResponseBodies: true,
};
function zipf(max) {
return Math.max(1, Math.floor(max * Math.pow(Math.random(), 3)) + 1);
}
export default function () {
const res = http.get(`${__ENV.BASE_URL}/orders/${zipf(1_000_000)}`, {
tags: { name: 'GET /orders/{id}' },
});
check(res, { ok: (r) => r.status === 200 });
}
关键检查:每级的稳态窗口(那 2 分钟)才是采数据的区间。如果只跑 30 秒的爬升段,你会得到一条平坦且偏低的曲线(见 3.4 节的解释)。
k6 run --out json=docs/experiments/E0x/results/k6-staircase.json loadtest/profile-staircase.js
二、恒定到达率:验证 SLO
// loadtest/profile-constant.js
import http from 'k6/http';
import { check } from 'k6';
import { Trend, Rate } from 'k6/metrics';
const okRate = new Rate('ok_rate');
const depLatency = new Trend('dep_latency_ms', true);
export const options = {
scenarios: {
steady: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.RATE || 500),
timeUnit: '1s',
duration: __ENV.DURATION || '10m',
preAllocatedVUs: 500,
maxVUs: 5000,
gracefulStop: '30s',
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{expected_response:true}': ['p(95)<200', 'p(99)<500'],
ok_rate: ['rate>0.99'],
},
discardResponseBodies: true,
summaryTrendStats: ['avg', 'min', 'med', 'p(90)', 'p(95)', 'p(99)', 'p(99.9)', 'max'],
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/orders/1`, { tags: { name: 'GET /orders/{id}' } });
okRate.add(check(res, { ok: (r) => r.status === 200 }));
}
预热怎么办:k6 没有独立的预热阶段概念,两种做法:
# 做法 A:先跑一次短的预热(结果丢弃),再跑正式测量
k6 run --quiet --vus 20 --duration 60s loadtest/profile-constant.js > /dev/null
k6 run --out json=results/k6-constant.json loadtest/profile-constant.js
# 做法 B:在脚本里用 ramping-arrival-rate 把前 60 秒作为预热段,
# 分析时只取预热之后的时间窗口(推荐,更省事)
三、尖峰测试:验证弹性与恢复
// loadtest/profile-spike.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
spike: {
executor: 'ramping-arrival-rate',
startRate: 500,
timeUnit: '1s',
preAllocatedVUs: 2000,
maxVUs: 10000,
stages: [
{ target: 500, duration: '2m' }, // ① 基线(必须从非零开始!)
{ target: 3000, duration: '10s' }, // ② 尖峰升起(要快)
{ target: 3000, duration: '30s' }, // ③ 尖峰维持
{ target: 500, duration: '10s' }, // ④ 快速回落
{ target: 500, duration: '3m' }, // ⑤ ⚠️ 恢复观察期(最有价值的一段)
],
},
},
discardResponseBodies: true,
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/orders/1`, { tags: { name: 'GET /orders/{id}' } });
check(res, { ok: (r) => r.status === 200 });
}
为什么第 ⑤ 段最重要:尖峰本身只告诉你「会不会崩」,而恢复期告诉你「崩了之后多久能好、能不能自愈」——这决定了故障的严重程度(见 3.6 节)。
尖峰测试要采集的数据:
# 压测期间持续记录关键指标(10 秒一个点)
(
while true; do
echo "$(date +%s) $(curl -s localhost:8080/metrics | grep -E 'db_pool_pending|executor_queue_depth' | tr '\n' ' ')"
sleep 10
done
) > docs/experiments/E0x/results/spike-metrics.txt &
MONITOR_PID=$!
k6 run loadtest/profile-spike.js
kill "$MONITOR_PID" 2>/dev/null || true
四、三种曲线的对照与选择
| 阶梯加压 | 恒定到达率 | 尖峰测试 | |
|---|---|---|---|
| 回答 | 拐点在哪? | 达标吗? | 会崩吗?能恢复吗? |
| 时长 | 20–60 分钟 | 10–20 分钟 | 5–10 分钟 |
| 必须有的段 | 每级稳态 2–5 分钟 | 预热段 + 稳态段 | 基线段 + 恢复段 |
| 关键输出 | 拐点 QPS、崩溃点 | P50/P95/P99、错误率 | 恢复时间、崩溃形态 |
| 典型误用 | 每级只跑 30 秒 | 用 VU 模型 | 从 0 直接跳到峰值 |
五、一个通用的「先检查再分析」脚本
#!/usr/bin/env bash
# tools/check-k6-result.sh <summary.json> <expected_rate>
# 分析前先验证数据可用性——这一步能避免大量无效分析
set -uo pipefail
SUMMARY="${1:?usage: check-k6-result.sh <summary.json> <expected_rate>}"
EXPECTED="${2:?}"
python3 - "$SUMMARY" "$EXPECTED" <<'PY'
import json, sys
summary, expected = sys.argv[1], float(sys.argv[2])
m = json.load(open(summary))["metrics"]
actual = m["http_reqs"]["rate"]
failed = m["http_req_failed"]["rate"]
dur = m["http_req_duration"]
print(f"设定到达率 : {expected:>10.1f} req/s")
print(f"实际到达率 : {actual:>10.1f} req/s ({actual/expected-1:+.1%})")
print(f"错误率 : {failed:>10.2%}")
print(f"P50 : {dur['med']:>10.1f} ms")
print(f"P95 : {dur['p(95)']:>10.1f} ms")
print(f"P99 : {dur['p(99)']:>10.1f} ms")
print(f"P99.9 : {dur['p(99.9)']:>10.1f} ms")
print(f"max : {dur['max']:>10.1f} ms")
print()
problems = []
if abs(actual / expected - 1) > 0.05:
problems.append("❌ 实际到达率与设定值偏差 > 5%:压测机可能是瓶颈,数据不可用")
if failed > 0.01:
problems.append("❌ 错误率 > 1%:延迟数据可能因剔除失败请求而虚低")
if dur["p(99)"] > 0 and dur["p(50)"] > 0 and dur["p(99)"] / dur["p(50)"] < 2:
problems.append("⚠️ P99/P50 < 2:分布过于干净,怀疑闭环模型导致尾延迟被低估")
if problems:
print("\n".join(problems))
sys.exit(1)
print("✅ 数据看起来可用")
PY
第三条检查特别有用:真实后端的延迟分布通常是重尾的,P99 往往是 P50 的 5–20 倍。如果 P99 只比 P50 高一点,很可能是闭环模型掩盖了长尾(第 0 章 0.6 节)。
六、动手改造
| 改动 | 观察什么 |
|---|---|
| 把阶梯的稳态从 2 分钟改成 20 秒 | 曲线变平坦、变低——你会「找不到」拐点 |
| 把尖峰的基线段去掉(从 0 直接跳到 3000) | 测出的是冷启动,不是尖峰弹性 |
| 把尖峰的恢复段从 3 分钟改成 10 秒 | 你会看不到「系统是否恢复」——这正是最该看的部分 |
给恒定压测加上 dep_latency 趋势指标 |
把客户端延迟与服务端依赖延迟对比,直接看出网络+排队占多少 |
把 summaryTrendStats 里的 p(99.9) 去掉 |
你就失去了观察极端长尾的能力 |
七、这段代码的局限
- k6 的
ramping-arrival-rate在爬升段的「到达率」是线性变化的,所以爬升段的数据混合了多个负载水平,不能用于分析单个负载点——必须只用稳态窗口的数据(本文件里的做法是让稳态窗口足够长,分析时按时间切片)。 - 脚本里的齐夫取样是近似的,与真实热点分布可能有差异。
- 单机压测:如果 k6 与服务在同一台机器,
preAllocatedVUs再大也无法消除资源争用。 gracefulStop会让压测结束后还有请求在飞,如果分析时用了完整时间窗口,末尾的数据可能不完整。