Go 连接池调优:空闲连接不是越多越安全
一、连接池问题常被误判成慢查询
线上接口变慢时,很多排查会先看数据库慢查询。慢查询当然重要,但连接池耗尽也会让请求卡住。请求还没进入数据库执行,就已经在应用层等待连接。这个等待如果没有独立的监控指标,很容易被误认为整个请求都慢。
有一次产品同学反馈"订单列表加载很慢"。DBA 查了一圈慢查询日志说"没有任何慢查询,所有 SQL 平均执行 15ms"。我又去看应用监控,发现 P50 是 50ms,看起来完全正常。但拉出 P99 一看——8 秒。每 100 个请求里有 1 个等了 8 秒才返回。最终排查定位:高峰期 40 个数据库连接全部占满,后续请求在 Go 的database/sql内部连接请求队列中排队。等了 7 秒才拿到一个释放的连接,实际 SQL 执行只花了 50ms。而那 7 秒的等待在任何慢查询日志里都找不到——因为请求根本没有到达数据库层,查询还没开始。
连接池调优的目标不是简单地把最大连接数调大,而是让连接数量、数据库容量和请求并发三者正确匹配。连接太少,请求会在应用层排队,用户感知延迟上升。连接太多,数据库被超额连接压垮,所有查询都变慢。两边都是坑,中间的平衡点需要持续的监控数据来校准,而不是一次设好就一劳永逸。
二、连接池有三个核心参数
常见参数包括最大打开连接数、最大空闲连接数和连接生命周期。最大打开连接数控制并发上限——这是最容易被调错的一个值。最大空闲连接数控制复用能力——太少会频繁建连增加延迟,太多则会占用数据库资源。连接生命周期用于处理连接老化和负载均衡——长时间不换的连接可能出现不可预期的网络问题。
空闲连接不是越多越安全。一个常见的误解是"连接池设大一点,留够余量",但数据库可能因为连接数过多而拒绝新连接,或者因为维护太多空闲连接而消耗内存和 CPU。同时,过多空闲连接意味着你的连接生命周期更长,底层的 TCP 连接可能已经因为中间网络设备的超时被悄悄断开了,而应用层还不知道。这种"僵尸连接"会在下次使用时立即失败,导致请求出错。
flowchart LR A[请求到达] --> B[连接池等待] B --> C[获取可用连接] C --> D[数据库执行 SQL] D --> E[归还连接到池] E --> F[空闲池] F -->|超过空闲时间| G[主动关闭连接] F -->|连接存活超过生命周期| G连接池参数不是一次设好就完事。业务量的季节性波动、上下游升级、数据库迁移都会改变合理的参数范围。建议每季度重新拉一遍 P50/P99 等待时间趋势图,如果等待时间持续攀升而 SQL 执行时间没变化,就是连接池需要调整的信号。
三、代码里要设置超时
查询必须带 context 超时,没有超时的查询会长期占用连接。rows.Close()也不能漏——结果集不关闭,连接无法及时归还。很多连接泄漏就出在这些小细节上。同时要注意,连接池的参数要和业务特征匹配。查询密集的服务需要更多连接、更短的超时;写入为主的服务连接不能太多但每个连接要更稳定。
db.SetMaxOpenConns(40) db.SetMaxIdleConns(10) db.SetConnMaxLifetime(30 * time.Minute) db.SetConnMaxIdleTime(5 * time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id, status FROM orders WHERE user_id=?", uid) if err != nil { return fmt.Errorf("query orders: %w", err) } defer rows.Close()一个容易漏掉的点是事务。事务持有的连接在提交或回滚前不会归还连接池。如果事务内有外部 HTTP 调用或消息发送,连接会一直被占用。建议在开启事务前准备好所有非数据库数据,事务内只做数据库操作,做完立刻提交或回滚。不要用事务"包裹"一次完整的业务流程。
四、观测要区分等待和执行
Go 的database/sql包提供连接池统计。重点看 wait_count 和 wait_duration——等待持续增长就是连接池成为瓶颈的信号。要把 SQL 执行耗时和连接等待耗时分开记录,才能判断问题在数据库执行还是在应用层排队。服务副本数增加后总连接数会成倍增长,20 个 Pod × 40 连接 = 800 潜在连接,很多数据库根本扛不住。读写分离场景要分池管理,写库连接更谨慎。发布和扩容时也要考虑总连接峰值。
连接池还有两个容易被忽略的指标:max_idle_closed(空闲连接超时被关闭的数量)和max_lifetime_closed(连接过期被关闭的数量)。如果这些指标频繁跳动,说明空闲时间或生命周期设得过于激进,连接在频繁建连和断连,TCP 握手开销会拖慢整体延迟。最好的状态是这两个指标平稳,大部分连接被正常复用。
五、总结
Go 连接池调优要匹配业务并发和数据库真实容量,设置连接数量、空闲时间和生命周期,并用 context 控制每次查询的超时。空闲连接不是安全感。真正的安全感来自可观测的等待时间指标、明确的超时策略和受控的数据库压力。连接池的参数应该像限流参数一样定期校准,而不是上线设一次就忘。