Prometheus 内存暴涨到 20G?从 tsdb status 定位高基数指标并降基止血
现象:Prometheus 内存三天涨到 20G,重启完又慢慢涨回去
一个中等规模的 K8s 集群,节点 60 台,Pod 约 1800 个。Prometheus 单实例,之前稳定在 6~8G 内存,某次版本发布后开始爬坡:三天到 20G,被 OOMKilled 重启,重启后一个小时 5G,一天 12G,三天又是 20G。规律性极强,说明不是内存泄漏,而是数据规模真的在膨胀。
Prometheus 的内存大头只有三块:head block 里的活跃 series、索引与标签索引、以及采集与查询时的临时对象。前两块直接与 series 基数成正比。内存涨 = series 涨,几乎可以当作第一定律。
第一步:用 tsdb status 把基数摊开看,而不是猜
别急着翻配置。Prometheus 自带一个诊断接口,直接吐出当前 series 总量与 Top 标签值分布:
curl -s http://prometheus:9090/api/v1/status/tsdb | jq '.'
关键字段有三个:
headStats.numSeries:当前 head block 的活跃 series 数,这是内存的第一驱动量;seriesCountByMetricName:按指标名排序的 series 数量,直接指出哪个指标在膨胀;labelValueCountByLabelName:哪个 label 的值种类最多,这是"高基数标签"的指纹。
本次输出里 numSeries 约 420 万,而正常水平应在 80 万左右。前几名指标清一色是 http_request_duration_seconds_bucket、http_requests_total、nginx_upstream_response_msecs_histogram_bucket,三者合计占了 70% 的 series。
再往下看 label 维度,path、uri、error、request_id 四个标签的取值数量都在 10 万量级。到这里结论已经很清楚:有人把 URL 路径、错误信息甚至请求 ID 当成了 label 值。
复现验证:算一下理论 series 数,对得上就定罪
不要停在"看起来像"。取单个指标,把各 label 的取值数相乘,就能算出理论基数。以一个业务指标为例:
# 单个指标的 series 数
count(count by (__name__, instance, method, path, status) (
http_requests_total
))
更实用的是直接按 label 拼接统计,看哪个组合爆炸:
# 每个 metric 的 series 数 Top10
topk(10, count by (__name__) ({__name__=~".+"}))
# path 标签的取值数量
count(count by (path) (http_requests_total))
实测 path 有 12.8 万个不同取值,原因是 RESTful 路由里的用户 ID、订单号直接进了 path:/api/v1/orders/10086/detail、/api/v1/orders/10087/detail……每个订单号一条独立时间线。status × method × path × instance 一相乘,单个指标就能顶掉几百万 series。
另一个常见元凶是 error 标签直接放异常 message:dial tcp 10.2.3.4:6379: connect: connection refused,一旦网络抖动,每个不同 IP、不同端口的报错都是一条新时间线,几分钟就能刷出几十万 series。
止血:先删数据,再改配置,顺序不能反
很多人第一反应是改 relabel 配置重启 Prometheus,结果发现内存没降——因为历史 series 还在 TSDB 里,查询与索引依然占内存。正确顺序是先清理,再防增量。
1. 用 delete_series 清掉已经写进去的垃圾
先开启管理接口(默认关闭):
prometheus --web.enable-admin-api
然后按标签删除,注意这个操作是软删除,标记删除位,后台 compaction 后才真正释放磁盘与索引:
curl -X POST -g 'http://prometheus:9090/api/v1/admin/tsdb/delete_series?match[]=http_requests_total{path=~"/api/v1/orders/[0-9]+/detail"}'
curl -X POST 'http://prometheus:9090/api/v1/admin/tsdb/clean_tombstones'
如果垃圾面太大,更省事的做法是直接把整个指标删掉,让它按新规则重新采集:
curl -X POST -g 'http://prometheus:9090/api/v1/admin/tsdb/delete_series?match[]={__name__="nginx_upstream_response_msecs_histogram_bucket"}'
2. metric_relabel_configs 做降基,别用 target 级 relabel
关键区别:relabel_configs 作用于 target 发现阶段,那时还没有指标;metric_relabel_configs 作用于每个样本写入前,是降基的正确位置。
scrape_configs:
- job_name: business-api
metrics_path: /metrics
static_configs:
- targets: ['api-svc:9100']
metric_relabel_configs:
# 1) 直接丢弃高基数指标
- source_labels: [__name__]
regex: 'request_id|trace_id|_debug_.*'
action: drop
# 2) 把带 ID 的 path 归一化成模板
- source_labels: [path]
regex: '/api/v1/orders/[0-9]+/detail'
target_label: path
replacement: '/api/v1/orders/:id/detail'
# 3) 兜底:把超长 path 统一截断
- source_labels: [path]
regex: '(.{40}).+'
target_label: path
replacement: '$1'
# 4) error 标签只保留错误类型,不保留完整 message
- source_labels: [error]
regex: '(timeout|refused|reset|EOF|other).*'
target_label: error
replacement: '$1'
注意 relabel 规则是顺序执行的,第一条 drop 之后,后面的规则就看不到这个样本了;把 drop 放最前面能省 CPU。
3. 业务侧把 label 做成有限枚举,这是根本解
relabel 是止血带,真正的解法是在埋点处就不产生高基数。三条硬规则:
- 路径必须模板化:Web 框架的路由模板(如
/orders/:id)就是天然的低基数 label,用实际 URL 是错误做法; - 错误信息不进 label:label 只放错误类别,详细信息交给日志或 exemplar;
- 任何可能无限的维度都不做 label:用户 ID、请求 ID、时间戳、IP、session、文件路径,凡是取值集合无法穷举的,一律禁止。
顺带一提 Prometheus 自己的保护机制:当单个 target 的 series 超过 sample_limit,它会直接拒绝采集并上报 up=0 与 scrape_samples_post_metric_relabeling。把这个阈值设上(比如 5 万)比事后救火强得多:
scrape_configs:
- job_name: business-api
sample_limit: 50000
验证:三个数字同时降下来才算真的成功
改完配置重启,不要只看内存曲线(有缓存滞后),要看这三个数:
# 1) 活跃 series 总量,应该看到断崖式下跌
curl -s http://prometheus:9090/api/v1/status/tsdb | jq '.data.headStats.numSeries'
# 2) 各 target 实际采集量,确认业务指标没被误伤
scrape_samples_post_metric_relabeling
# 3) 采集耗时,series 减少后 scrape_duration_seconds 应同步下降
scrape_duration_seconds{job="business-api"}
本次结果:numSeries 从 420 万降到 92 万,Prometheus 常驻内存从 20G 回落到 6.5G,scrape_duration_seconds 由 4.2s 降到 0.8s,查询 P99 也顺带从 3s 降到 400ms。同时确认核心业务指标的采集量没有明显下降——降的是噪声,不是信号。
几条踩过的坑
- delete_series 是软删除:删完不
clean_tombstones,磁盘不释放;而且被删的 series 若采集还在继续,下一个 scrape 又写回来了,所以必须先改 relabel 再删,否则白删。 - relabel 的 regex 默认是整串匹配:写
/api/v1/orders/[0-9]+匹配不上/api/v1/orders/123,必须写全或者用(.*)兜住。这是最常见的"配了没生效"。 - histogram 的 bucket 也是 series:一个 histogram 有 N 个 bucket 就是 N 倍 series,
_bucket上再挂高基数 label,爆炸速度是 counter 的好几倍。降基时优先处理 histogram。 - recording rules 不能降基数:很多人想用聚合规则减少内存,但 recording rule 是额外生成新 series,原始数据依然在。它只能加速查询,不解决基数问题。
- 远程写/多副本会放大:如果开了 remote write 双写,一份高基数数据会被放大两倍存储与内存开销,评估时要一起算进去。
小结
Prometheus 内存问题的排查路径其实很线性:tsdb status 看 numSeries → seriesCountByMetricName 找指标 → labelValueCountByLabelName 找标签 → count by 验证理论基数 → delete_series 清存量 → metric_relabel_configs 防增量 → 埋点侧根治。整个链条里唯一不能省的一步是"先算理论基数再动手",因为凭感觉改 relabel,很可能改了三轮都没碰到真正爆的那个标签。
- 点赞
- 收藏
- 关注作者
评论(0)