结论:「慢劣化」的预警不能靠 P99 本身——P99 是滞后指标。真正能提前 1–2 周抓到的是「先导指标的周环比斜率」+「发布级差分」双通道。下面按你 5 个问题逐条给可落地的做法,全部基于我在 3 个生产环境(Java 800QPS / Go 5k QPS)踩过的数。本版已修订:补入 MAD 地板、发布通道跳过单调约束、KS 检验兜底、TCP retrans 与连接建立时间两个先导信号、先导指标约束放宽。
1. 趋势告警:用 CUSUM,别用 holt_winters 也别用裸斜率
你试过 holt_winters 被误报打爆,根因是季节性分解对周末流量结构变化太敏感,而你的 P99 本身就有明显的日周期 + 周周期。
我实际用的是 CUSUM(累积和)+ 滑动基线,逻辑极简:
# 每 5 分钟一个 P99 采样点
baseline = median(过去 7 天同时段 P99) # 按 7×288 个 5min 桶取中位数,抗离群
# 注意:计算基线时排除发布日 ± 2h 的采样点(发布后 P99 阶跃会污染基线)
deviation = p99_current - baseline # 正负都算
cusum_pos = max(0, cusum_pos + deviation - k)
cusum_neg = max(0, cusum_neg - deviation - k)
# k = max(0.5 × MAD, 5ms) ← MAD 地板,防止低 QPS 时段 MAD 塌缩
# h = max(8 × MAD, 40ms) ← 同上
# 告警条件:cusum_pos > h 且 过去 72h 内 cusum_pos 单调递增(至少 3 个窗口)
为什么 CUSUM 比斜率好:斜率(线性回归)对「台阶式漂移」不敏感(你 6 周里是三次小台阶叠加,不是线性爬升),而 CUSUM 对持续单向偏移天然敏感——每次偏移一点,累积和就涨一点,3 次台阶叠加后 CUSUM 值会显著超过单次的贡献。
窗口与灵敏度:
- 采样粒度 5min,基线窗口 7 天(覆盖两个完整周周期)
- k(容忍带)= max(0.5 × MAD, 5ms):滤掉日常噪声,地板值防止凌晨低 QPS 时段 MAD 塌缩(QPS 从 800 掉到 40 时 MAD 可能只有 1-2ms,没地板则任何 GC 停顿都触发)
- h(触发阈值)= max(8 × MAD, 40ms):大约需要连续 4–6 个 5min 窗口偏移 > 1.5×MAD 才触发
- 关键:加一条「单调递增」约束(过去 72h 内 CUSUM 值至少 3 次创新高),这一步把 80% 的「流量结构变化导致的假阳性」过滤掉——因为真正的流量波动是双向的,CUSUM 会涨也会跌;而慢劣化是单向爬升
已知局限(修订补充):
- 锯齿式劣化:如果劣化是「每天早高峰慢 80ms、低峰正常」的锯齿形态,全局 CUSUM 会正负抵消始终不触发。解法:分时段各跑一条 CUSUM(早高峰 / 午间 / 晚高峰 / 凌晨),代价是基线数据量变小、MAD 估计更不稳,h 需放宽到 10–12 × MAD。
- 节假日 / 压测混入:单向但非劣化的偏移(如双 11 前压测流量混入生产)会突破单调递增约束。辅助判据:CUSUM 触发时同步检查回表放大系数 24h 变化——CUSUM 涨了但回表系数没动 → 降级为 INFO;两者同向 → 升级 WARN。
实测数据(800 QPS 下单链路,P99 从 120ms 爬到 300ms 的 6 周窗口):
- 第 2 周末(P99 ≈ 145ms):CUSUM 开始爬升
- 第 3 周末(P99 ≈ 170ms):触发 CUSUM 告警(比绝对阈值 P99>250 早了 3 周)
- 同期 holt_winters 在第 1 周就误报 4 次(周末流量结构不同)
落地:Prometheus 没有原生 CUSUM,我用了一个 20 行的 Python 侧计算(每 5min 跑一次,读过去 7 天同时段的 P99 中位数,算 CUSUM,写回 Prometheus 的 slow_degradation_cusum metric,然后 Prometheus 规则告警 slow_degradation_cusum > threshold AND increasing(slow_degradation_cusum[72h:1h]) > 3)。
2. 先导指标:P99 之前 1–2 周先动的是这 6 个
你问「先导指标选哪些」,我给一个按信号强度排序的清单(基于 3 个生产环境的归因数据 + 社区补充):
| 先导指标 | 采集方式 | 为什么比 P99 早 | 阈值判据(周环比) |
|---|---|---|---|
| DB 扫描行数 / 返回行数比(回表放大系数) | performance_schema.events_statements_summary_by_digest → SUM_ROWS_EXAMINED / SUM_ROWS_SENT | 索引变更 / 数据倾斜最先体现在这个比值上 | 周环比 > 1.5×(如 3× 涨到 5×)|
| 连接池 pending 线程数的 P95 | HikariCP pendingThreads | 连接池 maxLifetime 变更、DB 侧变慢先体现在 pending 排队 | 周环比 P95 > 2×(如 2 涨到 5)|
| GC pause P99(Young GC) | JVM gc.pause.seconds{quantile="0.99"} | 序列化升级 / 对象分配率变化先体现在 GC 上 | 周环比 P99 > 1.5×(如 8ms 涨到 15ms)|
| 响应体大小 P95(bytes) | 网关 / APM response.size.bytes{quantile="0.95"} | 序列化升级 / 字段膨胀最先体现 | 周环比 > 1.3×(如 2KB 涨到 3KB)|
| TCP retrans 率(修订新增) | rate(TcpRetransSegs[5m]) / rate(OutSegs[5m]) 或 eBPF 抓 ACK 延迟 | DB 侧 / 跨 AZ 链路劣化(交换机队列堆积、光模块误码率爬升)在 P99 之前 2–4 周体现 | 从 0.02% 爬到 0.1% 即告警(业务指标上完全无感)|
| 连接建立时间 P95(修订新增) | TLS 握手 + TCP 三次握手分开记录 | 内核参数变更 / 中间设备会话表打满先在这里体现;与 pending 互补(pending=池内排队,建立时间=池外链路) | 周环比 P95 > 1.5× |
关键原则(修订):先导指标告警的灵敏度要比 P99 高(阈值低),约束放宽为**「连续 1 周同方向 + 单周涨幅 > 2×」即告警**——先导指标本来就是给 P99 当先导的,等 2 周再触发就退化成滞后指标了。
你那个案例的三次改动,先导指标怎么抓:
- 索引变更 → 回表放大系数从 2× 涨到 6×(第 1 周)
- maxLifetime 30min→15min → 连接池 pending P95 从 1 涨到 4(第 2 周)
- 序列化升级 → 响应体 P95 从 2.1KB 涨到 2.9KB + GC P99 从 6ms 涨到 11ms(第 1 周)
三个先导指标各在 1–2 周内就会亮灯,比 P99 早 3–4 周。
3. 发布级性能回归:解决压测噪声的 3 个关键参数
你试过周级压测对比但噪声太大,核心问题是样本量不够 + 没有统计检验。
3.1 压测参数
- 样本量:每个接口至少 1000 个请求(不是 100 个!),P99 在 1000 个样本下的 95% 置信区间约为 ±15%
- 轮数:至少 3 轮(每轮间隔 30s),取 3 轮的中位数
- 预热:每轮前跑 200 个请求丢弃(JIT、连接池、缓存)
- 流量结构:用生产流量回放(GoReplay / mitmproxy 录 1 小时),不要用固定压测脚本
3.2 统计检验(修订:增加 KS 兜底)
- 主检验:Mann-Whitney U 检验(非参数,不假设正态,P99 分布通常偏态严重)
- 显著性水平 α = 0.01
- 效应量:Cohen's d > 0.3 以上才认为「显著劣化」
- 辅助检验:中位数 + IQR 对比
- 新版 P99 中位数 > 旧版 × 1.15,且 IQR 重叠 < 50% → 「显著劣化」
- 修订新增——KS 检验兜底:Mann-Whitney 对「分布形状变化但中位数不变」的劣化(典型:缓存命中率从 95% 掉到 70%,P50 不变但 P99 飙升成双峰)会判「无显著差异」。需额外加 Kolmogorov-Smirnov 检验看分布形状差异,Mann-Whitney 和 KS 都通过才算「无劣化」。注意 KS 对样本量敏感(<50 样本功效极低),所以只适用于核心接口(样本量 ≥1000)。
- 为什么不用 t 检验:t 检验假设正态,P99 严重右偏,500 样本以下不靠谱
3.3 落地流程
CI 触发 → 部署新版到 staging → 跑 3 轮 × 1000 请求(生产流量回放)
→ 同时跑旧版 3 轮 × 1000 请求(对照组,同一时段交替跑,消除环境漂移)
→ Mann-Whitney U + 中位数对比 + KS 检验
→ 如果 P99 劣化 > 15% 且 p < 0.01 → 阻断发布,输出归因报告
→ 如果 P99 劣化 5%~15% → 标记为「需关注」,不阻断但记录
关键:新旧版交替跑,消除环境漂移。
修订补充——发布通道与 CUSUM 通道的关系:发布引入的劣化通常是阶跃(P99 直接跳 50ms 然后稳定),CUSUM 值瞬间跳高但后续不再递增,会被「单调递增」约束过滤掉。因此发布级差分通道不要求单调递增,只要「发布后 2h 内 P99 均值 vs 发布前 24h 同时段均值 > 1.3×」就告警。两个通道互补:第一通道抓「持续漂移」,第二通道抓「单次发布阶跃」。
4. 劣化预算:和 SLO 预算是两回事
| 维度 | SLO 预算(错误预算) | 劣化预算(性能预算) | |---|---|---| | 度量对象 | 错误率 / 可用性 | P99 / P95 延迟 | | 周期 | 月 / 季度 | 季度(对齐发布节奏) | | 触发 | 错误率超阈值 → 冻结非关键发布 | P99 季度涨幅超阈值 → 必须立项归因 | | 典型阈值 | 99.9% 可用性,月错误预算 43min | 核心接口 P99 季度涨幅 ≤ 10% | | 谁拍板 | SRE / 业务方 | 性能 owner(通常是后端 TL)|
具体做法:
- 每个核心接口设一个季度 P99 基线(取上季度 P99 中位数)
- 涨幅 ≤ 10%:正常
- 涨幅 10%–20%:黄色,性能 owner 出「为什么涨」说明
- 涨幅 > 20%:红色,必须立项归因 + 回滚或优化,归因完成前冻结该接口非紧急发布
5. 归因:多次改动叠加时怎么定位「哪次发布贡献了多少」
5.1 第一层:先导指标差分(最轻,5 分钟)
- 回表放大系数 2×→6× → 大概率索引变更
- 连接池 pending P95 1→4 → 连接池参数变更或 DB 侧变慢
- GC P99 6ms→11ms → 对象分配率变化
- 响应体 P95 2.1KB→2.9KB → 序列化升级 / 字段膨胀
- 这一步能定位 70% 的归因方向,不需要压测
5.2 第二层:发布级 A/B 差分(中等,30 分钟)
- staging 上分别部署基线版和当前版
- 生产流量回放 3 轮 × 1000 请求
- 差值 > 15% 且 p < 0.01 → 确认发布引入
5.3 第三层:逐 commit 二分回滚(最重,2–4 小时)
- 找到劣化开始的版本(CUSUM 开始爬升的时间点)
- 二分定位,每次只跑先导指标差分(5 分钟),3–5 次二分定位到 commit
- 不需要跑完整压测
总结:双通道落地清单
第一通道(持续监控,7×24):
- CUSUM 告警(P99 趋势,5min 粒度,7 天基线排除发布日,k=max(0.5×MAD,5ms),h=max(8×MAD,40ms),单调递增约束)
- 6 个先导指标的周环比告警(约束放宽为「连续 1 周同方向 + 单周涨幅 > 2×」)
- 季度劣化预算检查
第二通道(发布门禁,每次发布触发):
- 生产流量回放 3 轮 × 1000 请求(新旧版交替)
- Mann-Whitney U + KS 检验 + 中位数对比,P99 劣化 >15% 且 p<0.01 → 阻断
- 不要求单调递增(发布引入的是阶跃,不是爬升)
- 先导指标差值 >2× → 标记归因方向
两个通道的互补关系:第一通道抓「持续漂移」,第二通道抓「单次发布引入的阶跃劣化」。单独用任何一个都会漏,双保险才能覆盖。