连接池调优

数据库连接是昂贵资源。建立连接要经过三次握手、认证、分配资源,耗时几十到几百毫秒。连接池就是复用连接,避免频繁创建销毁。

GORM 连接池配置

GORM 底层使用 database/sql 的连接池,配置方法:

sqlDB, err := db.DB()

// 设置最大空闲连接数
sqlDB.SetMaxIdleConns(10)

// 设置最大打开连接数
sqlDB.SetMaxOpenConns(100)

// 设置连接最大存活时间
sqlDB.SetConnMaxLifetime(time.Hour)

// 设置连接最大空闲时间
sqlDB.SetConnMaxIdleTime(time.Minute * 10)

各参数含义

MaxIdleConns

空闲连接池的最大连接数。空闲连接是指用完后放回池中等待复用的连接。

设置太小:高并发时频繁创建连接 设置太大:占用过多资源,连接长期空闲浪费

MaxOpenConns

数据库最大连接数,包括正在使用和空闲的。

设置太大:数据库压力过大,可能被拒绝连接 设置太小:请求排队等待,响应变慢

ConnMaxLifetime

连接最大存活时间,超过后连接会被关闭。

为什么要设置?长时间运行的连接可能遇到:

  • 数据库重启后连接失效
  • 网络问题导致连接中断
  • 数据库端超时关闭

建议设置比数据库 wait_timeout 小的值。

ConnMaxIdleTime

连接空闲多长时间后关闭。Go 1.15 引入。

区别于 ConnMaxLifetime:ConnMaxLifetime 是连接创建后的总存活时间,ConnMaxIdleTime 是连接空闲的时间。

参数调优思路

没有放之四海皆准的配置,要根据实际情况调整。

MaxOpenConns

参考数据库的 max_connections:

-- MySQL
SHOW VARIABLES LIKE 'max_connections';

-- PostgreSQL
SHOW max_connections;

应用 MaxOpenConns 应该小于数据库限制,留出余量给其他应用和管理连接。

如果部署多个实例,要考虑总连接数:

数据库 max_connections = 500
应用实例数 = 5
每个实例 MaxOpenConns = 500 / 5 - 余量 ≈ 80

MaxIdleConns

一般设置为 MaxOpenConns 的 1/2 到 1/4。

流量稳定的服务可以设置高一些,减少连接创建。 流量波动大的服务设置低一些,避免空闲时占用资源。

ConnMaxLifetime

建议 5 分钟到 1 小时。太短会频繁重建连接,太长可能遇到连接失效问题。

// 常见配置
sqlDB.SetConnMaxLifetime(30 * time.Minute)

ConnMaxIdleTime

建议 5-15 分钟。空闲连接保留太久没意义。

sqlDB.SetConnMaxIdleTime(10 * time.Minute)

监控连接池状态

Go 1.11 提供了连接池统计:

stats := sqlDB.Stats()
fmt.Printf("等待连接数: %d\n", stats.WaitCount)
fmt.Printf("等待时长: %v\n", stats.WaitDuration)
fmt.Printf("最大打开连接数: %d\n", stats.MaxOpenConnections)
fmt.Printf("当前打开连接数: %d\n", stats.OpenConnections)
fmt.Printf("正在使用连接数: %d\n", stats.InUse)
fmt.Printf("空闲连接数: %d\n", stats.Idle)

可以暴露为 Prometheus 指标:

// 伪代码
prometheus.NewGaugeFunc(prometheus.GaugeOpts{
    Name: "db_pool_open_connections",
}, func() float64 {
    stats, _ := sqlDB.Stats()
    return float64(stats.OpenConnections)
})

常见问题

连接泄漏

症状:OpenConnections 持续增长,直到 MaxOpenConns,然后请求阻塞。

原因:没有正确关闭 Rows 或事务。

// 错误:没有关闭 Rows
rows, _ := db.Raw("SELECT * FROM users").Rows()
for rows.Next() {
    // ...
}
// 忘记 rows.Close()

// 正确
rows, _ := db.Raw("SELECT * FROM users").Rows()
defer rows.Close()
for rows.Next() {
    // ...
}

连接超时

症状:大量 WaitCount,WaitDuration 很长。

原因:MaxOpenConns 太小,或查询太慢。

解决:增大 MaxOpenConns,或优化慢查询。

连接失效

症状:偶尔出现 "invalid connection" 错误。

原因:ConnMaxLifetime 设置太长,或数据库端关闭了连接。

解决:设置合理的 ConnMaxLifetime,启用数据库端 keepalive。

不同数据库的配置

MySQL

sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(20)
sqlDB.SetConnMaxLifetime(30 * time.Minute)

MySQL 的 wait_timeout 默认 8 小时,ConnMaxLifetime 设置小一些可以避免问题。

PostgreSQL

sqlDB.SetMaxOpenConns(50)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)

PostgreSQL 连接比较重,MaxOpenConns 不宜太大。

SQLite

sqlDB.SetMaxOpenConns(1)  // 单连接避免锁竞争

SQLite 是文件数据库,多连接写入会锁冲突。

总结

连接池调优没有银弹,关键是:

  1. 理解每个参数的含义
  2. 监控连接池状态
  3. 根据监控数据调整
  4. 考虑部署架构(实例数、数据库配置)

调优是一个持续的过程,随着业务变化要不断调整。