背景与问题界定

周一早晨的告警群突然被MySQL连接数报警刷屏——某订单服务的数据库连接数在30分钟内从200飙升至3000,直接触发数据库端的max_connections限制,导致新建连接全部被拒绝。紧急重启服务后连接数恢复正常,但连接数再次以每分钟50个的速度持续增长。通过netstat排查发现大量TIME_WAIT状态的连接未被复用,数据库连接池的活跃连接远低于已建立连接数。

这是典型的连接泄漏场景。Go的database/sql包维护了一个自动的连接池,理论上可以复用连接,但在实际场景中,事务未提交或回滚、context超时后连接未正确归还、长事务占用连接不释放、第三方库对连接的生命周期管理不当等,都会导致连接池"虚假消耗"。更深层的问题是,慢查询会长时间占用连接,而连接数用尽后其他正常请求因获取不到连接而超时,最终形成"连接风暴"。

目标拆解与工程约束

  1. 连接池参数必须与服务特征匹配:MaxOpenConns、MaxIdleConns、ConnMaxLifetime等参数的设置取决于服务的QPS、单次查询耗时和实例数。过高会导致DB连接资源浪费,过低则导致请求排队超时。需要基于实际压测数据进行调优。

  2. 连接泄漏需要自动检测和告警:不能等连接数打满max_connections才发现问题。需要实时监测活跃连接数、空闲连接数和等待获取连接的请求数,在异常趋势出现时及时告警。

  3. 慢查询归因需要精确到代码行和调用栈:仅靠DB侧的慢查询日志只能定位SQL,无法定位到Go代码中的调用位置。需要在Go侧实现SQL执行追踪,关联请求ID、goroutine ID和调用栈。

  4. 需要在应用层和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状态,允许少量探测请求通过:

type CircuitBreaker struct {
    failureCount    atomic.Int64
    successCount    atomic.Int64
    state           atomic.Int32 // 0=closed, 1=open, 2=half-open
    threshold       float64     // 错误率阈值,默认0.5
    recoveryTimeout time.Duration
    lastFailure     atomic.Int64
}

func (cb *CircuitBreaker) Allow() bool {
    switch cb.state.Load() {
    case 0: // closed
        return true
    case 1: // open
        if time.Since(time.Unix(0, cb.lastFailure.Load())) > cb.recoveryTimeout {
            cb.state.Store(2) // half-open
            return true
        }
        return false
    case 2: // half-open
        return true
    }
    return false
}

实施路径与关键决策

  • 第一阶段:基础监控覆盖。所有MySQL连接池接入PoolMonitor,上报Stas到Prometheus。建立Grafana dashboard,展示连接池的健康状态。决策:连接使用率超过80%触发Warning,超过95%触发Critical告警。

  • 第二阶段:SQL追踪集成。将TracedDB封装替换裸sql.DB,通过编译选项控制是否开启追踪。设置慢查询阈值为200ms(可根据业务调整)。决策:慢查询日志必须关联trace_id,支持在链路追踪系统中直接溯源。

  • 第三阶段:熔断保护落地。连接池熔断器与限流器协同工作,当熔断时返回明确的ErrCircuitOpen错误码,限流器应优先处理非DB请求。决策:熔断触发后,降级路径(如读取缓存)自动激活。

  • 第四阶段:长事务治理。引入事务超时机制,所有事务声明时附带context.WithTimeout。超过30秒未提交的事务自动回滚并告警。决策:事务对象纳入PoolMonitor的监控范围,记录每个事务的存活时间和创建的调用栈。

验证指标与可持续迭代

验证指标:连接数从3000峰值降至稳定200以内;慢查询从发现到定位调用栈的时间从小时级降至分钟级;熔断器触发后,服务P99延迟上升控制在200%以内而非无限增长;因连接泄漏导致的P0事故清零。可持续迭代方面,每周运行连接池压力测试,验证参数配置的适应性。建立SQL性能基线,每次发布都自动对比慢查询数量的变化。针对被熔断的路径,建立故障演练机制,确保降级逻辑持续有效。

工程落地思考

数据库连接池的治理往往是"平时没人管、出事后才想起来"的领域。但实际上,连接池是最容易通过指标化和自动化进行治理的基础设施之一——连接数、活跃数、等待数、等待时间这四个指标已经足够覆盖90%以上的问题场景。另一个容易被忽视的点是连接池参数不是"配一次就完事"的静态配置:随着业务数据量的增长和查询模式的演变,MaxOpenConns从200调到500再到800可能是正常的变化轨迹。定期回顾连接池指标并调整配置,应该像日常巡检一样融入运维流程。