Prometheus 指标采集性能损耗:每秒万次采集拖慢应用?采样率调整+Pushgateway 缓冲!

公司在做压测的时候发现一个奇怪的现象:QPS 跑到 8000 的时候,接口响应时间有个规律性的毛刺——每隔 15 秒,P99 延迟就会跳一下,从 50ms 飙到 200ms,持续一两秒又恢复正常。查了半天,根因是 Prometheus 来拉指标了。JVM 要遍历所有指标、序列化成文本、再通过网络发出去,这一整套操作每次都要几十毫秒。平时感觉不到,高并发下就成了定时炸弹。

Prometheus 好用,但它的 Pull 模式有个固有的问题:每次抓取,应用程序都要做一遍"遍历指标 → 格式化 → 输出"的全流程。 指标越多、频率越高,这个开销越不可忽略。


为什么 Pull 模式在高频采集下会出问题

Prometheus 默认每 15 秒拉一次指标。对于大多数服务来说,这个频率没啥问题——15 秒一次,每次几十毫秒,感知不到。

但如果你把采集频率调成了 5 秒、甚至 1 秒,或者你的应用里有大量自定义指标(几百个 Counter/Histogram),情况就不一样了。

每次采集,JVM 里发生的事情是:

Prometheus 发 GET /metrics 请求
  │
  ├─ 遍历所有注册的指标(几百个)
  │    ├─ Counter → 读当前值
  │    ├─ Histogram → 遍历所有 bucket
  │    └─ Gauge → 读当前值
  │
  ├─ 格式化成 Prometheus 文本格式
  │
  └─ 序列化输出(几百 KB 的文本)

这个流程本身不复杂,但它跑在业务线程上。也就是说,Prometheus 来拉指标的时候,处理这个 HTTP 请求的线程就是你的 Tomcat/Jetty 工作线程。指标越多、响应体越大,这个线程被占用的时间就越长。高并发下,这个线程本可以去处理用户请求的,结果在给别人数数。


方案一:采样率调整 —— 不用每次都全量采集

Prometheus 的 scrape_interval 是控制采集频率的。但这个参数只有一个值,所有指标一视同仁。问题在于,不是所有指标都需要那么高的精度。

  • 业务指标(订单量、支付成功率):5 秒一次不过分
  • JVM 指标(GC 次数、堆内存使用):30 秒一次足够了
  • 中间件指标(线程池活跃数、连接池使用率):60 秒一次都行

如果用同一个频率采集所有指标,就等于用最贵的成本收集了不太需要高频更新的数据。

解决思路:把指标分成两组,不同频率采集。

Prometheus 本身支持多 scrape_config,可以给不同路径配不同频率:

# prometheus.yml
scrape_configs:
  # 业务核心指标:高频采集
  - job_name: 'app_business'
    scrape_interval: 5s
    metrics_path: '/actuator/prometheus/business'

  # JVM + 基础设施指标:低频采集
  - job_name: 'app_infra'
    scrape_interval: 60s
    metrics_path: '/actuator/prometheus/infra'

应用侧通过 Micrometer 的 MeterFilter 把指标分流到不同的端点:

// 业务指标 → /actuator/prometheus/business
// JVM/中间件指标 → /actuator/prometheus/infra

这样 Prometheus 该高频的照样高频,该低频的每一分钟才来一次。实际效果是:原本每 5 秒一次的 500 个指标全量输出,变成了 5 秒一次只输出 100 个业务指标 + 60 秒一次输出 400 个基础指标。总开销大幅下降。


方案二:换 Push 模式 —— 让应用主动推,不用等 Prometheus 来拉

Pull 模式的问题出在"被动"。Prometheus 什么时候来拉,应用就什么时候干活——高峰期拉,高峰期卡。

Push 模式则反过来:应用自己决定什么时候推指标,推到 Pushgateway。Prometheus 从 Pushgateway 拉,不直接接触应用。

Pull 模式(问题):
  Prometheus ──拉──→ 应用(高峰期,工作线程被占用)

Push 模式(解决):
  应用 ──推──→ Pushgateway ←──拉── Prometheus
                 ↑
          业务线程不参与

Pushgateway 在这里充当了一个缓冲层。应用只需要把指标推过去就行,序列化、格式化这些开销完全在 Pushgateway 身上,不占用应用的业务线程。

但 Pushgateway 的 Push 模式也有代价。Prometheus 官方文档里明确写了:Pushgateway 不能做高可用,重启就会丢数据。 而且 Push 模式天然不适合生命周期短的指标——比如一个 Pod 挂了,它推上去的指标会一直残留在 Pushgateway 里。

所以 Push 模式适合什么?批处理任务、短生命周期任务、不方便被 Prometheus 拉取的场景。 对于常规的长期运行服务,Pull 模式依然是推荐方案。Pushgateway 是补充,不是替代。


方案三:聚合采样 —— 牺牲精度换性能

如果你的指标数量本身就很大(几百个 Histogram,每个 Histogram 十几个 bucket),即使不用高频采集,单次输出的序列化耗时也很可观。

这时候可以考虑客户端聚合:在应用内部把多个数据点合并成一个,减少输出量。

原始指标(每次请求都打一个数据点):
  http_request_duration_seconds_bucket{le="0.01"} 1523
  http_request_duration_seconds_bucket{le="0.05"} 3412
  ...(几十个 label 组合 × 十几个 bucket)

聚合后(10 秒合并一次):
  http_request_duration_seconds_bucket{le="0.01"} 15230
  http_request_duration_seconds_bucket{le="0.05"} 34120

思路就是:不要把每一个请求的耗时都作为独立的数据点记录下来,而是在内存里先攒着,攒够一个时间窗口(比如 10 秒)再一次性输出。数据点数量大幅减少,Prometheus 抓取时遍历的指标数量也跟着减少。

Micrometer 默认就有基于 StepMeterRegistry 的步进聚合机制,大多数指标已经是聚合过了的。如果你用的是自定义指标,注意不要在高频路径上直接调 registry.counter().increment() 而不做任何聚合。


实战:分级采集 + Pushgateway 混合方案

把上面的内容串起来,一个中等规模的落地方案长这样:

指标分层:

第一层:高优先级业务指标(5s 采集)
  - 接口 QPS、错误率、支付成功率
  - 走的 Pull 模式(直接暴露 /actuator/prometheus/business)
  - 指标数量少,开销可控

第二层:基础设施指标(60s 采集)
  - JVM GC、内存、线程
  - 连接池、线程池
  - 走的 Pull 模式(/actuator/prometheus/infra)
  - 指标数量多,但频率低

第三层:批处理任务指标(按需推)
  - 定时任务执行结果、数据同步进度
  - 走的 Push 模式(推到 Pushgateway)
  - 任务跑完推一次,不占常规采集资源

在 Micrometer 侧做指标分组的伪代码:

// 业务指标 → 高频端点
MeterFilter.denyUnless(id → id.startsWith("http.") || id.startsWith("business."))

// 基础设施指标 → 低频端点
MeterFilter.denyUnless(id → id.startsWith("jvm.") || id.startsWith("hikaricp."))

不需要大动干戈改架构,核心就是用 MeterFilter 把指标分成两组,在 Prometheus 侧用两个 scrape_config 区分频率。效果立竿见影。


几个容易忽略的细节

Histogram 的 bucket 数量是隐形杀手

一个 HTTP 请求耗时的 Histogram,如果你设了 20 个 bucket,又按 4 个接口 + 3 个状态码做 label 组合,那就是 20 × 4 × 3 = 240 个时间序列。

一个服务如果有 10 个这样的 Histogram,光序列化输出就能到几 MB。别设太多 bucket,也别设太多 label 组合。 能用 summary 就别用 histogram,除非你真的需要分位数聚合。

/metrics 端点记得做访问控制

/actuator/prometheus 暴露的是内部指标,没必要让公网能访问。至少加一层内网限制或者 Basic Auth。不然别人不仅能看你业务的 QPS,还能反推你的流量模型。

别在 hot path 上做字符串拼接

有人会在 Counter 的标签里做字符串拼接:

counter("orders", "channel", "wechat_" + subChannel).increment()

每次 increment() 都会产生一个新的字符串对象。高并发下这就是大量的临时对象,GC 压力会很明显。标签值尽量提前定义成常量,不要动态拼接。


总结

Prometheus 采集优化的核心不是去跟 Prometheus 较劲,而是管好你自己暴露出去的指标。

三个方向:

降低频率——不是所有指标都需要 5 秒一次。JVM 指标 60 秒一次完全够用,用多 scrape_config 做分级采集。

减少数量——斩掉不必要的 bucket、砍掉多余的 label 组合、指标分组输出。每次少发一点数据,Prometheus 少做一点序列化。

模式切换——短生命周期的批处理任务走 Pushgateway,别让它们在 Pull 模式的端点上占坑。

这几件事做完,那个每 15 秒一次的 P99 毛刺,基本就消失了。


有用的话转给还在 1 秒采集一次全量指标的运维。


标题:Prometheus 指标采集性能损耗:每秒万次采集拖慢应用?采样率调整+Pushgateway 缓冲!
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/05/1780412989995.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消