【背景】 我平时负责指标体系搭建,上周排查一次 P99 告警,发现一个很尴尬的口径问题:监控大盘上这个接口过去 1 小时的 P99 是 210ms,但我拿原始埋点日志重算,同样的时间窗 P99 是 480ms。
差异的根源是我们链路的存储设计:为了省存储,系统先按 1 分钟一个桶,把每个桶内的 P50 / P99 / count 算好落库,大盘展示时再把 60 个桶「合并」出一个小时级的 P99。问题就出在这里——百分位不是可加性指标:
- 第 1 分钟:99 个样本全在 100ms 附近
- 第 2 分钟:30 个样本全在 500ms 附近
- 单看每个桶的 P99 都不高,但合并后全局 P99 应该被那 30 个慢样本顶上去
- 用「桶内 P99 的加权平均」去近似,结果会系统性偏低,而且分布越不均匀、桶内样本越少,偏差越大
这不是个例,是我后来翻监控代码发现好几条链路都是这么算的。当时告警判「无异常」,事后看其实有真实的毛刺,只是被聚合口径吃掉了。
【常见分歧 / 几种做法】
- 原始派:百分位必须在全量原始数据上算,落库只存原始值,代价是存储和查询成本;
- sketch 派:用 t-digest / DDSketch 这类可合并的近似结构,既省空间又保留跨桶合并的正确性,误差可控(通常 <1%);
- 预聚合派:直接存桶内 P50/P99,接受误差,理由是「大盘只看趋势,趋势没看错就行」;
- 换指标派:干脆别报 P99,看 max + 慢请求条数(count where rt > 阈值),口径简单且可加。
【我的倾向与理由】 我倾向「sketch 派 + 口径显式化」,理由有三:
- 数据模型上把百分位当成一等公民存储,本身就是错的。我的原则是:落库层只允许存原始值、count、sum、sketch 这类「可合并原子」,P99 这类派生值只允许在查询时现算。
- sketch 是工程上最划算的折中:DDSketch 的相对误差是可控且可证明的(配置相对误差 1% 时,P99 的偏差基本可忽略),存储比原始值小一个量级,且天然支持跨桶 / 跨 pod 合并——这点在多副本服务上是刚需,预聚合方案跨机合并时误差会进一步放大。
- 口径要显式:大盘上每个百分位旁边应该同时显示「该时间窗的样本数」。样本数 < 30 的 P99 我认为基本没有统计意义,应该降权展示甚至不展示,否则很容易拿一个 5 个样本的 P99 去做决策。
另外补充一个容易被忽略的点:跨实例合并。4 副本服务,每实例每分钟 100 个样本,如果每实例先算 P99 再取平均,和全局 P99 是两回事。我们内部后来统一成「全局 P99 = 所有实例样本合并后计算」,这条规则写进了指标规范。
【想听大家在什么场景下怎么选】
- 你们的百分位指标是原始数据算、sketch 算,还是预聚合?用的什么系统(Prometheus / VictoriaMetrics / 自研)?
- 你们有没有遇到过「大盘 P99 和日志重算 P99 对不上」的情况?差距多大、最后信了哪个?
- 样本量小到什么程度你们会开始怀疑 P99 这个数字本身?
- 对于「非可加性指标」(percentile、去重计数、比率),你们的指标体系有没有明确的分层规则?还是都靠口头约定?
欢迎拍砖,尤其是踩过「预聚合口径坑」的同学,想看下大家的真实误差量级。
0赞
0收藏
0楼层