根本原因是中间件中重复注册指标、未校验空标签、错误调用Observe()时机及高基数路径标签;必须包级声明指标、init注册、c.Next()后Observe、用c.FullPath()归一化路径、状态码传字符串并兜底默认值。

为什么Gin的Prometheus指标采集会拖慢请求
根本原因不是Prometheus本身慢,而是你写的中间件在每次请求里做了不该做的事:重复注册指标、滥用字符串拼接、没控制标签基数、或在c.Next()前就调用Observe()。这些操作会让单次请求多出几百微秒开销,在QPS过万时直接抬高P95延迟。
- 别在handler函数里调用
prometheus.MustRegister()——它只该在init()或main()启动时执行一次,重复注册会panic -
http_request_duration_seconds直方图必须在响应写出后才Observe(),否则测的是中间件耗时,不是真实端到端延迟 - 用
c.FullPath()而非c.Request.URL.Path生成path标签,避免/user/123→/user/456这种高基数爆炸 - 状态码标签必须是字符串,传
200会panic,得传"200"
CounterVec和Histogram的初始化必须包级声明
所有prometheus.CounterVec、prometheus.Histogram都得声明为包级变量,且只在init()里注册一次。常见错误是把它们塞进handler里,导致每次请求都新建一个vec,触发duplicate metrics collector registration panic。
正确写法:
var (
httpReqCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5},
},
[]string{"method", "path", "status"},
)
)
func init() {
prometheus.MustRegister(httpReqCounter)
prometheus.MustRegister(httpRequestDuration)
}
打点中间件里怎么安全获取状态码和耗时
状态码不能直接读c.Writer.Status(),因为中间件执行时响应还没写出;耗时也不能只靠time.Since(),得覆盖panic和context取消路径。
- 用
gin.ResponseWriter包装原始Writer,重写WriteHeader()方法来捕获最终状态码 - 在
defer里检查c.Request.Context().Err():若为context.DeadlineExceeded,状态设为"timeout";若为context.Canceled,设为"canceled" - panic恢复路径里也要调用
httpReqCounter.WithLabelValues(c.Request.Method, c.FullPath(), "500").Inc() - 直方图
Observe()必须放在c.Next()之后、且确保response已写出的位置
标签值为空字符串会导致指标失效
比如status或path标签传了空字符串"",Prometheus抓取时会报invalid metric family name或静默丢弃,更糟的是同一指标多次用WithLabelValues("")会注册多个匿名时间序列,引发cardinality爆炸。
- 对所有label值做
strings.TrimSpace()清洗 - 关键label如
status、path必须设默认值:if status == "" { status = "unknown" } - 别依赖前端或下游传来的原始字段直接当label,比如用户ID、设备型号,一律兜底为
"unknown"



















