导语:笔者穷尽毕生绝学写就此文,通过剖析最典型的“怪现象”,解答 “Prometheus 指标值为何不准”这一灵魂拷问。
雷畅
腾讯高级工程师,目前主要负责腾讯云可观测系统的设计与研发。
引子
有一天,你打算试用 Prometheus,监控你的业务系统。
你来到腾讯云,仅需几次点击,指标便从四面八方来,汇聚成 Grafana 上的优雅曲线。
“不愧是云原生监控一哥,容器集成十分顺滑!交给云托管则更省心,连云产品也一键监控了。”
手握丰富的 Grafana 大盘,和适时的告警通知,你深感满意。
然而,没过多久,你发现不对劲:
——“32 核的 CPU,监控出来是 44.3 核?”
——“P99 百分位的值,竟比最大值还高?”
——”用不同时间范围计算 rate,出来的曲线天壤之别?“
——……
你搜索全网、询问 ChatGPT 老师、向腾讯云提工单,得到的答复干脆而统一:“Prometheus 的指标值,并非 100% 准确”。
你刨根问底,却碰上“外推”、“插值”、“窗口对齐”……等晦涩的概念。于是,你跳过解题步骤,直接背诵了答案:“Prometheus 的指标值,并非 100% 准确”。
然而,一些灵魂拷问在你脑中浮现:
——既然大家都知道它不准,为何人人还都安利它?
——现在我也知道它不准了,还值得继续用下去吗?
以上内容,纯属虚构;如有雷同,那必然是关于 Prometheus 的“谜团”太多,而“解谜”太少。
而本文正打算以一种不烧脑、能秒懂的方式,分析一些最常见的案例。
概述
长话短说,结论先行:Prometheus 指标值不准的“怪现象”,其实是在下面的“不可能三角”中,做出了取舍——为保全效率和可用性,舍弃了精度:
为何精度会被 Prometheus 舍弃?
归根结底,在可观测的世界里,metrics(指标)、log(日志)、trace(调用链) 三足鼎立。
而 Prometheus 所在的 metrics(指标) 领域,其目标是诊断整体健康状况,其手段通常是对原始数据先采样、再聚合,利用有限的信息,分析变化趋势;而并非像 log(日志)那样,翔实精确、事无巨细地,记录每一桩事件、每一条原始数据。
我们不妨用心脏监测来做类比:
如此看来,运动手表监测心率虽不精确,但胜在方便高效:不用跑到医院,就能 24 小时持续监控,还能自行设置告警阈值。在日常观测健康趋势方面,已然十分够用了。
除了 metrics 领域自身的特性,Prometheus 毕竟处在一个条件有限的真实世界,它还要随时面临以下困难:
Prometheus 需要在上述限制下,交出 not perfect、但是 good enough 的指标。于是就有了下述设计:
接下来,让我们观察几种最常见的案例,代入 Prometheus 的第一视角,体会它是如何在条件有限中,做出抉择的。
案例
失真的 rate/increase
在使用 rate 或者 increase 观测 counter 类型的指标增量时,经常碰到以下问题:
而一种最常见的原因,就是线性外推(linear extrapolation)。
简单粗暴解释:rate/increase[时间范围] 在计算该时间范围内的增量时,第一步要拿到该时间范围边界上(开始时刻和结束时刻)的样本点,相减得到差值。
然而事与愿违的是:在当前时间范围的边界,并不一定那么凑巧地有样本点存在。
此时 Prometheus 的选择是:naive 地假设所有样本点在该时间范围内是均匀分布的,然后按照这个均匀分布的线性规律,“脑补”估算出边界上的采样点。
那么,既然是 “脑补”,“补”出来一些不准确的值,也就不足为奇了。
假设有一个 counter 类型的指标 errors_total
,用于监控业务系统报错的次数。Prometheus 以 15 秒的间隔采样,采集到了如下样本:
现在需要计算一分钟之内,errors_total 值的增量,也即 increase(errors_total[1m])
。(此处为方便起见,仅以 increase 为例。而 rate 本质上是一样的,只是将 increase 在 [时间范围] 内的总增量除以 [时间范围] 的秒数,得到了速率/按秒增量。例如本例中 rate 值就是 increase 值除以 60 秒)。
要计算 [1m] 的时间范围/取样窗口内的 increase,在最理想的情况下,Prometheus 根本不想关心这个窗口内的其他数据,而只需从窗口左边界取第一个点,右边界取最后一个点,相减即可:
然而在真实的世界中,[1m] 窗口的左右边界却很少能精准“踩中”样本点,而是像下图这样:
那么问题来了:这 1 分钟的增量该怎么算呢?
Prometheus 选择了一种简易的线性外推算法:取窗口覆盖范围内的第一个点和最后一个点,计算斜率,并按照该斜率将直线延伸至窗口边界,无中生有地“脑补”出虚拟的两个“样本点”,即可相减计算 increase 了:
如上所示,用绿圈圈所代表的“虚拟样本”相减,得到的 increase 1.3 不仅是个小数,还比实际值偏大,也就不足为奇了。
离谱的 histogram
每当采集到的样本与 Prometheus 八字不合,P99 往往好似在告诉我们:“在全人类当中,99% 的人月收入少于一万亿。”——没毛病,但也大可不必。
此处就不得不提一个真实历史事件了:我们团队除了有腾讯云 Prometheus,还有个宝藏产品叫 PTS 云压测,能以海量并发向服务端发起请求,来观测服务端在压力下的响应状况。
比如,在压测出来的报告里,与响应时间相关的图表长这样:
可以看到,PTS 搜集了响应时间的平均值、P50、P90、P95——但就是没有 P99。
其实,最早的时候是有的——毕竟,谁不想用 P99 来做 SLA 啊。
但是,云压测背后的指标存取,还是用的 Prometheus。
于是,在 PTS 还拥有 P99 的那些年,我们三番五次、屡屡破防,最终忍痛拿掉了 P99:
histogram 百分位(percentile)不准,这是为啥呢?这就不得不提线性插值(linear interpolation) 了。
下面以 P99 为例说明(其他百分位也不一定准,但 P99 经常离最大的谱)。
首先,搬运 ChatGPT 老师对 P99 的概念介绍:
P99 是一个统计术语,代表着第99百分位数(99th percentile)。在性能监控和服务质量评估中,P99 常用来衡量响应时间或延迟的指标。具体来说,P99 的含义是在所有测量值中,有 99% 的数据点小于或等于这个值,而只有 1% 的数据点大于这个值。
例如,如果一个网络服务的响应时间的 P99 是 200 毫秒,这意味着在所有的请求中,99% 的请求的响应时间都不会超过 200 毫秒,只有 1% 的请求的响应时间会超过这个数值。这是一个衡量系统在高负载下性能的重要指标,因为它可以告诉你绝大多数用户的体验如何。
简单理解 P99 是怎么得来的:把样本按值的大小依序排队,队伍里第 99% 个样本的值,就是 P99。
那么 Prometheus 在用 histogram 计算 P99 的时候,是否要保存全部哪怕一亿个请求样本的耗时值,才能知道第 99% 的请求所用的时间呢?
显然这不是 Prometheus 的风格。Prometheus 的风格是:宁愿“脑补”,也不愿低效。
于是,跟上面 rate/increase 类似:先从茫茫多的原始数据中采样出样本点,放到各个 bucket(桶)里;然后 naive 地假设所有样本是均匀分布的,据此做线性插值,“无中生有”出所需的“样本点”。
让我们看一个简单案例,模拟每秒产生一个新的 HTTP 请求耗时的观察值,然后计算其 P99。
下面程序为了埋点生成 http_response_time_seconds
这一 histogram 指标,每秒钟暴露一个观察值:
package main
import (
"math/rand"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_response_time_seconds",
Help: "HTTP response time distribution",
Buckets: []float64{0.1, 0.5, 100}, // 划分四个桶: <= 0.1、<=0.5、<=100、<=正无穷
})
)
func init() {
prometheus.MustRegister(httpDuration)
}
func main() {
go func() {
for {
// 每秒添加一个新的观察值,是个随机数
// 其值大小有 50% 概率落在 [0.1, 0.5);50% 概率落在 [0.5, 1)
if rand.Float64() < 0.5 {
httpDuration.Observe(rand.Float64()*0.4 + 0.1)
} else {
httpDuration.Observe(rand.Float64()*0.5 + 0.5)
}
time.Sleep(time.Second)
}
}()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}
为了卡 Prometheus 的 feature(bug),我们这里划分桶的时候,是相当 naive、相当不合理的:划分四个桶: <= 0.1、<=0.5、<=100、<=正无穷。(对一群不超过 1 的针尖大小的样本值,特地划分一个 0.5 ~ 100 这样宽如黄浦江的 bucket 段,笔者也真是没安好心……)
histogram 的 http_response_time_seconds_bucket
指标是 counter
型,会统计采样到的观察值的样本,落在各个桶内的数量。
还记得我们代码逻辑把一半数值落在 [0.1, 0.5),一半在 [0.5, 1.0) 吗?程序运行一段时间后,将所有样本按值的大小依序排开,观察它们在各个桶内的分布,大致示意如下:
好了,现在要开始计算 P99 histogram_quantile(0.99, rate(http_response_time_seconds_bucket[5m]))
了。
假设现在总共采集到 100 个样本,其中:
将样本值从小到大排列,落在 0.1~0.5 bucket 段里的,我们叫它第 1号 ~ 50 号样本;落在 0.5~100 bucket 段里的,我们叫它第 51 号 ~ 第 100号样本。
P99 的计算逻辑如下:
最终计算分位值公式时,问题就来了。
Prometheus 只知道:
然而,它并不知道:
again,Prometheus “脑补” bucket 段内的全额差值,被均匀、线性地,分到了所有样本头上。
因为这样就又可以用线性插值的方式,来计算分位值了:
所求分位值 = bucket 段左边界值 + (bucket 段右边界值 - bucket 段左边界值) * (目标样本在本 bucket 段的排行 / 本 bucket 段的样本总数)
。
抛开桶不桶、样本不样本的不谈,在我们的例子中,从目标 bucket 段内求目标样本值的问题,就被简化成了下图所示的形式,直接梦回中学数学课堂,给它一个解:
也即:P99 = 0.5 + (100 - 0.5) * (49/50) = 98.01
。
给一群不超过 1 的值算出来接近 100 的 P99,其根因也就在于 Prometheus 的“脑补”,与我的桶划分和样本分布,八字不合。
如下图所示:上面的实心绿点代表一群值不超过 1 的真实样本,而由于桶的划分不太合理,导致 Prometheus 线性插值“脑补”出下面那群荧光绿圈,与实际分布偏差很大,最终估算出的 P99 值高达 98+,也就不足为奇了。
由此可以看出,若想用 histogram 获得较为准确的分位值,则需对样本分布有一定的了解,再根据这个分布,设置合理的 bucket 边界。(PS:若对分位值有较高精度要求、又不了解样本分布、对性能开销和聚合灵活度要求不高,则可考虑使用 summary 代替 histogram。)
薛定谔的 range
当我们选择 rate 的 range 时,我们在选择什么?
仍以上述 rate(errors_total[时间范围])
为例,若我们分别选时间范围 [30s]、[1m]、[5m],看一眼三者的 Grafana 图表,这不能说一模一样,只能说是毫不相关:随着时间范围扩大,主打一个逐渐平滑、失去尖峰……
有一说一,rate 不就是速率,速率不就是每秒增量吗?为啥时间范围窗口不同,差异如此之大?区别在于它们计算平均速率的时间窗口不同:
rate[30s]
计算过去 30 秒内的平均速率。rate[1m]
计算过去 1 分钟内的平均速率。rate[5m]
计算过去 5 分钟内的平均速率。上面这段废话,其实大有深意:同一个尖峰,以不同的形式“被平均”了。
假设我们系统的错误数长期为0,而在某时刻暴增100(如下图日志所示)。
那么上述三种时间范围窗口,意味着将这 100 均分到 30秒,还是 60 秒,还是300秒;那么答案也显而易见:分母越大,按秒平均后的增量则越平滑。
而这就是上述三个 Grafana 曲线随 rate 窗口而峰值和形态大变的原因:
关于 rate duration 的选择,并没有一成不变的规则,它并不是越小越好。
选择较小的时间范围可以让你更快地发现问题,但也可能会让你的图表出现很多噪音,特别是在高变化的指标上。
相反,较大的时间范围可以提供更平滑的数据视图,但可能会延迟发现问题。
所以,在选择合适的时间范围时,应考虑以下因素:
最终,选择合适的时间范围需根据具体情况,进行实验和调整,不断优化这个参数。
结语
以上列举的最典型案例,旨在解释为何 Prometheus 会出现指标值不准的“怪现象”,以及探究它背后的设计原理。
为了言简意赅、去粗存精地解说,上述论述仍然处于最核心、但也经过了简化的场景。
而 Prometheus 在实际使用中的情形,是影响因素更多、也更为复杂的。聊举几例:
如此种种,挂一漏万,难以尽述。
总而言之,本文聚焦在最常见、最核心的场景,解析为何 Prometheus 的值不准——而它真的是一个 feature,不是一个 bug。
而围绕着 Prometheus 宇宙,当然有更复杂、更深入的问题亟待探讨。
欲知后事如何,欢迎强势关注腾讯云可观测公众号,以及试用腾讯云可观测平台的 Prometheus 产品。它不仅提供 Prometheus 的原生能力,还能与可观测平台的告警能力强强联合,大大减少您的开发及运维成本。
更多详情欢迎大家微信关注,不仅能限时免费体验产品,还能加入交流群与大佬一起探讨技术。让我们携手不断升级中的腾讯云 Prometheus,一起 stay hungry, stay foolish;持续发掘、持续求索。
联系我们
如有任何疑问,欢迎扫码进入官方交流群~
关于腾讯云可观测平台
腾讯云可观测平台(Tencent Cloud Observability Platform,TCOP)基于指标、链路、日志、事件的全类型监控数据,结合强大的可视化和告警能力,为您提供一体化监控解决方案。满足您全链路、端到端的统一监控诉求,提高运维排障效率,为业务的健康和稳定保驾护航。功能模块有:
Prometheus 相关文章推荐: