文档目录

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 会让压测结束后还有请求在飞,如果分析时用了完整时间窗口,末尾的数据可能不完整。