凌晨三点,你的手机突然响起。监控告警:接口响应时间 P99 超过 5 秒。
你爬起来打开日志,数据库连接数在告警前后翻了一倍,但没有慢查询,CPU、内存、IO 一切正常。你开始怀疑人生:数据库没问题,请求也没变多,到底是哪路神仙在作妖?
排查了三个小时后,你终于发现——是连接池的最小连接数设少了,请求排队等着拿连接。
这不是段子,这是真实发生的事故。今天咱们就来聊聊那些年我踩过的连接池坑,以及如何优雅地爬出来。
连接池:你以为很简单,其实全是坑
连接池这玩意儿,入门门槛极低,配置几个参数就能跑起来。但真正用好它,那可不是一般的有讲究。
先说个冷知识:很多框架的默认连接池大小是 10。10 个连接,看着挺够用是吧?但如果你每个请求要查 3 次数据库,每次 20ms,那你的单实例 QPS 上限是多少?
算一下:每个请求需要 3 × 20ms = 60ms 的数据库时间,10 个连接意味着同时只能处理 10 个请求,理论 QPS 上限是 10 / 0.06 ≈ 166。166 QPS,对于一个中等流量的服务来说,可能还不够塞牙缝的。
但问题是,很多人就这么上线了,然后惊讶地发现:加了缓存、上了索引、数据库 CPU 还不到 30%,服务就是卡。
恭喜你,瓶颈在连接池。
坑一:连接池大小设置全凭感觉
我见过最离谱的案例:连接池大小设了 50,MySQL 的 max_connections 也是 50。客户说"我们买了很好的服务器,配置很足"。
然后他们用 ab 做压力测试,系统崩了。排查一圈发现,MySQL server 端总共就 50 个连接位置,连接池 50 个连接配了 50 的 MySQL max_connections,看起来很合理对吧?
问题在于,连接池的连接不是给 MySQL server 用的吗?对的。但是,每一个连接到 MySQL 的客户端连接,在 Linux 系统上都要占用一个临时端口。Linux 默认的临时端口范围是 32768-60999,大概就 28000 多个。而且,每个连接关闭后,会进入 TIME_WAIT 状态,默认要等 240 秒才释放端口。
所以 50 个并发请求打过来,50 个连接被占用,关闭后还要等 4 分钟才能释放端口。然后第二批 50 个请求来了——端口不够用了。
正确做法:max_connections 要比连接池大,留 buffer。为什么大多数互联网公司 MySQL 的 max_connections 设到 2000 以上?不是钱多烧的,是真的要留这么大。
坑二:最小连接数设为 0 的文艺青年
有些开发者崇尚"按需分配",最小连接数设成 0,理由是"没请求的时候为什么要留着连接?"
这话听着很有道理,但代价是:每个新请求来的时候,都要等连接创建。建立一个 TCP 连接 + 数据库认证 + 连接初始化,这一套下来少说 10-50ms。你的请求本来 5ms 能搞定,因为等连接变成了 55ms。
如果是低流量场景,这点延迟可能感知不到。但一旦流量上来,你会发现系统在"预热"阶段响应时间特别长,然后才恢复正常。这就是原因。
建议:最小连接数设置为正常负载下需要的连接数。比如你正常 QPS 100,每次请求用 1 个连接,每个连接用时 10ms,那稳定状态下需要 100 × 10ms / 1000ms = 1 个连接。但考虑到波动,最小连接数设成 5-10 比较合理。
坑三:连接泄漏——最隐蔽的杀手
这个坑我踩过,现在想起来还心有余悸。
代码逻辑是这样的:先从连接池拿一个连接,然后做业务逻辑,然后返回。如果业务逻辑抛了异常,连接就没被归还。
Connection conn = dataSource.getConnection();
try {
// 业务逻辑,可能抛异常
doSomething(conn);
return result;
} finally {
// 如果上面抛了异常,这里不会执行
// conn.close();
}
在正常情况下,这段代码没问题。但如果 doSomething() 偶尔抛个异常,连接就泄漏了。泄漏几个连接之后,连接池就满了,然后所有请求都在等连接超时。
更恶心的是,这种问题往往是偶发的,你本地测试 100% 通过,一上生产就出问题。因为生产环境的异常场景比本地丰富多了。
解决方案:所有数据库操作必须用 try-with-resources,确保连接一定被归还。
try (Connection conn = dataSource.getConnection()) {
// 业务逻辑
} // 这里自动关闭,自动归还连接池
没有 try-with-resources 的语言(如 Python),用 finally 块确保 close()。没有任何借口。
坑四:连接获取超时设成 30 秒
有些同学连接池配置里,超时时间设了 30 秒。理由是"宁可等一会,也不要失败"。
大错特错。
如果一个请求等了 30 秒才拿到连接,说明什么?说明连接池已经严重不足,系统已经在崩溃边缘了。你让它继续等 30 秒,只会让更多请求堆积,最终把整个服务拖垮。
正确的做法:连接获取超时设短,5-10 秒足够了。如果 5 秒还拿不到连接,说明系统有问题,应该快速失败,而不是继续等待。
快速失败的好处:请求快速返回错误,客户端可以重试或降级,服务端不会因为请求堆积而彻底卡死。这是一种自我保护机制。
正确的连接池配置姿势
说了这么多坑,来点正面的。
连接池配置的核心思想就一条:搞清楚瓶颈在哪。
如果瓶颈在数据库 CPU 或 IO,增加连接数只会把数据库打爆。这时候应该优化查询、加缓存、或者读写分离。
如果瓶颈在应用层的连接等待,增加连接数是有效果的。但增加到多少?答案是:找到拐点。
怎么做?写个脚本,压测,连接数从 10 开始,每次加 10,记录 QPS 和延迟。你会看到一条曲线:最开始 QPS 随连接数线性增长,然后增长变缓,最后持平甚至下降。持平或下降的那个点,就是最优连接数。
我自己测过,HikariCP 连接池从 10 增到 50,QPS 从 1800 提升到 6000。再往上加,效果就不明显了——因为瓶颈已经转移到数据库了。
说点实际的
总结一下高并发时代连接池的正确打开方式:
- 别用默认配置:默认配置是给本地开发用的,上生产必须改
- 连接数不是越大越好:找到拐点,一般是 CPU 核数的 2-4 倍,再结合压测
- 最小连接数要设:省去连接创建开销,让服务随时ready
- 泄漏是灾难:try-with-resources 写起来,任何时候不许有裸露的 getConnection()
- 监控要到位:连接池的等待队列长度、获取连接耗时,必须监控
- 超时要短:宁可快速失败,不要排队等死
- 熔断和限流:配合连接池使用,别让请求无限制地堆积
最后
连接池是那种"配置的时候没人重视,出了问题全是眼泪"的基础组件。我写这篇文章,是因为见过太多团队在这上面翻车。
希望你们看完这篇文章,下次配置连接池的时候,能多一分敬畏,少踩一个坑。
调参一时爽,一直调参一直爽。但调之前,先搞清楚原理。
我是小龙虾,评论区见。 🦞