跳到主要内容

数据库连接池问题

问题

Go 服务出现数据库连接超时、连接池耗尽、连接泄漏等问题,如何排查和优化?

答案

常见故障表现

错误信息可能原因
too many connectionsMySQL 最大连接数用完
connection pool exhaustedGo 连接池满了,获取连接超时
driver: bad connection连接已被服务端关闭(空闲超时)
context deadline exceeded获取连接或执行查询超时
unexpected EOF网络断开/服务端异常关闭连接

Go database/sql 连接池原理

db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}

// 关键参数
db.SetMaxOpenConns(100) // 最大打开连接数
db.SetMaxIdleConns(25) // 最大空闲连接数
db.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
db.SetConnMaxIdleTime(3 * time.Minute) // 空闲连接最大存活时间

参数配置指南

参数推荐值说明
MaxOpenConns50~200不超过 MySQL max_connections / 服务实例数
MaxIdleConnsMaxOpen 的 25%~50%太少会频繁创建连接,太多浪费资源
ConnMaxLifetime3~5 分钟小于 MySQL wait_timeout(默认 8 小时),避免使用被服务端关闭的连接
ConnMaxIdleTime1~3 分钟清理长期空闲连接
常见误区
  • MaxIdleConns > MaxOpenConns 没有意义,空闲连接数不可能超过打开连接数
  • MaxOpenConns 设 0 表示无限制,生产环境必须设置上限
  • 不设 ConnMaxLifetime 会导致使用过期连接

连接泄漏排查

连接泄漏 = 借了不还:从连接池获取连接后没有正确归还。

// ❌ 连接泄漏:Rows 没有 Close
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
return nil, err
}
// 忘记 rows.Close()!连接永远不归还

var users []User
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
users = append(users, u)
}
return users, nil
}

// ✅ 正确:defer Close
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
return nil, err
}
defer rows.Close()

var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
// ❌ 连接泄漏:事务未提交/回滚
func transfer(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
return err // 事务既没 Commit 也没 Rollback → 连接泄漏
}
return tx.Commit()
}

// ✅ 正确:defer Rollback
func transfer(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // Commit 后 Rollback 是 no-op

_, err = tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
if err != nil {
return err // defer 会自动 Rollback
}
return tx.Commit()
}

监控连接池状态

// database/sql 提供 DBStats
func monitorDB(db *sql.DB) {
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
stats := db.Stats()
log.Printf(
"DB Pool: open=%d inUse=%d idle=%d waitCount=%d waitDuration=%v",
stats.OpenConnections, // 当前打开的连接数
stats.InUse, // 正在使用的连接数
stats.Idle, // 空闲连接数
stats.WaitCount, // 等待获取连接的累计次数
stats.WaitDuration, // 等待获取连接的累计时间
)
}
}()
}

将状态暴露为 Prometheus 指标:

var (
dbOpenConns = promauto.NewGauge(prometheus.GaugeOpts{
Name: "db_open_connections",
})
dbInUse = promauto.NewGauge(prometheus.GaugeOpts{
Name: "db_in_use_connections",
})
dbWaitCount = promauto.NewCounter(prometheus.CounterOpts{
Name: "db_wait_count_total",
})
)

func collectDBMetrics(db *sql.DB) {
stats := db.Stats()
dbOpenConns.Set(float64(stats.OpenConnections))
dbInUse.Set(float64(stats.InUse))
}
诊断指标
  • InUse 持续等于 MaxOpen → 连接池满了
  • WaitCount 持续增长 → 连接不够用
  • InUse 持续增长不回落 → 连接泄漏

bad connection 问题

// MySQL 默认 wait_timeout=28800(8h),连接空闲超过此时间会被服务端断开
// Go 用到了这个"死连接"就会报 driver: bad connection

// 解决:ConnMaxLifetime < MySQL wait_timeout
db.SetConnMaxLifetime(5 * time.Minute)

// 或者使用 DSN 参数自动 ping
dsn := "user:pass@tcp(host:3306)/db?timeout=5s&readTimeout=5s&writeTimeout=5s"

GORM 连接池

import "gorm.io/gorm"
import "gorm.io/driver/mysql"

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

// GORM 底层使用 database/sql,获取原始 DB 来配置连接池
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

常见面试问题

Q1: MaxOpenConns 设多少合适?

答案

  • 公式:MaxOpenConns ≤ MySQL max_connections / 服务实例数
  • 例如 MySQL max_connections=500,5 个实例 → 每个实例 MaxOpenConns ≤ 100
  • 还需结合 QPS 和查询耗时:所需连接数 ≈ QPS × 平均查询耗时
  • 过高会打垮数据库,过低会排队超时

Q2: 为什么要设 ConnMaxLifetime?

答案

  • MySQL 会关闭空闲超过 wait_timeout 的连接
  • Go 如果不感知,会使用已被关闭的连接,导致 bad connection 错误
  • 设置 ConnMaxLifetime 让 Go 主动回收老连接
  • 建议设为 MySQL wait_timeout 的一半或更短

Q3: 如何快速判断是连接泄漏还是连接池配置不足?

答案

  • 泄漏InUse 持续增长且不回落,即使业务低峰也一样
  • 配置不足:高峰期 InUseMaxOpenWaitCount 增长,低峰恢复正常
  • 泄漏需要检查代码(rows.Close、tx.Rollback)
  • 配置不足需要调大连接池或优化慢查询

Q4: database/sql 的连接池是如何工作的?

答案

  1. db.Query() 时先从空闲连接列表取连接
  2. 如果没有空闲连接且 OpenConnections < MaxOpenConns,创建新连接
  3. 如果达到上限,放入等待队列,直到有连接归还或超时
  4. 连接使用完毕后归还到空闲列表
  5. 后台定时清理超过 ConnMaxLifetime/ConnMaxIdleTime 的连接

相关链接