数据库连接池问题
问题
Go 服务出现数据库连接超时、连接池耗尽、连接泄漏等问题,如何排查和优化?
答案
常见故障表现
| 错误信息 | 可能原因 |
|---|---|
too many connections | MySQL 最大连接数用完 |
connection pool exhausted | Go 连接池满了,获取连接超时 |
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) // 空闲连接最大存活时间
参数配置指南
| 参数 | 推荐值 | 说明 |
|---|---|---|
MaxOpenConns | 50~200 | 不超过 MySQL max_connections / 服务实例数 |
MaxIdleConns | MaxOpen 的 25%~50% | 太少会频繁创建连接,太多浪费资源 |
ConnMaxLifetime | 3~5 分钟 | 小于 MySQL wait_timeout(默认 8 小时),避免使用被服务端关闭的连接 |
ConnMaxIdleTime | 1~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持续增长且不回落,即使业务低峰也一样 - 配置不足:高峰期
InUse达MaxOpen、WaitCount增长,低峰恢复正常 - 泄漏需要检查代码(rows.Close、tx.Rollback)
- 配置不足需要调大连接池或优化慢查询
Q4: database/sql 的连接池是如何工作的?
答案:
db.Query()时先从空闲连接列表取连接- 如果没有空闲连接且
OpenConnections < MaxOpenConns,创建新连接 - 如果达到上限,放入等待队列,直到有连接归还或超时
- 连接使用完毕后归还到空闲列表
- 后台定时清理超过
ConnMaxLifetime/ConnMaxIdleTime的连接