4、自定义指标埋点:Prometheus Client SDK 实战

这一章我们聊聊埋点。说白了,就是怎么在你的代码里插桩,让 Prometheus 能抓到想要的状态数据。

我见过不少团队,上来就搞 Grafana 大屏,结果发现指标全是默认的 CPU、内存,业务状态一个没有。那监控还有什么意义?

所以,今天我们把 Java、Go、Python 三种语言的 SDK 都过一遍。重点不是 API 怎么调,而是四种指标类型——Counter、Gauge、Histogram、Summary——到底该用在什么场景。

核心观点: 埋点不是写代码,是设计数据模型。想清楚再动手。

4.1 四种指标类型,别搞混了

先看一张图,把四种类型的关系理清楚。

Prometheus 四种核心指标类型 Counter 只增不减的计数器 Gauge 可增可减的仪表盘 Histogram 分桶统计分布 Summary 预计算分位数 请求总数 错误次数 任务完成数 CPU 使用率 内存占用 连接数 请求延迟分布 响应大小分布 自定义分桶 TP99 延迟 TP90 延迟 服务等级 SLO Counter 和 Gauge 是基础指标,Histogram 和 Summary 用于分析分布 Histogram 可聚合,Summary 不可聚合——这是选型关键 选型建议 需要聚合计算 → Histogram 需要精确分位数且不跨实例聚合 → Summary

嗯,这张图基本把选型逻辑讲清楚了。下面我们逐个看代码。

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();
小提示: Counter 的命名习惯以 _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
);
注意: 分桶的边界要贴合你的业务。如果大部分请求在 10ms 以内,你设 0.1、0.5、1.0 就太粗了。我曾经因为桶设得太宽,导致 P99 看起来很低,实际上全是毛刺。

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)
避坑: Python 的 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。
一句话总结: Counter 数次数,Gauge 看当前,Histogram 算分布,Summary 要精确。选对了,监控就成功了一半。