日志中间件统计QPS本质错误:它滞后、不可靠、开销高,无法满足实时性要求;QPS必须在HTTP handler入口用atomic计数,而非依赖日志解析。

日志中间件里统计 QPS 本质是错的路
日志中间件不适合承担 QPS 统计职责。它天然滞后、不可靠、开销高,且无法满足实时性要求。QPS 是速率指标,必须在请求进入处理流程的最早可拦截点(如 HTTP handler 入口)完成原子计数,而不是等日志写入时再读取时间戳做聚合。
常见错误现象:log.Printf("req: %s %s %d", r.Method, r.URL.Path, statusCode) 后试图从日志行里 grep + awk 统计每秒请求数——这会漏掉 panic、超时、连接中断等未打日志的请求;日志刷盘延迟导致窗口错位;高并发下 I/O 成瓶颈,反而拖慢主流程。
- 日志是事后记录,QPS 是实时负载信号,二者语义不匹配
- 日志格式可能被修改(加 traceID、字段重命名),导致解析脚本失效
- 日志采集 agent(如 filebeat)本身有缓冲和重试逻辑,时间戳已失真
- 即使你用结构化日志(如 zap)+ Loki + LogQL,
rate(count_over_time({job="api"} |~ "200" [1m]))的精度和稳定性远不如原生 Counter
真正该放 QPS 计数的位置:HTTP 中间件入口
QPS 统计必须发生在请求被接受、尚未进入业务逻辑前,且全程无锁、无分支、无内存分配。Gin/Chi/stdlib http.Handler 都支持中间件,这里才是唯一正确位置。
推荐用 sync/atomic + time.Tick 方案,轻量、稳定、Go 标准库全覆盖:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
var qps uint64
<p>func QPSMiddleware() gin.HandlerFunc {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
prev := atomic.SwapUint64(&qps, 0)
// 可选:发到监控系统,或存入 prometheus.Gauge
}
}()
return func(c *gin.Context) {
atomic.AddUint64(&qps, 1)
c.Next()
}
}- tick 频率必须严格为
time.Second,不要用time.Millisecond * 1000(Go 1.20+ 有纳秒级偏差) -
atomic.AddUint64在 x86-64 上编译为单条ADDQ指令,无锁、零分支 - 别在中间件里调用
atomic.LoadUint64(&qps)做日志或埋点,高频读会引发缓存行伪共享 - 如果用 Prometheus,把
&qps绑定到prometheus.Gauge,并在 tick 回调里gauge.Set(float64(atomic.LoadUint64(&qps)))
为什么不能用 expvar 或 log-based counter 暴露 QPS
expvar.NewInt("qps") 和基于日志的计数器都会引入非必要开销和语义偏差。前者底层是 mutex + map 查找,比裸 atomic 慢一个数量级;后者依赖日志落地时机,根本不是“请求到达速率”。
更关键的是:QPS 必须是可聚合、可下钻的业务维度指标,而日志中间件无法可靠提取 endpoint、department、status 这些标签——状态码在日志写入时可能还是 0,路径没归一化(/user/123 vs /user/{id})会导致时间序列爆炸。
- 暴露端点建议独立路由,如
/debug/qps,响应体只写纯数字,不套 JSON - 别在
expvar.Publish("qps", expvar.Func(...))里每次调用atomic.LoadUint64——虽然能用,但 Prometheus 抓取时会放大原子操作竞争 - 真实线上问题往往卡在标签设计和打点位置,而不是计数器实现本身

















