3. 监控数据采集:Prometheus 四种核心指标类型
监控系统好不好用,很大程度上取决于指标数据怎么采集、怎么表达。Prometheus 之所以流行,除了它 Pull 模型简洁之外,更关键的是它把指标抽象成了四种核心类型——Counter、Gauge、Histogram、Summary。
这四种类型,说白了就是四种不同的“数据视角”。你选对了类型,告警规则和后续分析就顺了;选错了,后面全是坑。我刚开始用 Prometheus 时,就吃过这个亏——把 Gauge 当 Counter 用,结果告警阈值怎么调都不对。
咱们一个一个来看。
核心观点:四种指标类型对应四种不同的监控场景。Counter 适合“只增不减”的累计量,Gauge 适合“可增可减”的瞬时值,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 服务端增加负担的场景。