3. 监控数据采集:Prometheus 四种核心指标类型

监控系统好不好用,很大程度上取决于指标数据怎么采集、怎么表达。Prometheus 之所以流行,除了它 Pull 模型简洁之外,更关键的是它把指标抽象成了四种核心类型——Counter、Gauge、Histogram、Summary。

这四种类型,说白了就是四种不同的“数据视角”。你选对了类型,告警规则和后续分析就顺了;选错了,后面全是坑。我刚开始用 Prometheus 时,就吃过这个亏——把 Gauge 当 Counter 用,结果告警阈值怎么调都不对。

咱们一个一个来看。

核心观点:四种指标类型对应四种不同的监控场景。Counter 适合“只增不减”的累计量,Gauge 适合“可增可减”的瞬时值,Histogram 和 Summary 则用于分析分布和分位数。选对类型,事半功倍。

Prometheus 四种核心指标类型 Counter 只增不减的累计值 Gauge 可增可减的瞬时值 Histogram 分布统计与分位数 Summary 预计算分位数 请求总数 错误次数 CPU 时间累计 内存使用量 当前连接数 温度/水位 请求延迟分布 响应大小分布 P50/P90/P99 预计算分位数 服务端聚合 固定分位输出 选择建议 累计量 → Counter | 瞬时值 → Gauge | 分布分析 → Histogram | 固定分位 → Summary 注意:Histogram 和 Summary 在客户端计算方式不同,选择时需考虑性能与精度权衡

3.1 Counter:只增不减的计数器

Counter 是最直观的指标类型。它记录的是一个单调递增的数值——比如 HTTP 请求总数、系统启动后的 CPU 时间、数据库写入次数。你想想看,这些数据是不是只会变大?

Counter 的核心特点就两个:

  • 只能增加:进程重启时归零,但运行期间只增不减。
  • 适合计算速率:配合 rate() 或 irate() 函数,可以算出每秒的增量。

实际项目中,我见过不少人直接用 Counter 的原始值做告警——比如“请求总数超过 10000 就报警”。这其实不对。Counter 的原始值会随着运行时间一直增长,阈值很难定。正确的做法是用 rate() 算每秒请求量。

我的习惯:Counter 类型的指标,命名时尽量以 _total 结尾。比如 http_requests_total、errors_total。这样别人一看就知道是 Counter,也方便 Prometheus 自动识别。

来看一个 Go 客户端示例:

// 定义一个 Counter 指标
httpRequestsTotal := prometheus.NewCounter(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests.",
    },
)

// 注册到 Prometheus
prometheus.MustRegister(httpRequestsTotal)

// 每次收到请求时 +1
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    httpRequestsTotal.Inc()
    w.Write([]byte("Hello"))
})

查询时,用 rate(http_requests_total[5m]) 就能得到过去 5 分钟的平均每秒请求数。这个值才是真正有意义的。

3.2 Gauge:可增可减的仪表盘

Gauge 和 Counter 正好相反——它记录的是可以上下波动的瞬时值。比如当前内存使用量、活跃连接数、队列长度、CPU 温度。这些值今天可能是 80%,明天可能是 30%,很正常。

Gauge 的典型用法:

  • 直接展示当前状态(比如仪表盘上的数字)
  • 配合 avg()、max()、min() 做聚合
  • 用 delta() 看一段时间内的变化量

我记得有一次排查线上问题,发现某个服务的内存持续增长。用 Gauge 指标 process_resident_memory_bytes 配合 delta() 一看,每 5 分钟涨 200MB——明显是内存泄漏。如果没有 Gauge 类型,这种“先升后降”的场景根本没法监控。

我曾经踩过的坑:Gauge 的 Set() 方法会直接覆盖旧值。如果你在多个 goroutine 里同时调用 Set(),可能会出现竞态条件。建议用 Set() 前加锁,或者直接用 prometheus.NewGaugeVec() 按标签区分。

代码示例:

// 定义一个 Gauge 指标
memoryUsage := prometheus.NewGauge(
    prometheus.GaugeOpts{
        Name: "app_memory_usage_bytes",
        Help: "Current memory usage in bytes.",
    },
)

prometheus.MustRegister(memoryUsage)

// 定时更新内存使用量
go func() {
    for {
        var m runtime.MemStats
        runtime.ReadMemStats(&m)
        memoryUsage.Set(float64(m.Alloc))
        time.Sleep(15 * time.Second)
    }
}()

查询时,直接 app_memory_usage_bytes 就能拿到当前值。如果想看变化趋势,用 delta(app_memory_usage_bytes[5m])。

3.3 Histogram:分布统计与分位数

Histogram 就有点意思了。它不只是记录一个数值,而是把数据分桶统计。比如你想知道“过去 5 分钟,有多少请求在 100ms 以内?多少在 200ms 以内?多少超过了 500ms?”——这就是 Histogram 的用武之地。

Histogram 会自动生成三个时间序列:

  • _bucket{le="..."}:每个桶的累计计数
  • _count:总样本数
  • _sum:所有样本值的总和

有了这些数据,你可以算出 P50、P90、P99 等分位数。比如:

# 计算 P99 延迟(过去 5 分钟)
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

我个人习惯把 Histogram 的桶边界设置成“业务可接受”的阈值。比如接口要求 200ms 内响应,那桶就设成 {0.05, 0.1, 0.2, 0.5, 1.0}。这样 le="0.2" 的桶直接告诉你“达标请求有多少”。

避坑指南:桶的数量不是越多越好。我见过有人设了 50 个桶,结果每个桶的数据稀疏得不行,算出来的分位数偏差很大。一般 5-10 个桶就够了,重点覆盖你关心的阈值附近。

代码示例:

// 定义一个 Histogram,桶边界为 0.05s, 0.1s, 0.2s, 0.5s, 1.0s
requestDuration := prometheus.NewHistogram(
    prometheus.HistogramOpts{
        Name:    "http_request_duration_seconds",
        Help:    "HTTP request duration in seconds.",
        Buckets: []float64{0.05, 0.1, 0.2, 0.5, 1.0},
    },
)

prometheus.MustRegister(requestDuration)

// 记录每次请求的耗时
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // ... 处理请求 ...
    requestDuration.Observe(time.Since(start).Seconds())
})

3.4 Summary:预计算的分位数

Summary 和 Histogram 有点像,都是用来分析分布的。但有一个关键区别:Summary 在客户端就计算好了分位数,直接暴露出来。而 Histogram 是在服务端用 histogram_quantile() 算的。

Summary 会生成三个时间序列:

  • _quantile{quantile="..."}:预计算的分位数值
  • _count:总样本数
  • _sum:所有样本值的总和

什么时候用 Summary?我总结了两条:

  • 你明确知道要哪些分位数(比如 P50、P90、P99)
  • 客户端计算更合适(比如不想在 Prometheus 服务端增加计算压力)

注意:Summary 的分位数是固定的,你定义时设了 P50、P90、P99,它就只算这三个。后期想加 P95?得改代码重新部署。而 Histogram 可以在查询时灵活指定分位数。所以,如果你不确定未来需要哪些分位数,优先用 Histogram。

代码示例:

// 定义一个 Summary,计算 P50、P90、P99
requestDuration := prometheus.NewSummary(
    prometheus.SummaryOpts{
        Name:       "http_request_duration_seconds",
        Help:       "HTTP request duration in seconds.",
        Objectives: map[float64]float64{
            0.5:  0.05,   // P50,允许 5% 误差
            0.9:  0.01,   // P90,允许 1% 误差
            0.99: 0.001,  // P99,允许 0.1% 误差
        },
    },
)

prometheus.MustRegister(requestDuration)

// 使用方式与 Histogram 相同
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // ... 处理请求 ...
    requestDuration.Observe(time.Since(start).Seconds())
})

3.5 四种类型对比与选型

说了这么多,咱们用一张表总结一下:

特性 Counter Gauge Histogram Summary
数值变化 只增不减 可增可减 累计分布 累计分布
典型场景 请求总数、错误次数 内存、连接数、温度 延迟分布、响应大小 预计算分位数
分位数计算 不支持 不支持 服务端计算 客户端计算
灵活性 高 高 中(桶固定) 低(分位固定)
性能开销 低 低 中(桶越多开销越大) 中(分位越多开销越大)
推荐指数 ★★★★★ ★★★★★ ★★★★☆ ★★★☆☆

选型其实不难:

  • 累计量 → Counter
  • 瞬时值 → Gauge
  • 需要分布分析,且不确定未来要哪些分位 → Histogram
  • 明确知道要哪些分位,且客户端计算更合适 → Summary

我的建议:刚开始做监控时,优先用 Counter 和 Gauge 覆盖基础指标。等业务稳定了,再引入 Histogram 做延迟分析。Summary 我一般只在“客户端计算分位数有明确优势”时才用——比如数据量极大、不想给 Prometheus 服务端增加负担的场景。