Go数据库连接池治理:从长连接泄漏到慢查询归因
背景与问题界定 周一早晨的告警群突然被MySQL连接数报警刷屏——某订单服务的数据库连接数在30分钟内从200飙升至3000,直接触发数据库端的max_connections限制,导致新建连接全部被拒绝。紧急重启服务后连接数恢复正常,但连接数再次以每分钟50个的速度持续增长。通过netstat排查发现大量TIME_WAIT状态的连接未被复用,数据库连接池的活跃连接远低于已建立连接数。 这是典型的连接泄漏场景。Go的database/sql包维护了一个自动的连接池,理论上可以复用连接,但在实际场景中,事务未提交或回滚、context超时后连接未正确归还、长事务占用连接不释放、第三方库对连接的生命周期管理不当等,都会导致连接池"虚假消耗"。更深层的问题是,慢查询会长时间占用连接,而连接数用尽后其他正常请求因获取不到连接而超时,最终形成"连接风暴"。 目标拆解与工程约束 连接池参数必须与服务特征匹配:MaxOpenConns、MaxIdleConns、ConnMaxLifetime等参数的设置取决于服务的QPS、单次查询耗时和实例数。过高会导致DB连接资源浪费,过低则导致请求排队超时。需要基于实际压测数据进行调优。 连接泄漏需要自动检测和告警:不能等连接数打满max_connections才发现问题。需要实时监测活跃连接数、空闲连接数和等待获取连接的请求数,在异常趋势出现时及时告警。 慢查询归因需要精确到代码行和调用栈:仅靠DB侧的慢查询日志只能定位SQL,无法定位到Go代码中的调用位置。需要在Go侧实现SQL执行追踪,关联请求ID、goroutine ID和调用栈。 需要在应用层和DB层之间建立熔断机制:当DB连接池连续N次获取连接超时时,应触发熔断降级,快速失败而非无限排队,防止连接风暴破坏整个集群的可用性。 方案设计 核心方案围绕三个组件:连接池监控器、SQL追踪中间件和熔断保护器。 连接池监控器定期采样stats.PoolStats,计算关键指标并上报至Prometheus: type PoolMonitor struct { db *sql.DB interval time.Duration metrics map[string]prometheus.Gauge } func (m *PoolMonitor) Run(ctx context.Context) { ticker := time.NewTicker(m.interval) for { select { case <-ticker.C: stats := m.db.Stats() m.metrics["db_open_connections"].Set(float64(stats.OpenConnections)) m.metrics["db_in_use"].Set(float64(stats.InUse)) m.metrics["db_idle"].Set(float64(stats.Idle)) m.metrics["db_wait_count"].Set(float64(stats.WaitCount)) m.metrics["db_wait_duration"].Set(float64(stats.WaitDuration.Milliseconds())) m.metrics["db_max_open"].Set(float64(stats.MaxOpenConnections)) case <-ctx.Done(): ticker.Stop() return } } } SQL追踪中间件基于database/sql的钩子机制,在每个查询执行前后记录时间、调用栈和请求上下文: type TracedDB struct { *sql.DB slowThreshold time.Duration } func (db *TracedDB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { start := time.Now() caller := captureCaller(2) // 获取调用栈 rows, err := db.DB.QueryContext(ctx, query, args...) elapsed := time.Since(start) if elapsed > db.slowThreshold { traceID := GetTraceID(ctx) log.Warnw("slow query detected", "trace_id", traceID, "query", query, "args", args, "elapsed_ms", elapsed.Milliseconds(), "caller", caller, "rows_affected", getRowsAffected(rows), ) } return rows, err } 熔断保护器基于Google SRE的熔断算法,当连续错误率超过阈值时进入open状态,拒绝所有请求;经过冷却窗口后进入half-open状态,允许少量探测请求通过: ...