结论先行:双峰分布下,对全量算 P90 本身就是错误的统计姿势。 P90 恰好落在两个峰之间的"谷底",对峰间比例极度敏感——35% 的"坏峰"只要波动 ±5pp(比如移动端占比临时升高),P90 就会跨越你的 3.5s 阈值,这就是你 43 次误报的根因。
正确思路:把"单指标绝对阈值"换成"分段 + 占比漂移 + 联动条件"三层判定。 下面给一套我实际跑过的组合(B 端 12 万 UV 场景,和你量级一致),附可复现配置。
一、分段策略:先消除双峰干扰
用 CDN 命中率(或 LCP < 2s 作为代理)把会话切成两个子群:
| 子群 | 判定条件 | 占比(你的数据) | |------|----------|----------------| | A 群(好) | LCP < 2s 且 CDN hit=true | ~65% | | B 群(差) | LCP ≥ 2s 或 CDN hit=false | ~35% |
分别对两个子群设阈值,而不是对全量算分位数:
- A 群:P75 > 1.8s 持续 15 min → 告警(A 群本身应该非常快,P75 飙到 1.8s 说明"好路径"也出问题了)
- B 群:P50 > 5.0s 持续 10 min → 告警(B 群波动大,用中位数而非 P90,阈值放宽到 5s 避免移动端低端机噪声)
为什么 A 群用 P75 而非 P90:A 群只有 65% 流量,P90 样本太少且离群点敏感;P75 在 12 万 UV × 65% ≈ 7.8 万/天的量级下统计稳定。 为什么 B 群用 P50 而非 P90:B 群是"本来就慢"的群体,P90 会落在 6-7s,阈值形同虚设;P50 代表"一半人的体验",对故障更敏感。
二、占比漂移检测:抓"结构性变化"
这是你漏报 CDN 节点故障和误报"移动端占比升高"的关键:
指标:bad_ratio = B 群会话数 / 总会话数
- 基线:取过去 7 天同时段的
bad_ratio均值(注意"同时段"——早高峰和深夜的基线不同) - 告警条件:
bad_ratio > baseline × 1.3(即比基线高 30%)且持续 20 min - 为什么 30%:你的 35% 基线下,30% 漂移意味着从 35% → 45.5%,这通常对应 CDN 大面积回源或网络层故障,不会由"移动端占比临时升高"触发(那种情况 bad_ratio 会涨到 38-40%,不触发)
这层解决你"加了错误率联动还是漏了纯 LCP 劣化"的问题——不需要错误率,bad_ratio 本身就是"纯 LCP 劣化"的代理指标。
三、联动条件:降低误报但不引入漏报
不要"AND"错误率(你已经验证过会漏),改为 AND 以下任一:
- CDN 命中率:
cdn_hit_rate < 7d_baseline - 10pp(绝对值下降 10 个百分点) - TTFB P75:
ttfb_p75 > 2s(服务端响应慢会推高 LCP,但此时问题不在前端) - bad_ratio 自身(上面第二层)
即:告警 = (A群 P75 超阈值 OR B群 P50 超阈值) AND (CDN 命中率骤降 OR TTFB 超标 OR bad_ratio 漂移)
这样"某地区弱网"会触发 B 群 P50 超标,但 CDN 命中率和 TTFB 都正常、bad_ratio 没漂移 → 不告警(降级为 warning 打日志即可)。 "CDN 节点故障"会同时触发 B 群超标 + CDN 命中率骤降 → 告警。 "纯前端资源加载劣化"(如 Vite 打包体积突然变大)会触发 A 群 P75 超标 + TTFB 正常 + CDN 命中率正常 + bad_ratio 漂移 → 告警。
四、可复现配置(Grafana + RUM 数据源)
假设你的 RUM 数据落到 ClickHouse/PostgreSQL,以下是一个 Grafana Alert 规则的 YAML 片段(适配 Grafana 9+ 统一告警):
apiVersion: 1
groups:
- orgId: 1
name: web-vitals-lcp
folder: frontend
interval: 1m
contactPointIds:
- oncall-frontend
rules:
# 规则1: B群 P50 劣化 + 联动条件
- uid: lcp-b-p50
title: "LCP B-group P50 > 5s (sustained 10m)"
condition: C
data:
- refId: A
queryType: query
datasourceUid: <your-rum-ds>
model:
rawSql: |
SELECT approx_quantile(lcp_ms, 0.50) AS p50
FROM rum_web_vitals
WHERE ts > now() - INTERVAL 10 MINUTE
AND (lcp_ms >= 2000 OR cdn_hit = 0)
interval: 1m
maxDataPoints: 10
reducer: last
- refId: B
queryType: query
datasourceUid: <your-rum-ds>
model:
rawSql: |
SELECT cdn_hit_rate
FROM rum_cdn_stats
WHERE ts > now() - INTERVAL 10 MINUTE
interval: 1m
maxDataPoints: 10
reducer: last
- refId: C
queryType: threshold
model:
type: threshold
conditions:
- $A > 5000 AND $B < (
SELECT avg(cdn_hit_rate) FROM rum_cdn_stats
WHERE ts BETWEEN now() - INTERVAL 7 DAY AND now() - INTERVAL 1 DAY
) - 0.10
for: 10m
noDataState: OK
forDuration: 10m
labels:
severity: critical
segment: B-group
实际部署时建议把
baseline预计算成一张物化视图(每 5 分钟刷新一次 7 天同时段均值),避免 alert 查询里嵌套子查询拖慢 Grafana。
五、量化收益预估(基于你的数据推算)
| 指标 | 现状(P90>3.5s) | 改造后(分段+漂移+联动) | |------|-----------------|----------------------| | 月均告警次数 | ~15 次(47次/3月) | 预计 2-3 次(真正故障) | | 告警准确率 | 8.5% | 预计 70-80% | | 漏报风险 | CDN 故障漏报 1 次/3月 | bad_ratio 层兜底,预计 0 | | 配置复杂度 | 1 条规则 | 3 条规则 + 1 张 baseline 物化视图 |
维护成本增加约 0.5 人天(配 baseline 物化视图 + 分段 SQL),但告警疲劳基本消除。
六、一句话总结
双峰分布别用单一分位数——拆段、盯占比、加联动,三层各管一类故障,互不干扰。 核心指标从"LCP P90 是多少"变成"好路径还快不快 + 差路径占比有没有突变 + 基础设施(CDN/TTFB)有没有恶化",这才是 B 端双峰场景下的正确告警模型。
安全工程师视角补一个容易被忽视的风险面:「分段 + 占比漂移 + 联动」这套模型在逻辑上很优雅,但落地时有一个供应链层面的隐患——baseline 物化视图(7 天同时段均值)本身成了单点信任源。如果攻击者或上游 CDN 配置变更导致过去 7 天的 baseline 被污染(比如被批量刷了"好"数据),你的 bad_ratio 阈值就会被静默抬高,真正的故障反而被吞掉。建议 baseline 本身也要做「异常检测」:如果 7 天基线的 P95 突然偏离 30 天滑窗中位数超过 2 个 MAD,就冻结 baseline 更新并回退到更长窗口的兜底值。另外,bad_ratio 的 30% 漂移阈值在 B 端 12 万 UV 场景下合理,但如果你的 B 端客户中有大量"固定弱网"用户(比如工业平板、离线仓库),B 群基线本身就偏 50%+,30% 漂移意味着 50%→65%,此时可能只是某批新设备上线而非故障——建议对 B 群再做一层"设备/地域"子分段,把"结构性变化"和"真实劣化"分开。