自定义指标必须定义为包级变量且只注册一次,否则会因重复注册导致panic;需显式挂载/metrics路由并匹配Prometheus配置;选错指标类型或滥用标签将导致监控失真或性能崩溃。

Go 服务里自定义指标写得再漂亮,只要注册、打点、暴露三步中任何一环出错,Prometheus 就抓不到真实业务数据——不是没采集,是根本没“上线”。
自定义指标必须定义为包级变量且只注册一次
常见错误是把 prometheus.NewCounterVec 写在 handler 函数里,每次请求都新建+注册,结果进程直接 panic:duplicate metrics collector registration。这是因为 Prometheus 客户端对同名指标注册极其敏感,且无法 recover。
- 所有指标变量(如
httpReqCounter、orderDurationHist)必须声明为包级变量 - 注册逻辑统一放在
init()或main()开头,只执行一次 - 若用
CounterVec/GaugeVec,标签名数组(如[]string{"method", "status", "tenant_id"})必须与后续WithLabelValues()的传参顺序严格一致,错一位就 panic - 别在多个包里 import 同一个 metrics 初始化文件,否则重复调用
MustRegister()是高频 panic 来源
/metrics 端点返回 404 或空响应的真正原因
不是 Prometheus 没配置好,是你没把指标“挂上去”。promhttp.Handler() 只暴露注册在默认注册器上的指标,而 Go 默认的 http.DefaultServeMux 根本不会自动绑定 /metrics 路由。
- 必须显式调用
http.Handle("/metrics", promhttp.Handler()),路径要和 Prometheus 配置里的scrape_configs.job.metrics_path完全一致 - 若用 Gin,得用
router.GET("/metrics", gin.WrapH(promhttp.Handler()));Echo 则用e.GET("/metrics", echo.WrapHandler(promhttp.Handler())) - 如果用了自定义注册器(如
reg := prometheus.NewRegistry()),必须改用promhttp.HandlerFor(reg, ...),否则指标不会出现在响应里 - 别在
promhttp.Handler()外再包一层自定义http.HandlerFunc,会导致 Accept 头协商失败、gzip 不生效,甚至返回406 Not Acceptable
选错指标类型会让监控完全失真
用 Counter 记活跃连接数?用 Gauge 当累计请求数?这两类误用在生产环境极难排查,但后果严重:前者指标只涨不跌,后者聚合语义混乱。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
-
Counter只支持Inc()和Add(),适合订单总数、请求次数等只增不减场景;它不支持减操作,也**不能用于活跃连接统计** -
Gauge支持Set()、Inc()、Dec(),适合当前在线人数、队列长度等瞬时状态;goroutine 退出时必须调用Dec(),否则数值永远不准 - 延迟分布用
Histogram,别用Gauge硬凑 P99——Histogram自带 buckets,rate()和histogram_quantile()才能算出真实分位值 - HPA 场景下,
Counter必须配合rate(my_app_http_requests_total[2m])使用,原始累计值对扩缩容毫无意义
业务标签设计不当会拖垮 Prometheus 存储
标签不是越多越好。一个用户 ID、一个 traceID 塞进去,就能让指标基数爆炸,几小时内压垮 Prometheus 的内存和查询性能。
- 标签名必须全小写、下划线分隔(如
tenant_id,不能写tenant-id或TenantID) - HTTP 路径中的动态 ID(如
/order/12345)必须归一化为占位符(如/order/{id})再打点,否则每个 ID 都生成新时间序列 - 高基数字段(用户 ID、手机号、traceID)禁止直接塞进 label;应改用
Counter按error_type分类,或做采样上报 - 高频打点场景下,避免在 handler 里反复调用
WithLabelValues();可提前缓存子指标实例,比如counterByTenant["prod-001"].Inc()
最常被忽略的是:指标定义时没想清楚业务维度,后期补标签等于重写整个监控链路;注册器用错或混用,本地跑通上 K8s 就 panic;还有人把 Gauge 当 Counter 用,看着数字在涨,其实全是噪音。

















