关键在于指标定义时绑定service、tenant_id等真实业务标签,而非仅用method、status等通用维度;须用NewXXXVec显式声明业务标签名,确保WithLabelValues()顺序一致,预处理路径动态ID为占位符,并避免高基数标签导致指标爆炸。

Go 服务要让 Prometheus 抓到有意义的业务指标,关键不是“暴露/metrics”,而是指标定义时就绑定业务维度——比如 service、tenant_id、workflow_stage 这类真实业务标签,而不是只用 method 和 status 这种通用维度。
用 NewXXXVec 定义指标时必须显式声明业务标签名
很多人写 prometheus.NewCounterVec 只传 []string{"method", "status"},结果查监控时发现所有订单创建失败都堆在同一个时间线里,根本分不清是哪个租户、哪个渠道触发的。业务维度漏掉,后续 PromQL 就没法 by(tenant_id) 聚合。
- 标签名必须是合法标识符:不能含空格、斜杠、点号;
tenant-id要写成tenant_id - 顺序必须和
WithLabelValues()调用时完全一致,错一位就 panic - 避免标签爆炸:路径中动态 ID(如
/order/12345)必须预处理为占位符(如/order/{id}),否则每个 ID 都生成新指标序列 - 高频打点场景下,别在请求处理函数里反复调用
WithLabelValues();应提前缓存prometheus.Counter实例,例如:counterByTenant["prod-001"].Inc()
go-zero/gf 等框架的自动指标不包含业务维度,得自己补
go-zero 默认暴露的 http_requests_total 只有 method、status、path,path 还是原始路由(如 /api/v1/orders/{id}),没注入租户或业务线信息。Gf 框架的 gmetric 虽支持 Attributes,但默认中间件也不带业务上下文。
- HTTP 中间件里从 context 或 header 提取
X-Tenant-ID,再透传给指标打点逻辑 - 不要在 handler 里硬编码
counter.WithLabelValues("create_order", tenantID, "success")—— 把维度提取和指标更新拆成两层,方便复用 - gRPC 场景下,从
metadata.MD读取tenant-id,比解析请求体更轻量 - go-zero 的
PrometheusHandler是装饰器模式,可 wrap 一层自己的中间件来注入额外标签
注册前确保 registry 实例唯一,否则指标丢失或 panic
常见现象是本地跑通,一上 K8s 就 duplicate metrics collector panic,或者重启后指标归零。根源是多个包各自调用 prometheus.MustRegister(),撞上了全局 DefaultRegisterer。
立即学习“go语言免费学习笔记(深入)”;
- 微服务多模块时,统一用自定义
prometheus.NewRegistry(),并在promhttp.HandlerFor(reg, opts)中显式传入 - 避免在
init()函数里注册指标——它可能被多次执行(尤其单元测试 reload 包时) - go-zero 启动时调用
prometheus.StartAgent会创建独立 registry,但你自定义的业务指标仍需手动注册到该 registry,不能依赖默认注册器 - 检查是否重复 import 同一 metrics 包两次(比如 vendor 和 go mod 并存),会导致两个同名变量指向不同 registry
打点时机不对会让 P99 延迟失真,尤其是异步任务
用 prometheus.NewHistogramVec 统计 HTTP 请求延迟没问题,但统计“下单后发 Kafka 消息耗时”这类异步链路时,如果在 goroutine 里直接 .Observe(),很可能因为 GC 或调度延迟导致观测值偏大,且无法对齐 trace ID。
- 异步操作的延迟指标,应在发起处记录开始时间(如
start := time.Now()),在回调或完成 channel 收到信号时计算并打点 - 别用
Gauge模拟 P99:有人把每次耗时存进Gauge再用 PromQL 算quantile(0.99, ...),这是错的——Gauge不保留分布,quantile在 Prometheus 里只对Histogram的_bucket序列有效 - Buckets 设置必须贴合业务实际:电商下单 P99 通常在 300ms 内,Buckets 可设为
[]float64{0.05, 0.1, 0.2, 0.3, 0.5, 1.0}(单位秒),别直接抄默认DefBuckets - 若业务逻辑本身有重试(如三次调用下游),应按“最终成功”打一次延迟,而非每次重试都打点,否则 P99 会被重试次数放大
真正难的不是写几行 NewCounterVec,而是让每个标签值都能在告警、Grafana 下钻、日志关联时准确对应到业务语义——比如 tenant_id 必须和数据库分库键、API 网关路由规则里的值完全一致,差一个字符,整个可观测链条就断了。


















