8.3 全链路性能门禁
上一节:8.2 CI 微基准门禁 | 下一节:8.4 生产观测:SLI 与错误预算 配套代码:08-continuous-performance/03-e2e-perf-gate
一句话结论
全链路门禁成本高、噪声大,所以它不该出现在每个 PR 上。 正确的定位是:发布前的最后一道验收——用专用环境、固定机器、触发式运行。
一、用「飞机试飞」理解全链路门禁
汽车出厂前会做路试,但不会每装一个零件就上路试一次。路试的时机是:
| 时机 | 目的 |
|---|---|
| 每个零件装好后 | 台架试验(对应微基准) |
| 整车装配完成 | 转鼓试验(对应集成基准) |
| 出厂前 | 路试(对应全链路门禁) |
| 上市后 | 用户反馈 + 定期检查(对应生产告警) |
全链路门禁的定位就是「出厂前路试」——不是每次都做,但发布前必做。
二、什么时候跑全链路门禁
| 触发时机 | 是否合适 | 理由 |
|---|---|---|
| 每个 PR | ❌ 不合适 | 成本太高(几十分钟),噪声大,会拖慢开发 |
| 主干合并后 | ⚠️ 可以 | 比 PR 好,但仍是高频 |
| 发布候选(RC) | ✅ 推荐 | 发布前的最后一道门 |
| 每日夜间 | ✅ 推荐 | 与发布解耦,能发现趋势 |
| 手动触发 | ✅ 推荐 | 用于验证重大改动 |
推荐组合:
① 主干合并后:跑【微基准门禁】(分钟级)
② 每日夜间:跑【全链路回归】(发现趋势退化)
③ 发布候选:跑【全链路门禁 + 容量复测】(发布前验收)
④ 重大架构改动:手动触发
三、全链路门禁的三个必要条件
条件一:专用环境
不能在共享的 CI runner 上跑全链路压测。
| 需求 | 说明 |
|---|---|
| 固定规格的机器 | 不能今天 4 核明天 8 核 |
| 与被测服务隔离的压测客户端 | 避免抢 CPU |
| 固定的数据库/中间件 | 版本、配置、数据量都要固定 |
| 无其他任务干扰 | 独占环境 |
如果没有专用环境:不要做全链路门禁——用定期的手动压测(第 3 章)代替,并明确记录"数据不可用于容量规划"。
条件二:固定的数据集
数据集要求:
① 数据量固定(每次跑之前重置到同一状态)
② 分布固定(用同一份幂律分布的数据)
③ 缓存状态固定(每次清空或预热到同一程度)
④ 写操作有隔离(避免数据累积导致每次不一样)
为什么必须固定:否则你测的不是代码的性能变化,而是数据的变化。
❌ 第 1 次跑:100 万行 → P99 = 95ms
第 2 次跑:130 万行(上次压测写的)→ P99 = 120ms
→ 你以为是代码退化,其实是数据涨了
条件三:相对基线 + 宽松阈值
与 CI 微基准门禁同样的纪律,但阈值要更宽(因为全链路的噪声更大):
| 指标 | 微基准的阈值 | 全链路的阈值 |
|---|---|---|
| P50 | 噪声 × 2 | 噪声 × 2 |
| P99 | 噪声 × 2 | 噪声 × 2~3(尾部波动更大) |
| QPS | 噪声 × 2 | 噪声 × 2 |
| 错误率 | 绝对阈值(如 0.1%) | 绝对阈值 |
四、门禁脚本的骨架
#!/usr/bin/env bash
# tools/e2e-perf-gate.sh <VERSION_TAG>
#
# 全链路性能门禁:发布候选阶段运行。
set -euo pipefail
VERSION="${1:?usage: e2e-perf-gate.sh <version_tag>}"
EXP_DIR="docs/experiments/gate-${VERSION}"
mkdir -p "$EXP_DIR/results"
echo "═══ 全链路性能门禁:$VERSION ═══"
echo
# ① 环境检查(必须通过)
echo "【1/6】环境检查"
tools/check-environment.sh "gate-${VERSION}"
if grep -q "❌" "$EXP_DIR/results/env-check.txt"; then
echo "❌ 环境检查未通过 —— 本次门禁结果不可信,终止"
exit 1
fi
echo
# ② 重置数据集(保证每次一致)
echo "【2/6】重置数据集"
scripts/reset-dataset.sh
scripts/restart-app.sh "$EXP_DIR/results/gc.log"
sleep 10
echo
# ③ 预热(不计入统计)
echo "【3/6】预热 60 秒"
BASE_URL="$TARGET_URL" k6 run --quiet --vus 20 --duration 60s \
loadtest/profile-constant.js > /dev/null
echo
# ④ 正式测量(跑 3 轮,取中位数)
echo "【4/6】测量 3 轮"
for i in 1 2 3; do
BASE_URL="$TARGET_URL" RATE=500 DURATION=5m \
k6 run --summary-export="$EXP_DIR/results/round-$i.json" \
loadtest/profile-constant.js > /dev/null
echo " 第 $i 轮完成"
done
echo
# ⑤ 与基线对比
echo "【5/6】与基线对比"
python3 tools/compare_e2e.py "$EXP_DIR/results" perf/e2e-baseline.json
RESULT=$?
echo
# ⑥ 输出结论
echo "【6/6】结论"
if [ $RESULT -eq 0 ]; then
echo "✅ 性能门禁通过"
echo
echo "后续:"
echo " ① 把本次结果存为新的候选基线"
echo " ② 继续发布流程"
else
echo "❌ 性能门禁失败"
echo
echo "后续:"
echo " ① 查看 $EXP_DIR/results/ 下的详细数据"
echo " ② 判断是真退化还是噪声(第 8.2 节的排查流程)"
echo " ③ 【不要】在未验证的情况下更新基线"
exit 1
fi
五、全链路门禁要检查的四件事
| 检查项 | 阈值 | 为什么 |
|---|---|---|
| SLO 达标 | P99 < 200 ms、错误率 < 0.1% | 发布前的核心验收 |
| 拐点未后移 | 拐点 QPS ≥ 基线 × 0.9 | 检测容量退化 |
| 饱和度正常 | 同等负载下 pending = 0、队列深度接近 0 | 检测"看起来达标但已接近极限" |
| 资源无异常 | 内存/连接数无单调上升(跑 30 分钟) | 检测泄漏 |
第三项最容易被忽略:
❌ 只看 SLO:P99 = 190ms < 200ms ✅ 通过
但连接池 pending 从基线的 0 涨到了 8
→ 系统已经在排队,只是还没反映到延迟上
→ 一旦流量再涨 10%,就会崩
✅ 同时看饱和度:pending > 0 → 拒绝通过(或标记为警告)
这就是第 1 章「饱和度是领先指标」在门禁上的应用。
六、如果门禁失败:决策树
门禁失败
│
├─ ① 检查是不是环境问题
│ ├─ 环境检查有没有 ❌?
│ ├─ 数据集是否重置成功?
│ └─ 是否在预期的时间窗口内跑(避开备份等)?
│
├─ ② 判断幅度
│ ├─ P99 变化 < 噪声底线 → 可能是噪声,重跑 1 次
│ ├─ P99 变化 1~2 倍噪声 → 需要查清(用第 6 章的方法定位)
│ └─ P99 变化 > 2 倍噪声 → 高度怀疑真退化
│
├─ ③ 定位(用第 6 章的分析流程)
│ ├─ 延迟分解:哪一段变慢了?
│ ├─ 依赖指标:是数据库/缓存/下游?
│ └─ 火焰图:有没有新的热点?
│
└─ ④ 决策
├─ 真退化 → 阻塞发布,修复后重跑
├─ 预期变化(如新增功能带来合理开销)→ 更新基线(单独提交 + 说明)
└─ 环境噪声 → 重跑;记录到误报日志
关键:「预期变化」也需要走更新基线的流程——不能因为"知道原因"就直接改基线,因为这个变化对容量的影响需要评估。
七、一个现实的落地建议
如果你们还没有专用环境:
❌ 不要做:在开发机/共享 CI 上跑全链路门禁
→ 数据不可信,会不断误报,最终被关闭
✅ 应该做:
① 先建微基准门禁(成本低、见效快)
② 用「发布前手动压测」代替自动门禁(第 3 章的方法)
- 每次发布前跑一次
- 结果记录进实验档案
- 与上次发布的基线对比
③ 等有了专用环境,再把手动流程自动化
手动流程也是「持续化」——它在机制上不如自动门禁,但比"什么都不做"好得多。关键是每次发布都做,且有记录可对比。
八、本节小结
- 全链路门禁不该出现在每个 PR 上——成本高、噪声大。
- 正确的触发时机:发布候选、每日夜间、手动触发。
- 三个必要条件:专用环境(固定规格、隔离客户端)、固定数据集(重置到同一状态)、相对基线 + 更宽的阈值。
- 门禁要检查四件事:SLO 达标、拐点未后移、饱和度正常、资源无泄漏。
- 「SLO 达标但 pending > 0」应该拒绝通过——这是领先指标的应用。
- 失败后的决策树:查环境 → 判幅度 → 定位 → 决策(修复 / 更新基线 / 重跑)。
- 没有专用环境时:用「发布前手动压测 + 记录对比」代替,不要在共享环境上跑自动门禁。
九、自测
- 一个团队把全链路压测放进了每个 PR 的 CI 里。请说出三个可能的问题,以及更合适的做法。
- 门禁显示 P99 = 190 ms(阈值 200 ms)通过,但连接池 pending 从基线的 0 涨到了 8。请评价这个结果,以及你会怎么处理。
- 门禁失败,P99 从 95 ms 涨到 125 ms(噪声底线是 13%)。请按决策树说明你的处理步骤。
- 三个问题:① 拖慢开发——全链路压测要几十分钟,每个 PR 都跑会让 CI 排队、反馈变慢,开发者会开始绕过它;② 数据不可信——PR 的 CI runner 是共享的、规格不固定、有邻居干扰,测出的差异大部分是环境噪声,会导致频繁误报;③ 成本高——需要完整环境与数据集准备,每个 PR 都做不经济;④ 第四个问题:数据集无法固定——多 PR 并行跑会互相污染数据库状态。更合适的做法:① 每个 PR 只跑微基准门禁(分钟级);② 主干合并后或每日夜间跑全链路回归;③ 发布候选阶段跑全链路门禁(用专用环境);④ 重大架构改动时手动触发。
- 应该拒绝通过(或至少标记为强制复查)。理由:① SLO 达标不等于系统健康——P99 = 190 ms 只是"还没反映到延迟上";②
pending > 0是领先指标——说明请求已经在排队等连接,系统的余量正在被耗尽(第 0 章 0.5 节的排队非线性:利用率从 80% 涨到 90%,延迟会非线性上升);③ 风险:一旦流量再涨 10% 或某个查询稍慢,延迟就会突然恶化(不是渐变)。处理方式:① 阻塞发布(或标记为高风险),因为这是一个明确的容量退化信号;② 按第 6.5 节排查:是查询变慢了(占用连接更久)还是并发上来了;③ 如果是预期的(比如新增了功能),要单独评估容量影响并更新基线 + 调整容量规划;④ 修复后重跑门禁,确认 pending 回到 0。 - 处理步骤(P99 从 95 → 125 ms,涨幅 32%,是噪声底线 13% 的 2.5 倍):① 检查环境——先看
env-check.txt有没有 ❌(数据集是否重置成功、机器规格是否一致、是否有其他任务);② 判断幅度——32% > 2 × 13%,高度怀疑是真退化;③ 定位(用第 6 章的流程)——(a) 延迟分解:哪一段变慢了(用第 6.2 节的三点减法);(b) 依赖指标:是数据库(查pg_stat_statements的calls与mean)、缓存(命中率)、还是下游;(c) 火焰图:有没有新的热点(对比基线的参考火焰图);(d) 查这次版本改了什么(用git log对比上一个基线版本);④ 决策——如果确认是真退化,阻塞发布并修复;如果是预期变化(比如新增了必要的功能开销),评估容量影响后再单独提交更新基线;如果最终确认是环境噪声(需要多次重跑验证),记录到误报日志并考虑调整阈值。