SLI的数学心脏:分位数背后的采样魔法
说实话,大多数人用SLI,只知道取个比率,比如成功请求数除以总请求数。这太浅了。真正的难题在延迟类SLI——P99、P95这些分位数。为什么?因为无法记录所有请求,只能用直方图采样,然后估算分位数值。Prometheus的histogram_quantile函数,背后是分段线性插值。原理不复杂:你预定义一堆桶边界,像{0.1, 0.5, 1, 2, 5, 10}秒,系统把每个请求落入的桶的计数累加。计算P99时,函数找到累积计数刚好超过99%的那个桶,然后假设桶内数据均匀分布,线性映射出具体的延迟值。但这里有个魔鬼细节——桶边界必须覆盖尾延迟。若最高桶只有10秒,而真实有12秒的请求,P99会被硬生生压到10秒以内,严重失真。

被平均骗了:一个P99打败99.9%可用性的真实压测
多讲个翻车故事。某次大促前压测,一个核心API,成功率SLI高达99.93%,可用性指标完美无瑕。但用户投诉“卡顿、转圈”。看平均延迟——20毫秒,稳稳的。团队百思不得其解。我多了个心眼,拉出P99一瞧:2.1秒!原来,有0.7%的请求因为数据库慢查询,延迟爆发。平均值的欺诈性在这里暴露得淋漓尽致:平均数被大量快请求拉低,慢请求藏得无影无踪。而可用性SLI只关心HTTP 200,压根不管耗时。那次之后,我强制所有关键服务的SLI,必须同时包含成功率和延迟P99,并且延迟SLO要基于时间窗口的达标率,比如“95%的5分钟窗口内P99 < 200ms”。
- 传统方案:只监控平均延迟+成功率。结果:平均延迟35ms,成功率99.92%,一切绿灯。
- 真实体验:P99在2.3~3.8秒间剧烈波动。错误预算按每5分钟消耗计算,仅3分钟就耗光了全天的预算。
- 引入P99 SLI后:立刻暴露出连接池泄露的问题——WiredTiger引擎的锁争用。修复后,P99稳定在85ms,错误预算充足。
三个让我半夜惊醒的落地陷阱
陷阱一:指标维度爆炸。 你想精细,给SLI贴上各种标签:接口名、HTTP方法、返回码、集群、Pod名……结果Prometheus内存疯涨,存储成本暴增。有一次,我的一张表突然多出200万个时间序列,就因为一个开发小哥加了个没有限制的user_id标签。解决方案简单但痛苦:限制标签基数,只保留报警和排障必要的最粗粒度维度。比如,线上SLI只保留“服务名+接口前缀”,更细的排查用分布式追踪,不在指标层做。经验证,标签数减少90%后,查询速度提升了4倍。
陷阱二:固定滑动窗口引发的误报风暴。 很多团队用5分钟固定窗口计算SLI,然后告警。但流量波谷时段,窗口内样本稀疏,一个慢请求就能把P99打上天,触发误报。我吃过这个亏——凌晨3点被叫醒,查了一圈健康得不行。后来改进为基于速率的动态窗口:当请求速率低于阈值时,自动扩大窗口,保证窗口内有足够样本量(比如至少1000个请求)。Prometheus的avg_over_time配合rate可以实现大致效果,但最好在采集端前置聚合。另一个角度,直接用错误预算燃尽率做趋势告警,比基于固定窗口的单点误报柔和得多。
陷阱三:服务依赖下的SLI失真。 一个服务自身的SLI完美,但因为它调用的下游超时,导致它的延迟SLI劣化。这时候如果你只看自己的SLI,会觉得很冤——明明代码没变。可用户才不管底层,他只会觉得你的服务慢。解决之道:在边界处明确合约,将依赖的SLO转化为内部错误预算的一部分。比如,下游API承诺P99 ≤ 50ms,那你在这层就假设超出50ms的请求为内部错误,据此调整自己的错误预算。同时,对下游的真超时错误单独计数,以便快速归因。我们落地时,直接让上游在SLI计算时,把下游返回的超时错误码视为“依赖方不可用”,不计入自身的错误预算,但发出依赖告警。这样既明晰责任,又避免无谓的自我处罚。

作者|大讲堂
排版|大讲堂
审核|知知
大讲堂