3.1 高基数指标的隐性成本:抓取、存储与查询的连锁反应 我处理过一个真实的故障:一个 vLLM 推理集群,上线后吞吐始终不达标,GPU 利用率比预期低了 15%。排查了好几天,最后发现罪魁祸首竟然是监控本身——某个工程师为了"方便排查",在推理服务的 端点里暴露了一个带 标签的延迟指标,每个请求一个独立的 requestid。这个标签的基数是天文数字(每个请求一个新值),导致 Prometheus 抓取 时要处理上百万条 series,exporter 进程吃掉了 15% 的 CPU,而这些 CPU 本来该用来跑推理。 这是我第一次深刻理解"高基数指标是 Prometheus 头号隐形杀手"。
我处理过一个真实的故障:一个 vLLM 推理集群,上线后吞吐始终不达标,GPU 利用率比预期低了 15%。排查了好几天,最后发现罪魁祸首竟然是监控本身——某个工程师为了"方便排查",在推理服务的 /metrics 端点里暴露了一个带 request_id 标签的延迟指标,每个请求一个独立的 request_id。这个标签的基数是天文数字(每个请求一个新值),导致 Prometheus 抓取 /metrics 时要处理上百万条 series,exporter 进程吃掉了 15% 的 CPU,而这些 CPU 本来该用来跑推理。
这是我第一次深刻理解"高基数指标是 Prometheus 头号隐形杀手"。这一节就把高基数指标的隐性成本讲透——它怎么从抓取 CPU、到存储膨胀、再到查询变慢,形成连锁反应,最终把被监控系统本身拖垮。
先定义清楚"基数"(cardinality)。一个指标的基数,指它所有标签组合的去重数量。比如一个指标 http_requests_total{method, status},method 有 4 种值(GET/POST/PUT/DELETE),status 有 10 种值(各种 HTTP 状态码),那它的基数就是 4 × 10 = 40。这种基数是可控的。
但如果给这个指标加一个 user_id 标签,user_id 有 100 万种值,基数就变成 4 × 10 × 1000000 = 4000 万。这种指标叫高基数指标。Prometheus 的时间序列数据库,每一条独立的标签组合就是一条时间序列(series),每条 series 都要独立存储、独立抓取、独立参与查询聚合。基数一旦失控,series 数量爆炸,系统的三个环节连锁崩坏。
第一个崩的是抓取。每次 scrape,Prometheus 要解析目标暴露的所有 series。一个暴露 50 万 series 的 /metrics 端点,每次抓取可能消耗数百 MB 内存和可观的 CPU——而这些资源本来该留给业务本身(在我的那个案例里,是留给推理)。第二个崩的是存储。Prometheus 默认每个 sample 约 1 到 2 字节(压缩后),但 series 数量本身就是内存大头——每条 series 的标签集要常驻内存的倒排索引里。一个 200 万 series 的 Prometheus,光是 series 索引就能吃掉 10GB 以上内存,高峰期极易 OOM。第三个崩的是查询。promql 聚合(比如 sum by (model)(rate(...)))要扫描所有匹配的 series,基数越高扫描量越大,查询从毫秒级退化到几十秒甚至超时,Grafana 看板因此卡顿,告警因查询超时而漏报。
这三个环节是连锁的——抓取慢导致 scrape 超时,数据缺失;存储膨胀导致 OOM,Prometheus 重启丢失近期数据;查询变慢导致看板不可用、告警失效。一个高基数标签,就能引发这一连串灾难。
高基数几乎总是来自几类"危险标签",识别它们是治理的第一步。
用户级标签是头号嫌疑犯。user_id、session_id、request_id、trace_id,这些标签每个请求或每个用户一个新值,基数动辄百万起步。绝对不要让这类标签进 Prometheus,它们属于日志或分布式追踪系统(如 Loki、Tempo、Jaeger),不是时序数据库能扛的。
自由文本标签是第二类。prompt(用户输入)、error_message(错误信息)、url(带查询参数的完整 URL),这些标签的值域是无限的,必爆。一个 error_message 标签可能因为各种异常栈的不同组合,产生成千上万种值。
高基数控件是第三类。把 container_name、pod_name 配在短生命周期对象上,比如 K8s 自动扩缩容时 pod 名不断新生(每次部署生成新 pod 名),这些标签的值会无限增长。
自检的方法:Prometheus 自带几个有用的指标和查询。prometheus_tsdb_head_series 是当前 head 里的 series 总数,监控这个数字的趋势,突然飙升就是基数失控的信号。topk(10, count by (__name__)({__name__=~".+"})) 这个查询能找出哪类指标贡献了最多的 series,是定位高基数元凶的利器。我建议每个团队都把这两个查询加到监控看板里,作为基数治理的"雷达"。
不是所有指标都值得"全精度全量采集"。按价值/成本做分级是治理的核心。
黄金信号(QPS、延迟、错误、饱和)基数低、价值高,必须全量采集,这是监控的基础,不能省。分位数指标(histogram)基数中等(由 bucket 数控制)、价值高,也全量采集,但要控制 bucket 的数量——每个 histogram 的 bucket 不超过 10 到 15 个,否则基数也会膨胀。资源利用率(CPU、显存)基数低、价值中等,全量采集。
调试用的细粒度指标(per-request 的明细)基数高、价值低(只在排查问题时有用),不应该全量入库。要么只在客户端做聚合后暴露(比如只暴露 P50/P99 这些聚合值,不暴露每个请求),要么按需开启——平时关闭,排查时临时打开一个高基数端点,排查完关闭。
一次性排查用的指标(比如某个新功能的灰度监控)基数高、价值临时,更要节制。这类指标往往是为了回答一个具体问题("这个新功能上线后错误率高不高"),问题回答完就应该下掉,而不是永远留在 /metrics 里。我见过不少团队的历史指标越积越多,很多是早期排查留下的"考古层",没人记得为什么加,但它们还在持续贡献基数。
基数治理不是一次性的,是持续的工作。建议建立这样的流程。
第一,建立 series 数量的监控和告警。监控 prometheus_tsdb_head_series 的趋势,设一个阈值(比如单实例不超过 100 万 series),超过就告警,及时发现问题。第二,定期审计 /metrics 端点。用一个脚本拉取所有被监控目标的 /metrics,统计每个指标的 series 贡献量,找出异常的高基数指标。第三,标签评审流程。新增指标时要 review 它的标签,任何"看起来基数可能很高"的标签(用户级、自由文本)都要被拒绝。第四,调试指标的"临时性"纪律。任何为了排查而加的高基数指标,都要带一个"下线时间",到点自动移除。
回到开头那个案例,我们把那个带 request_id 的标签从 /metrics 里移除(改成写日志),exporter 的 CPU 占用立刻从 15% 降到 1% 以下,推理吞吐恢复到预期水平。一个标签的治理,换回了 15% 的 GPU 算力——这就是基数治理的直接收益。
高基数指标是 Prometheus 头号隐形杀手,引发"抓取 CPU → 存储膨胀 → 查询变慢 → OOM"的连锁反应,甚至拖垮被监控系统本身。危险标签(user_id、自由文本、短生命周期对象)绝不能进 Prometheus,它们属于日志和追踪系统。按"价值/成本"分级采集:黄金信号全采,调试指标降采样或按需。最后,基数治理是持续工作,要配套 series 监控告警和标签评审流程。
下一节 3.2 讲当单机 Prometheus 真的顶不住时,VictoriaMetrics 和 Mimir 这些替代方案到底该不该换。