关于 load-testing-plan
用于为服务编写负载测试计划,面向需要明确测试场景和通过标准的工程团队。内容涵盖目标、阈值、工具、环境、指标、结果分析及 CI 集成。
主要功能
- 01基准、压力、突发和耐久场景
- 02基于 SLO 的通过标准
- 03工具与测试环境指南
- 04指标采集与结果分析
- 05CI 发布门禁
适用场景
- 01规划服务发布的性能测试
- 02设置与服务 SLO 挂钩的阈值
- 03安排定期压力与耐久测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mohitagw15856/ pm-claude-skills --skill load-testing-plan安装后在 Claude Code 中输入/load-testing-plan确认已加载。
详细介绍
负载测试计划技能
为服务制定完整的负载与性能测试计划,涵盖测试目标、场景定义、工具配置、成功阈值和 CI 集成。优秀的负载测试计划会明确“可接受的性能”具体意味着什么,让工程师无需自行解读原始数据,即可运行测试并获得通过或失败的结论。
所需输入
如果用户尚未提供,请询问以下信息:
- 服务名称和关键端点 — 要测试哪些端点(路径、方法、典型请求/响应结构)
- 当前流量基线 — 正常负载下当前的请求数/秒、p50/p99 延迟、错误率
- 峰值流量预期 — 预期峰值 RPS(例如,限时促销期间达到基线的 10 倍,或季节性峰值)
- SLO 目标 — 延迟 SLO(p99 < X ms)、错误率 SLO(< Y%)、可用性目标
- 首选测试工具 — k6、Locust、JMeter、Gatling,或无偏好
- 测试环境可用性 — 专用负载测试环境、预发布环境,或生产环境(配合流量整形)
输出格式
负载测试计划:[服务名称]
作者:[姓名] | 团队:[团队名称] 日期:[日期] | **审查周期:**每次重要发布前及每季度 测试工具:[k6 / Locust / JMeter / Gatling] 测试环境:[环境名称和 URL]
1. 目标与范围
测试内容:[服务名称] 负责处理[描述功能 — 例如“来自移动客户端和 Web 客户端的用户身份验证请求”]。本计划用于验证服务在预期流量和更高流量条件下均能满足其 SLO。
范围内:
- [端点 1:METHOD /path — 描述]
- [端点 2:METHOD /path — 描述]
- [端点 3:METHOD /path — 描述]
范围外:
- [明确排除的端点及原因 — 例如“管理 API — 流量较低,不纳入负载测试”]
- [无法进行负载测试的第三方集成 — 改用模拟方式]
2. 性能目标(成功标准)
每个场景都有明确的通过/失败阈值。任何阈值超标,测试运行即判定为失败。
| 指标 | 基线场景 | 压力场景 | 峰值场景 | 稳定性场景 |
|---|---|---|---|---|
| p50 延迟 | < [X] ms | < [X × 1.5] ms | < [X × 2] ms | < [X] ms |
| p95 延迟 | < [Y] ms | < [Y × 1.5] ms | < [Y × 2] ms | < [Y] ms |
| p99 延迟 | < [Z] ms | < [Z × 2] ms | < [Z × 3] ms | < [Z] ms |
| 错误率 | < [0.1]% | < [1]% | < [2]% | < [0.1]% |
| 吞吐量 | ≥ [N] RPS | ≥ [N × 3] RPS | 不适用 | ≥ [N] RPS |
| 失败请求 | 0(5xx) | < [阈值] | < [阈值] | 0(5xx) |
**SLO 参考:**这些阈值根据服务的 SLO 得出 — p99 < [Z ms]、错误率 < [0.1]%、可用性 [99.9]%。
3. 流量模型
基线流量(当前生产环境):
- 平均 RPS:[N] 请求/秒
- 峰值 RPS(观测值):[N] 请求/秒
- 按端点划分的请求分布:
- [端点 1]:[X]% 的流量
- [端点 2]:[Y]% 的流量
- [端点 3]:[Z]% 的流量
模拟用户行为:
- 请求间的思考时间:[X–Y] 秒(随机)
- 平均会话时长:[N] 分钟
- 已认证用户与匿名用户的比例:[X]%/[Y]%
- 地理分布:[区域 1 X]%、[区域 2 Y]%
4. 测试场景
场景 1:基线(稳定状态)
**目的:**确认服务在正常生产负载下的表现可以接受。 **持续时间:**10 分钟 **负载曲线:**在 2 分钟内逐步提升至 [N] RPS,并保持 8 分钟。 并发数:[N] 个虚拟用户
**通过标准:**满足上述目标表中“基线”列的所有阈值。
场景 2:压力测试
目的: 找出崩溃临界点——服务在 SLO 被违反前能承受多大的负载? 时长: 20–30 分钟 负载配置: 从 [N] RPS(基线)开始,以 5 分钟为步长递增至 [N × 5] RPS。每个负载级别保持 5 分钟。首次违反 SLO 时停止。 并发数: 随 RPS 目标值变化
需要记录:
- p99 延迟首次超过 SLO 时的 RPS
- 错误率首次超过 SLO 时的 RPS
- 负载降回基线后,服务是否能够恢复
场景 3:突增测试
目的: 模拟流量突然激增(限时抢购、爆红事件、机器人攻击)。 时长: 15 分钟 负载配置: 以 [N] RPS(基线)保持 3 分钟,瞬间升至 [N × 10] RPS 并保持 5 分钟,再降回基线并保持 7 分钟。
需要记录:
- 突增期间及恢复期间的延迟
- 服务是否能平稳地削减负载(速率限制、队列深度)
- 突增结束后恢复至基线延迟所需的时间
场景 4:浸泡/耐久测试
目的: 检测内存泄漏、连接池耗尽以及随时间推移而缓慢退化的问题。 时长: 4–8 小时(通宵运行) 负载配置: 在整个测试期间保持 [N × 1.5] RPS(高于基线 50%)。
观察指标:
- 随时间变化的内存使用趋势(不应无限增长)
- 错误率趋势(应保持平稳,不应逐渐上升)
- GC 暂停频率(JVM/Go 服务)
- 数据库连接池利用率
- p99 延迟趋势(数小时内不应逐渐升高)
5. 测试环境要求
基础设施
| 组件 | 要求 | 备注 |
|---|---|---|
| 被测服务 | 与生产环境隔离 | [N] 个副本,资源限制与生产环境匹配 |
| 数据库 | 使用生产规模数据的独立实例 | 种子脚本见第 7 节 |
| 缓存(Redis/Memcached) | 测试开始时为空 | 确保测试冷启动条件 |
| 负载生成器 | 与被测服务分离 | 至少 [N] 个 vCPU 和 [N] GB RAM |
| 网络 | 到服务的低延迟链路 | 不要在同一主机上运行生成器 |
数据填充
每次测试运行前,确保环境中包含:
# Seed test users (needed for authenticated endpoint tests)
[seed command or script path — e.g. python scripts/seed_load_test_users.py --count 10000]
# Seed test data for read endpoints
[seed command — e.g. ./scripts/seed_products.sh --count 50000]
# Verify seed completed
[verification command — e.g. psql $DB_URL -c "SELECT COUNT(*) FROM users WHERE load_test=true"]
测试数据规则:
- 负载测试中绝不使用真实生产用户数据
- 为所有测试生成的记录添加
load_test=true标签,以便轻松清理 - 每次测试后运行清理命令:
[cleanup command]
6. 工具配置
k6 脚本框架
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('error_rate');
const endpointLatency = new Trend('endpoint_latency', true);
// Test configuration — override per scenario
export const options = {
scenarios: {
baseline: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: [BASELINE_VUS] },
{ duration: '8m', target: [BASELINE_VUS] },
{ duration: '1m', target: 0 },
],
},
},
thresholds: {
http_req_duration: [
'p(95)<[Y_MS]',
'p(99)<[Z_MS]',
],
error_rate: ['rate<0.01'],
http_req_failed: ['rate<0.01'],
},
};
// Auth helper — get token once per VU
export function setup() {
const loginRes = http.post('[BASE_URL]/auth/login', JSON.stringify({
username: `load_test_user_${Math.floor(Math.random() * 10000)}@example.com`,
password: '[LOAD_TEST_PASSWORD]',
}), { headers: { 'Content-Type': 'application/json' } });
check(loginRes, { 'login ok': (r) => r.status === 200 });
return { token: loginRes.json('access_token') };
}
export default function (data) {
const headers = {
Authorization: `Bearer ${data.token}`,
'Content-Type': 'application/json',
};
// Endpoint 1: [Description]
const res1 = http.get('[BASE_URL]/[endpoint-1]', { headers });
check(res1, {
'[endpoint-1] status 200': (r) => r.status === 200,
'[endpoint-1] latency < [X]ms': (r) => r.timings.duration < [X],
});
errorRate.add(res1.status >= 400);
endpointLatency.add(res1.timings.duration, { endpoint: '[endpoint-1]' });
sleep(Math.random() * [THINK_TIME_MAX] + [THINK_TIME_MIN]);
// Endpoint 2: [Description]
const res2 = http.post('[BASE_URL]/[endpoint-2]',
JSON.stringify({ [key]: '[value]' }),
{ headers }
);
check(res2, {
'[endpoint-2] status 201': (r) => r.status === 201,
});
errorRate.add(res2.status >= 400);
}
Locust 脚本框架(替代方案)
from locust import HttpUser, task, between
import random
class [ServiceName]User(HttpUser):
wait_time = between([THINK_TIME_MIN], [THINK_TIME_MAX])
token = None
def on_start(self):
"""Called once per simulated user — authenticate."""
user_id = random.randint(1, 10000)
response = self.client.post("/auth/login", json={
"username": f"load_test_user_{user_id}@example.com",
"password": "[LOAD_TEST_PASSWORD]",
})
self.token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {self.token}"}
@task([WEIGHT_1]) # Weight = relative frequency
def [endpoint_1_task](self):
"""[Endpoint 1 description]"""
with self.client.get(
"/[endpoint-1]",
headers=self.headers,
catch_response=True
) as response:
if response.elapsed.total_seconds() > [LATENCY_THRESHOLD]:
response.failure(f"Too slow: {response.elapsed.total_seconds()}s")
@task([WEIGHT_2])
def [endpoint_2_task](self):
"""[Endpoint 2 description]"""
self.client.post(
"/[endpoint-2]",
json={"[key]": "[value]"},
headers=self.headers,
)
运行测试
# k6 — run baseline scenario
k6 run --env BASE_URL=https://[test-env-url] scripts/load_test.js
# k6 — run stress scenario with output to InfluxDB
k6 run --out influxdb=http://[influxdb-host]:8086/k6 \
--env SCENARIO=stress \
scripts/load_test.js
# Locust — headless run
locust -f locustfile.py \
--headless \
--users [N] \
--spawn-rate [N] \
--run-time 10m \
--host https://[test-env-url] \
--csv=results/[run-id]
# Locust — web UI (interactive)
locust -f locustfile.py --host https://[test-env-url]
7. 需要采集的指标
每次测试运行期间都要采集以下所有指标。缺少其中任何一项都会导致结果比较不可靠。
| 指标 | 来源 | 重要性 |
|---|---|---|
| 各端点的 p50、p95、p99、p999 延迟 | 负载工具 | SLO 验证 |
| 各端点的错误率(4xx、5xx) | 负载工具 | SLO 验证 |
| 每秒请求数(吞吐量) | 负载工具 | 容量基线 |
| CPU 利用率(%) | 基础设施监控 | 饱和信号 |
| 内存利用率(%) | 基础设施监控 | 泄漏检测 |
| GC 暂停时间/频率 | JVM/Go 指标 | 延迟突增的根本原因 |
| DB 连接池:活动/空闲/等待 | DB 指标 | 检测连接池耗尽 |
| DB 查询延迟(p99) | DB 指标 | 下游瓶颈 |
| 缓存命中率 | 缓存指标 | 检测未命中风暴 |
| Pod/实例数量(如启用自动伸缩) | 基础设施 | 伸缩行为 |
| 网络入站/出站字节数 | 基础设施 | 带宽饱和 |
8. 结果分析框架
每次测试运行后,按以下顺序进行分析:
步骤 1 — 检查通过/失败 将所有采集到的指标与第 2 节中的阈值进行比较。记录每个场景的通过/失败情况。
步骤 2 — 延迟分布 绘制完整的延迟直方图,而不只是百分位数。双峰分布(两个凸起)表示存在两条不同的代码路径——请调查较慢的峰。
步骤 3 — 错误关联分析 如果发生错误,请将其与以下情况关联起来:
- 发生时间(是在升压阶段、稳定负载阶段还是突发阶段?)
- 特定端点(是一个端点还是所有端点?)
- 基础设施事件(CPU 飙升、OOM、数据库连接耗尽?)
步骤 4 — 饱和度分析 绘制 CPU、内存和连接池随时间变化的图表。如果任何资源达到容量的 80% 或以上,就可能是瓶颈——即使本次运行的 SLO 达标也不例外。
步骤 5 — 与基线运行进行比较 每次运行都应与上一次运行进行比较。即使 p99 延迟仍在 SLO 范围内,出现 10% 的回退也值得调查。
回退分类:
| 变化 | 分类 | 操作 |
|---|---|---|
| p99 与上一次运行的差异在 5% 以内 | 绿色 — 无回退 | 无需操作 |
| p99 比上一次运行差 5–15% | 黄色 — 观察 | 在下次发布前调查 |
| p99 比上一次运行差 >15% | 红色 — 回退 | 阻止发布,创建工单 |
| 错误率较上一次运行升高 | 红色 — 回退 | 阻止发布 |
| 超出 SLO 阈值 | 严重 | 阻止发布,通知值班人员 |
9. CI 集成
将负载测试作为发布流水线中的门禁步骤。每个发布候选版本都运行基线场景;每周运行所有场景。
# Example: GitHub Actions step (adapt for your CI platform)
load-test:
runs-on: ubuntu-latest
needs: [deploy-staging]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Install k6
run: |
curl -s https://dl.k6.io/key.gpg | sudo apt-key add -
echo "deb 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
- name: Seed test data
run: [seed command]
- name: Run baseline load test
run: |
k6 run \
--env BASE_URL=${{ secrets.LOAD_TEST_ENV_URL }} \
--out json=results.json \
scripts/load_test.js
env:
LOAD_TEST_ENV_URL: ${{ secrets.LOAD_TEST_ENV_URL }}
- name: Check thresholds
run: |
# k6 exits with non-zero if any threshold fails — this step fails the build
echo "k6 threshold check complete"
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: load-test-results-${{ github.run_id }}
path: results.json
- name: Cleanup test data
if: always()
run: [cleanup command]
CI 门禁摘要:
- 每次发布到预发布环境时运行基线场景
- 按计划每周运行完整场景套件(压力、突发、长时间运行)
- 任何阈值失败都会阻止晋级到生产环境
- 归档结果以进行趋势分析
质量检查
- 至少有一个测试场景覆盖所有关键端点——没有未经测试的生产端点
- 阈值基于实际 SLO 目标,而非猜测
- 测试数据植入已脚本化且可复现——测试不依赖预先存在的环境状态
- 负载生成器运行在与被测服务分离的基础设施上
- CI 集成会在阈值失败时阻止晋级,而不只是记录结果
- 至少运行过一次长时间运行测试,以建立内存和连接池基线
- 分析内容包括与上一次运行的结果比较,而不只是绝对的通过/失败判定
反模式
- 不要在缺乏实际 SLO 目标或生产基线依据的情况下设置阈值——随意设定的数字会导致毫无意义的通过/失败结果
- 不要在与被测服务相同的主机上运行负载生成器——这会同时污染测试结果和服务指标
- 不要在负载测试数据植入中使用生产用户数据——所有测试数据都必须是合成数据、带有标记,并在每次运行后清理
- 首次部署时不要跳过长时间运行测试——只有这类测试才能发现短时测试无法捕捉的缓慢内存泄漏和连接池耗尽问题
- 不要将基线测试通过视为服务能够处理突发负载的证据——基线、压力、突发和长时间运行场景测试的是本质上不同的故障模式