数据库连接池参数怎么调 Go 侧连接池与 pgxpool 对比
连接池是"配置看起来都填了、线上却总在高峰期崩"的重灾区。多数团队的参数是从博客抄来的,没人解释过为什么是 20。本文从排队论出发,说清每个参数的作用边界,给出 Go database/sql 与 pgxpool 的实操配置和观测方法。
一、池大小的正确算法
连接数不是越大越好。数据库端每个连接都吃内存与调度,超过 CPU 核数后吞吐反而下降。经典估算(源于 PostgreSQL 社区):
core_connections ≈ CPU核数 × 2
spindle_connections ≈ spindle_count × 2 # SSD 一般按 2~4 估
再考虑放大效应:上游实例数 × 每实例池大小 ≤ DB 可承受连接数。200 个 PO 各设 25,就是 5000 连接,直接压垮 DB。经验值:单服务实例的池大小常在 5~20,而不是 100。
二、database/sql 的参数语义
db, err := sql.Open("pgx", dsn)
db.SetMaxOpenConns(20) // 上限:含 idle,超了就排队
db.SetMaxIdleConns(20) // 空闲保留:小于 MaxOpen 会频繁建断
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
几个易错点:
sql.Open不建连接,第一次Query才真连——启动时以为连上了,其实没检查。必须显式db.PingContext(ctx);MaxIdleConns < MaxOpenConns时,流量峰谷会造成"建连—关闭"抖动,PG 上表现为大量FATAL: remaining connection slots;一般让两者相等;ConnMaxLifetime必须小于中间层(ProxySQL/PgBouncer/云代理)的连接空闲上限,否则会拿到已被对端关闭的连接。
三、pgxpool 的差异
pgxpool 不基于 database/sql,语义等价但默认值与可观测性更好。
cfg, err := pgxpool.ParseConfig(dsn)
cfg.MaxConns = 16
cfg.MinConns = 4
cfg.MaxConnLifetime = 30 * time.Minute
cfg.MaxConnIdleTime = 5 * time.Minute
cfg.HealthCheckPeriod = time.Minute
pool, err := pgxpool.NewWithConfig(ctx, cfg)
st := pool.Stat()
log.Printf("total=%d idle=%d used=%d max=%d construct=%d inherit=%d",
st.TotalConns(), st.IdleConns(), st.UsedConns(), st.MaxConns(),
st.NewConnsCount(), st.MaxLifetimeDestroyCount())
pgx 独有的关键收益:CopyFrom 批量写入、原生类型(不转 Go 类型)、真正的 context 取消(database/sql 的取消依赖驱动支持)。性能上 pgx 走二进制协议,比 lib/pq 快 30% 以上。
四、必备观测:等待时间才是核心指标
连接池健康度不看"用了多少连接",而看"有多少时间在等连接"。
type poolStatsExporter struct{ db *sql.DB }
func (p *poolStatsExporter) Describe(ch chan<- *prometheus.Desc) {}
func (p *poolStatsExporter) Collect(ch chan<- prometheus.Metric) {
s := p.db.Stats()
ch <- prometheus.MustNewConstMetric(waitCount, prometheus.CounterValue, float64(s.WaitCount))
ch <- prometheus.MustNewConstMetric(waitDur, prometheus.CounterValue, s.WaitDuration.Seconds())
ch <- prometheus.MustNewConstMetric(openConns, prometheus.GaugeValue, float64(s.OpenConnections))
ch <- prometheus.MustNewConstMetric(inUse, prometheus.GaugeValue, float64(s.InUse))
ch <- prometheus.MustNewConstMetric(idle, prometheus.GaugeValue, float64(s.Idle))
}
告警条件:rate(pool_wait_count[1m]) > 0 持续出现,说明池已成为瓶颈。此时先查是不是"连接被慢事务长期占用",而不是盲目加大池。
五、把池问题误诊成"数据库慢"的经典案例
现象:接口 P99 突然从 20ms 涨到 900ms,DB 监控 CPU 只有 30%。
排查顺序:
- 看
pool_wait指标——等待时间涨到 800ms,确诊池等待; - 找占用者:
SELECT state, now()-xact_start AS dur, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY dur DESC; - 常见元凶是在一个连接上跑了 20 秒报表查询,或者代码里
tx未提交/回退导致连接长期被占。
结论永远是"缩短占用时"而不是"加连接"。加连接只是把瓶颈推到 DB 上。
六、每条连接都必须有超时
连接池挡不住慢查询把连接吃满,三层超时缺一不可:
// 1. 语句级:PG 侧兜底
pool.Exec(ctx, `SET statement_timeout = '3s'`) // 或 dsn: statement_timeout=3s
// 2. 事务级:Go 侧
func WithTx(ctx context.Context, pool *pgxpool.Pool, fn func(pgx.Tx) error) error {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return pgx.BeginFunc(ctx, pool, fn)
}
dsn 片段:connect_timeout=3&statement_timeout=3000&idle_in_transaction_session_timeout=10000
idle_in_transaction_session_timeout 尤其重要:它专门回收"开了事务却卡住不动"的连接,是防止池被耗尽最有效的一招。
七、什么时候需要 PgBouncer
单服务实例数超过几十、或存在大量短生命周期连接(Serverless/CLI 任务)时,应用侧池无法控制总连接数,这时上 PgBouncer 做连接复用聚合。
注意两种模式的行为差别:
transaction模式:每条语句后归还连接,SET/prepared statement/LISTEN/ Advisory lock 都会失效或串态,需要query_cache与server_reset_query谨慎配置;session模式:兼容性好但复用率下降。
pgx 的 prepared statement 在 transaction 模式下会报错,需要 default_query_exec_mode=simple_protocol。这类"池语义变化"是引入 PgBouncer 后最常见的事故源。
小结
正确的配置方式:先用 CPU 核数 × 2 估单实例池大小,再核对"实例数 × 池大小"的总连接是否在 DB 承载内;MaxIdle == MaxOpen,ConnMaxLifetime 对齐中间层上限;启动做 PingContext;每条语句带 statement_timeout、每个事务带 Go 侧 deadline;上线接 pool_wait_duration 指标做告警。调优顺序永远是"先缩短占用时长,再考虑扩大池"。
评论(0)
登录 后参与评论。