4、自定义指标埋点:Prometheus Client SDK 实战
这一章我们聊聊埋点。说白了,就是怎么在你的代码里插桩,让 Prometheus 能抓到想要的状态数据。
我见过不少团队,上来就搞 Grafana 大屏,结果发现指标全是默认的 CPU、内存,业务状态一个没有。那监控还有什么意义?
所以,今天我们把 Java、Go、Python 三种语言的 SDK 都过一遍。重点不是 API 怎么调,而是四种指标类型——Counter、Gauge、Histogram、Summary——到底该用在什么场景。
4.1 四种指标类型,别搞混了
先看一张图,把四种类型的关系理清楚。
嗯,这张图基本把选型逻辑讲清楚了。下面我们逐个看代码。
4.2 Java:micrometer 还是 simpleclient?
Java 生态里有两个选择:官方 simpleclient 和 Spring 生态的 micrometer。我个人习惯用 micrometer,因为它自动帮你做了很多绑定,比如 JVM 指标、线程池、数据库连接池。
但如果你要埋业务指标,两种方式都差不多。看代码:
Counter:统计请求总数
// 使用 Micrometer
MeterRegistry registry = new SimpleMeterRegistry();
Counter requestCounter = Counter.builder("api_requests_total")
.tag("method", "GET")
.tag("endpoint", "/users")
.description("Total API requests")
.register(registry);
// 每次请求 +1
requestCounter.increment();
// 使用 simpleclient
import io.prometheus.client.Counter;
Counter requests = Counter.build()
.name("api_requests_total")
.labelNames("method", "endpoint")
.help("Total API requests.")
.register();
requests.labels("GET", "/users").inc();
_total 结尾。Prometheus 会自动追加,但你自己加上更清晰。
Gauge:监控连接池大小
Gauge activeConnections = Gauge.build()
.name("db_active_connections")
.help("Current active database connections.")
.register();
// 连接建立时
activeConnections.inc();
// 连接关闭时
activeConnections.dec();
// 也可以直接设值
activeConnections.set(42);
Gauge 最容易被误用。我见过有人用 Gauge 统计累计请求数——那应该用 Counter。Gauge 只适合「当前值」,比如温度、水位、队列长度。
Histogram:记录请求延迟
Histogram requestLatency = Histogram.build()
.name("http_request_duration_seconds")
.help("Request latency in seconds.")
.buckets(0.1, 0.3, 0.5, 1.0, 2.0, 5.0)
.register();
// 记录耗时
long start = System.currentTimeMillis();
// ... 处理请求 ...
requestLatency.observe(
(System.currentTimeMillis() - start) / 1000.0
);
Summary:精确分位数
Summary requestLatency = Summary.build()
.name("http_request_duration_seconds")
.help("Request latency in seconds.")
.quantile(0.5, 0.05) // 中位数,允许误差 5%
.quantile(0.9, 0.01) // TP90,允许误差 1%
.quantile(0.99, 0.001) // TP99,允许误差 0.1%
.register();
requestLatency.observe(0.215);
Summary 和 Histogram 的区别在哪?说白了,Summary 在客户端就算好了分位数,你直接拿结果。但它的分位数不能跨实例聚合。如果你要算整个集群的 TP99,必须用 Histogram。
4.3 Go:官方 client_golang
Go 的 SDK 设计得很清爽。我最早用 Go 写埋点是在一个消息队列中间件里,当时踩了个坑——忘记注册指标,结果 Prometheus 抓不到数据。
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// Counter
var requestCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "api_requests_total",
Help: "Total API requests.",
},
[]string{"method", "endpoint"},
)
// Gauge
var activeConnections = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "db_active_connections",
Help: "Current active database connections.",
},
)
// Histogram
var requestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Request latency in seconds.",
Buckets: []float64{0.1, 0.3, 0.5, 1.0, 2.0, 5.0},
},
[]string{"method"},
)
// Summary
var requestLatencySummary = promauto.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_duration_seconds",
Help: "Request latency in seconds.",
Objectives: map[float64]float64{
0.5: 0.05,
0.9: 0.01,
0.99: 0.001,
},
},
[]string{"method"},
)
// 使用
func handler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
requestCounter.WithLabelValues("GET", "/users").Inc()
// ... 业务逻辑 ...
requestDuration.WithLabelValues("GET").Observe(
time.Since(start).Seconds(),
)
}
promauto 包自动注册,省得你手动调用 prometheus.MustRegister()。少写一行是一行。
4.4 Python:prometheus_client
Python 的 SDK 最灵活,但也最容易写错。我记得有一次在 Flask 应用里埋点,忘了用 startTimer() 的上下文管理器,结果时间戳全乱了。
from prometheus_client import Counter, Gauge, Histogram, Summary, start_http_server
import time
# Counter
REQUEST_COUNT = Counter(
'api_requests_total',
'Total API requests',
['method', 'endpoint']
)
# Gauge
ACTIVE_CONNECTIONS = Gauge(
'db_active_connections',
'Current active database connections'
)
# Histogram
REQUEST_DURATION = Histogram(
'http_request_duration_seconds',
'Request latency in seconds',
buckets=[0.1, 0.3, 0.5, 1.0, 2.0, 5.0]
)
# Summary
REQUEST_LATENCY_SUMMARY = Summary(
'http_request_duration_seconds',
'Request latency in seconds',
['method']
)
# 使用示例
def handle_request():
REQUEST_COUNT.labels(method='GET', endpoint='/users').inc()
ACTIVE_CONNECTIONS.inc()
with REQUEST_DURATION.time(): # 自动计时
# 业务逻辑
time.sleep(0.1)
ACTIVE_CONNECTIONS.dec()
# 启动 HTTP 服务暴露指标
start_http_server(8000)
Histogram.time() 是上下文管理器,但 Summary.time() 也是。别混用。我见过有人用 Summary 的 time() 去记录 Histogram 的数据,结果分位数全错了。
4.5 四种指标对比速查
| 指标类型 | 核心特征 | 典型场景 | 可聚合 | 客户端计算 |
|---|---|---|---|---|
| Counter | 只增不减,重启归零 | 请求数、错误数、任务数 | 是 | 否 |
| Gauge | 可增可减,反映当前值 | CPU、内存、连接数、队列长度 | 是 | 否 |
| Histogram | 分桶统计,可计算分位数 | 延迟分布、响应大小分布 | 是 | 否 |
| Summary | 预计算分位数,不可聚合 | TP99、TP90 等精确分位数 | 否 | 是 |
4.6 埋点最佳实践
代码写完了,但埋点这件事远没结束。我总结了几条经验:
- 命名规范: 用
_total、_seconds、_bytes等后缀,Prometheus 官方推荐。 - 标签不要太多: 每个指标标签控制在 5 个以内。标签太多会导致基数爆炸,Prometheus 会撑不住。
- 标签值不要太高基数: 比如 user_id、request_id 这种,千万别当标签。我见过一个团队把 session_id 当标签,结果 Prometheus 内存直接打满。
- Histogram 的桶要贴合业务: 先跑一段时间看分布,再调整桶边界。
- Summary 的误差参数要合理: 误差越小,计算开销越大。TP99 设 0.001 就够了,别设 0.0001。