Prometheus 内存暴涨到 20G?从 tsdb status 定位高基数指标并降基止血

举报
yd_243555368 发表于 2026/11/10 00:25:47 2026/11/10
【摘要】 Prometheus 三天涨到 20G 被 OOMKilled,用 /api/v1/status/tsdb 摊开 numSeries 与 label 取值分布,定位到 path/error 高基数标签;给出 delete_series 清存量、metric_relabel_configs 防增量的完整配置,以及埋点侧三条硬规则、sample_limit 兜底与五个实战坑

现象: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,很可能改了三轮都没碰到真正爆的那个标签。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。