4、指标采集体系:Prometheus架构详解、Exporter开发实战、ServiceMonitor与PodMonitor配置、指标采集性能优化

好,咱们进入指标采集这个核心环节。说实话,整个可观测性体系里,指标是最基础也是最关键的一环。没有可靠的指标采集,后面所有的告警、分析、可视化都是空中楼阁。今天我就把Prometheus这套东西掰开揉碎了讲清楚。

4.1 Prometheus架构:不只是拉取那么简单

很多人觉得Prometheus就是个时序数据库,其实它的架构设计很有讲究。我刚开始接触时也犯过迷糊,以为它跟Zabbix一样是推模式。后来踩了坑才明白,Prometheus的核心是拉取(Pull)模式

它的架构可以拆成几个关键组件:

  • Prometheus Server:负责拉取指标、存储数据、执行查询
  • Exporter:把业务或系统指标转换成Prometheus能理解的格式
  • Pushgateway:处理短生命周期任务的指标推送
  • Alertmanager:处理告警路由和去重
  • Service Discovery:动态发现目标,这在K8s里特别重要

为什么Prometheus坚持用拉取模式?你想想看,如果每个服务都往中心推数据,一旦网络抖动或者中心挂了,数据就丢了。拉取模式的好处是:你随时可以知道目标是否存活。我在项目中遇到过,某个服务推模式下的指标突然断了,排查了半天才发现是客户端挂了,但Prometheus那边完全不知道。

核心要点:Prometheus的拉取模式天然支持健康检查。如果拉取失败,说明目标有问题,这本身就是一条重要指标。

4.2 Exporter开发实战:从零写一个自定义Exporter

说实话,市面上现成的Exporter已经很多了,但总有业务场景需要自己写。我记得有一次,客户需要一个监控Redis集群中每个分片连接数的Exporter,官方Exporter做不到那么细,只能自己动手。

写一个Exporter其实不复杂,核心就三步:

  1. 暴露一个HTTP端点(通常是/metrics
  2. 在端点处理函数中采集指标数据
  3. 用Prometheus客户端库注册并返回指标

我拿Python举个例子,这是最常用的方式:

from prometheus_client import start_http_server, Gauge
import time
import random

# 定义一个Gauge类型的指标
app_requests_total = Gauge('app_requests_total', 'Total number of requests', ['method', 'endpoint'])

def collect_metrics():
    while True:
        # 模拟采集业务指标
        app_requests_total.labels(method='GET', endpoint='/api/v1/users').set(random.randint(100, 500))
        app_requests_total.labels(method='POST', endpoint='/api/v1/orders').set(random.randint(50, 200))
        time.sleep(15)

if __name__ == '__main__':
    start_http_server(8000)
    collect_metrics()

嗯,这里要注意几个坑:

  • 指标命名规范:用下划线分隔,不要用驼峰。比如app_requests_total而不是appRequestsTotal
  • 标签不要太多:每个指标标签组合太多会导致基数爆炸。我曾经见过一个Exporter,标签组合超过10万种,直接把Prometheus内存打爆了
  • 采集频率别太高:默认15秒一次就够了,别设成1秒,除非你确定业务能扛住

个人经验:写Exporter时,一定要加上_info_created这类元数据指标。比如app_build_info{version="1.2.3"},这样查问题时能快速知道当前跑的是哪个版本。

4.3 ServiceMonitor与PodMonitor配置:K8s下的动态发现

在Kubernetes里,手动配置每个目标是不现实的。Pod随时可能重建,IP也会变。这时候就需要ServiceMonitorPodMonitor出场了。

说白了,这两个资源就是告诉Prometheus Operator:你要监控哪些Pod,怎么找到它们

先看ServiceMonitor的配置示例:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app-monitor
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: my-app
  endpoints:
  - port: metrics
    interval: 30s
    path: /metrics
  namespaceSelector:
    matchNames:
    - production

这个配置的意思是:在production命名空间里,找到所有带app: my-app标签的Service,然后通过名为metrics的端口,每30秒拉取一次/metrics路径。

PodMonitor跟ServiceMonitor的区别是什么?我简单说一下:

  • ServiceMonitor:通过Service的标签选择器来发现目标,适合有Service的场景
  • PodMonitor:直接通过Pod的标签选择器来发现目标,适合不需要Service的场景(比如Job或CronJob)

我曾经踩过一个坑:某个服务没有创建Service,直接用PodMonitor配置了,但Prometheus怎么都拉不到数据。排查了半天才发现,PodMonitor需要Pod的端口有名字,而且名字必须跟配置里的port字段一致。

避坑指南:PodMonitor的端口名必须跟Pod spec里定义的端口名完全匹配,大小写敏感。我曾经因为端口名写成了Metrics(首字母大写),而Pod里定义的是metrics,结果浪费了两个小时。

4.4 指标采集性能优化:别让监控拖垮业务

指标采集本身也会消耗资源。我见过最夸张的情况,一个Prometheus实例采集了50万个时间序列,查询响应时间超过10秒,CPU跑满。所以性能优化是必须的。

我总结了几条核心优化策略:

优化方向 具体做法 效果
减少时间序列 控制标签基数,避免高基数标签(如user_id、request_id) 大幅降低存储和查询压力
调整采集频率 非关键指标从15秒改为30秒或60秒 减少网络和CPU开销
使用Relabeling 丢弃不需要的指标,比如__meta_*标签 减少存储量
分片采集 用多个Prometheus实例分担采集任务 线性扩展采集能力
启用压缩 开启Prometheus的snappy压缩 减少网络传输量

这里重点说说Relabeling,这是个很实用的技巧。比如你只想保留app_requests_total这个指标,其他都丢掉:

scrape_configs:
  - job_name: 'my-app'
    metric_relabel_configs:
    - source_labels: [__name__]
      regex: 'app_requests_total'
      action: keep

嗯,这个配置的意思是:只保留指标名匹配app_requests_total的指标,其他的全部丢弃。我在项目中用过这个技巧,把采集量从20万降到了3万,效果立竿见影。

我的建议:不要一开始就追求全量采集。先采集核心指标,运行一段时间后,根据实际需求逐步增加。我曾经见过一个团队,上来就采集了所有JVM指标,结果发现90%的指标根本没人看,白白浪费资源。

最后说一句,指标采集性能优化是个持续的过程。你可以在Prometheus的/status页面看到当前的时间序列数量,如果超过100万,就该考虑优化了。我个人的经验是,单个Prometheus实例管理50万以内的时间序列是比较舒服的,超过这个数,建议做分片或者升级硬件。