4. 指标采集与暴露:Prometheus客户端接入、Histogram/Summary/Gauge/Counter使用场景、/metrics端点设计
好,咱们进入正题。报价路由系统里,指标怎么采、怎么露,这事儿我踩过的坑可不少。说白了,Prometheus 客户端接入本身不难,难的是选对指标类型,以及把 /metrics 端点设计得既清晰又高效。
4.1 Prometheus 客户端接入:别小看这一步
接入 Prometheus 客户端,我习惯用官方的 client_golang(如果服务是 Go 写的)。Java 项目就用 simpleclient。嗯,这里要注意:版本一定要对齐,不然会有兼容性问题。
我曾经在一个老项目里,直接 copy 了网上的一段代码,结果 client 版本和 Prometheus server 版本不匹配,指标一直报错。排查了半天,最后发现是 protobuf 格式不一致。所以,我建议你直接从官方仓库拉依赖,别偷懒。
接入的核心就两步:
- 引入依赖:在项目中添加 Prometheus 客户端库。
- 注册指标:创建并注册你需要的指标对象。
举个例子,Go 语言里这样搞:
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// 定义一个 Counter 指标
requestCount = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "route_requests_total",
Help: "Total number of routing requests.",
},
[]string{"method", "endpoint"},
)
)
func init() {
// 注册到默认注册器
prometheus.MustRegister(requestCount)
}
你看,就这么简单。但别忘了,MustRegister 如果注册重复的指标会 panic。我在生产环境见过因为这个 panic 导致服务重启的案例。所以,我建议你注册前先检查一下,或者用 Register 方法处理错误。
4.2 四种指标类型:选对工具干对活
Prometheus 提供了四种核心指标类型:Counter、Gauge、Histogram、Summary。很多新手容易搞混,我一个个说。
4.2.1 Counter:只增不减的计数器
Counter 就是计数器,只能增加,不能减少。适合统计请求总数、错误次数、任务完成数等。
举个例子,报价路由系统里,我们统计“总路由请求数”:
requestCount.With(prometheus.Labels{"method": "GET", "endpoint": "/quote"}).Inc()
为什么用 Counter?因为它的值只增不减,Prometheus 可以计算速率(rate),比如“每秒请求数”。
Add() 方法。但说实话,我很少这么干,因为 Prometheus 更关心增量变化。
4.2.2 Gauge:可增可减的仪表盘
Gauge 可以上下波动,适合内存使用率、CPU 负载、队列长度、在线连接数等。
在报价路由系统里,我们监控“当前待处理报价数”:
pendingQuotes.Set(float64(len(queue)))
Gauge 的典型用法是 Set()、Inc()、Dec()。我建议你尽量用 Set(),因为它能保证数值的准确性。用 Inc()/Dec() 在高并发下可能会有竞态问题,虽然 Prometheus 客户端内部做了原子操作,但逻辑上还是 Set() 更可控。
4.2.3 Histogram:分布直方图
Histogram 用来统计数据的分布情况,比如请求延迟、响应大小。它会将数据分桶(bucket),然后统计每个桶里的样本数。
举个例子,我们统计“报价路由请求延迟”:
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "route_request_duration_seconds",
Help: "Histogram of routing request durations.",
Buckets: prometheus.DefBuckets, // 默认桶:.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10
},
[]string{"method"},
)
使用的时候:
timer := prometheus.NewTimer(requestDuration.WithLabelValues("GET"))
defer timer.ObserveDuration()
Histogram 能告诉你:有多少请求在 100ms 以内?有多少在 500ms 以内?这对于设置 SLO(服务等级目标)非常有用。
我个人习惯把桶设置得跟业务相关。比如报价路由要求 P99 在 200ms 以内,那我就会在 200ms 附近多设几个桶:
Buckets: []float64{0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.5, 1.0}
4.2.4 Summary:预计算的分位数
Summary 和 Histogram 类似,也是统计分布。但 Summary 在客户端就计算好了分位数(quantile),直接暴露给 Prometheus。
举个例子:
requestDurationSummary = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "route_request_duration_summary_seconds",
Help: "Summary of routing request durations.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
[]string{"method"},
)
这里 Objectives 定义了你要计算的分位数,以及允许的误差。比如 0.99: 0.001 表示 P99 的误差在 0.1% 以内。
- Histogram 在服务端计算分位数,可以灵活聚合。
- Summary 在客户端计算,无法跨进程聚合。
我建议:如果你需要跨多个实例计算全局 P99,用 Histogram。如果只关心单个实例的延迟,用 Summary 更省资源。
4.3 /metrics 端点设计:别一股脑全暴露
/metrics 端点就是 Prometheus 来抓取数据的地方。设计得好,运维省心;设计得不好,Prometheus 抓取超时,甚至把服务拖垮。
我见过最离谱的 /metrics 端点,把每个请求的完整日志都暴露出来了,结果 Prometheus 一抓,内存直接爆了。嗯,这绝对是反面教材。
设计 /metrics 端点,我总结了几个原则:
- 只暴露必要的指标:不要把所有内部变量都暴露出去。比如,不要暴露每个用户的详细数据,只暴露聚合后的统计值。
- 控制指标数量:一个服务暴露几百个指标很正常,但几千个就有点过了。我建议单个服务不超过 1000 个时间序列。
- 使用标签(Labels)要克制:标签是 Prometheus 的维度,但标签值太多会导致基数爆炸。比如,不要把用户 ID 作为标签,因为用户数量可能上百万。
- 设置抓取超时:在 Prometheus 配置里设置
scrape_timeout,比如 10 秒。如果 /metrics 生成太慢,就优化代码。
下面是一个标准的 /metrics 端点实现(Go 语言):
http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(":8080", nil))
就这么一行。但如果你需要自定义,比如加个认证或过滤,可以自己包装一下:
func metricsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 可以加一些鉴权逻辑
promhttp.Handler().ServeHTTP(w, r)
})
}
4.4 知识体系图:指标采集与暴露的核心逻辑
下面这张图,是我自己总结的。它把整个流程串起来了,你一看就明白。
4.5 避坑指南:我踩过的那些坑
最后,分享几个我亲身经历过的坑,希望能帮你少走弯路。
- 标签基数爆炸:我曾经把一个请求的 trace ID 作为标签,结果 Prometheus 存储直接爆了。后来改成只统计聚合值,问题解决。
- Histogram 桶设置不合理:默认桶最大是 10 秒,但我们的报价路由延迟都在毫秒级,导致所有请求都落在第一个桶里。后来我根据业务重新设置了桶范围。
- /metrics 端点没有限流:有一次 Prometheus 抓取频率太高,加上 /metrics 生成慢,导致服务响应超时。我后来加了限流和缓存,才稳住。
- Summary 无法聚合:我一开始用 Summary 统计全局 P99,结果发现每个实例的 P99 都不一样,无法直接求平均。后来换成 Histogram,用
histogram_quantile函数在 PromQL 里计算,才得到准确结果。
嗯,这些坑说起来都是泪。但吃一堑长一智,现在我做指标设计,都会先画个图,想清楚再动手。
公众号:蓝海资料掘金营,微信deep3321