指标采集的底层暗流:别被平均值骗了

你可能不知道,那个看似无害的 QPS 平均值,已经骗了你很多次。说实话,我第一次发现这个问题是在一个凌晨三点的 oncall 里——监控大屏一片绿,用户却在群里骂娘。那种懊恼感,至今记忆犹新。平均响应时间 200ms,P99 却干到了 4 秒。就因为这个,我决定把指标采集的底裤扒开看一看。

指标采集,远不是落个 SDK、抓个 endpoint 那么简单。它的核心挑战,发生在 写入路径存储结构 的深度纠缠中。

时序数据库的倒排索引:标签不是免费的

很多人把 Prometheus 当 MySQL 用,上来就恨不得给每个请求打上 user_id。然后,内存炸了。为什么?因为指标采集在底层不是一个简单的哈希表,而是一个 倒排索引 的暴力构建过程。

想象一下:每个指标名(比如 `http_requests_total`)加上一组键值对标签(`{method=”GET”, path=”/api”}`)会形成一个唯一的时间序列。当你为每个 user_id 加标签时,系列数瞬间膨胀到用户数乘以接口数。Prometheus 的内存消耗大头就在这里——它为每个标签组合维护独立的索引 posting list。

更致命的是 压缩。时序引擎(比如 Gorilla、Facebook 的内存时序数据库)使用了巧妙地 Delta-of-Delta 时间戳压缩和 XOR 值压缩。Gorilla 论文里说能把 16 字节的时间戳压到 1.37 字节。神了,对吧?但这一切的前提是同一序列内时间戳 间隔规律。如果你的采集间隔忽大忽小——比如因为网络抖动——压缩率会直接崩盘。我压测过:稳定 15s 间隔,压缩比能达到 12:1;引入 5% 的抖动,就掉到 4:1 以下。

所以工程美学在哪里?在 预先聚合。比如用 recording rules 把细粒度数据聚合成更粗的时间窗口,或者索性在客户端做局部聚合。这个取舍,直接决定你的存储成本和查询延迟。

Prometheus 倒排索引存储结构示意图
Prometheus 倒排索引存储结构示意图

Pull 模型的物理瓶颈:不是推就优雅

Prometheus 的 Pull 模型常被吹上天,但它的软肋在 百万级 endpoint 下暴露无遗。每次抓取都是 HTTP GET,挨个遍历 target。当 target 数量超过 1 万时,哪怕每个只花 10ms,光抓取一轮就要 100 秒。这还没算上网络抖动和丢包。所以你得拆分成多个 Prometheus 实例,再用 Thanos 或者 VictoriaMetrics 做全局查询。但拆分带来的难题是 跨实例聚合:sum(rate(requests_total[5m])) 这种查询,必须把数据从多个侧拉回查询节点计算,网络开销剧增。

我们做过对比测试:单实例 Prometheus 扛 50 万 active series 是极限(内存 30GB,抓取间隔 15s),换成 VictoriaMetrics 单实例能到 100 万,但写入放大更高。而如果换成 Push 模型(比如 InfluxDB 的老版本),写入端压力分散了,但容易丢数据或 double count。这是一场没有银弹的战争。Push 模型的 agent 需要处理反压,Pull 模型的中心需要做服务发现和分片。你问我选哪个?看你的拓扑——微服务网格下,Pull 的故障定位更直观;IoT 设备场景,Push 是唯一选择。

大规模 Prometheus 联邦集群采集架构图
大规模 Prometheus 联邦集群采集架构图

落地时的三个深坑

落地时的三个深坑
落地时的三个深坑

再扯点实在的。踩过这些坑,才算真正搞过指标采集。

坑一:高基数序列导致 OOM。前面提到了,但解决方案不是“少用标签”,那样太笼统。具体做法是:在 SDK 侧限制标签值域。比如对于 `customer_id` 这种,不要直接用原始值,而是哈希取模分桶(`hash(customer_id) % 1000`),把基数锁死在 1000 以内。或者用 exemplar 把 user id 挂在样本上,而非做成标签。我们线上这么搞,内存从 28G 降到 9G,P99 查询延迟从 3s 降到 200ms。这个数字记得很清楚。

坑二:时间戳回退导致数据丢失。时钟不同步是个老生常谈的问题,但 Prometheus 的处理特别鸡贼:如果新写入的点时间戳小于等于最新点,它会直接丢弃。这意味着你用了 NTP 同步但发生了微小回拨,那整批数据就丢了。解决办法不是不用 NTP,而是让 NTP 做平滑调整(`tinker panic 0`),或者在采集端加几秒缓冲输出,等时间戳单调递增再推送。VictoriaMetrics 更狠,它允许少量乱序写入,但需要额外配置 `-inmemoryDataFlushInterval` 并付出性能代价。

坑三:远端存储的读取延迟。很多人用 Thanos 或 Cortex 把数据 offload 到 S3,以为查询一样丝滑。太天真了。对象存储的随机读延迟是毫秒级,而时序查询往往是范围扫描。我们碰到过一个 case:查询过去 7 天的 P99 延迟,因为 block 分片过多,S3 LIST 操作就花了 8 秒。最终解决方案是重写查询逻辑,强制通过 Thanos Store 的缓存预取,并且把查询时间范围拆分成多个子查询并发执行,再在 PromQL 层聚合。查询时间从 30s 压到 5s 内。这就是 cache 策略 的胜利。

指标采集的真相就是:没有一个方案能一劳永逸。你得理解数据流在内存中的形状,在磁盘上的布局,在网络中的抖动。否则,你只是在调参,不是在搞架构。

免责声明:市场有风险,选择需谨慎!此文仅供参考,不作买卖依据。如有侵权请联系删除。
文章名称:指标采集的底层暗流:别被平均值骗了
文章链接:https://m.lfdjt.com/info_23_7645.html