十五年数据库相关经验,做过 DBA、架构师、技术顾问。不求"颠覆",只求"靠谱"。
翻过很多文档,也踩过很多坑。做 DBA 这些年,凌晨被叫醒的次数里,有三分之一跟连接池有关。
连接池这东西,配置好了默默无闻,配置错了要命。最常见的两个极端:一个是"连不上"------应用报连接超时,业务全挂;另一个是"连太多"------数据库被连接数打满,所有请求排队等。
这篇文章我把连接池相关的排查方法论整理出来,从"看到报错"到"定位配置问题",每一步都说清楚。
一、先分清两种报错:连不上 vs 连太多
接到连接池相关的告警,第一步是分清属于哪种情况。
连不上(连接耗尽):应用层报"无法获取数据库连接"或"连接超时"。根因通常是连接池里的连接不够用,或者数据库拒绝了新连接。
连太多(连接数打满):数据库层报"too many connections"或类似错误。根因通常是连接池配置过大,或者应用有连接泄漏。
怎么快速区分? 看报错来源。如果报错是应用层抛出的(比如 HikariCP 的 Connection is not available, request timed out),是"连不上"。如果报错是数据库返回的(比如 ERROR 1040: Too many connections),是"连太多"。
我的习惯 :接到连接告警,先登录数据库看一眼当前连接数。
SHOW PROCESSLIST(MySQL)或者SELECT count(*) FROM pg_stat_activity(PostgreSQL)。这个数字能帮你快速判断方向。
二、"连不上"排查------连接为什么不够用
应用报连不上,根因通常在以下几个方向。
原因 1:连接池大小配置太小
这是最直接的原因。连接池的最大连接数设得太小,扛不住当前的并发量。
举个例子:连接池最大连接数设了 10,但应用有 20 个线程同时需要数据库连接。多出来的 10 个线程只能排队等。如果等了太久(超过连接超时时间),就报超时错误。
怎么判断? 看连接池的监控指标。HikariCP 有 activeConnections 和 idleConnections。如果 activeConnections 持续等于 maximumPoolSize,说明连接池已经满了。
怎么解决? 调大连接池的最大连接数。但要注意,不能无限制调大------数据库本身有最大连接数限制,而且连接数太多会消耗数据库的内存和 CPU。
经验公式:连接池大小 = ((核心数 × 2) + 有效磁盘数)。这是一个经验起点,具体还要根据实际压测结果调整。
原因 2:慢查询占住了连接
连接池大小没问题,但还是连不上。这时候要看看是不是慢查询把连接占住了。
场景:连接池有 50 个连接,正常情况下够用。但突然来了几条慢查询,每条查询执行 30 秒。这 30 秒里,这些连接被占住不能释放。新的请求进来,连接池里没有可用连接,只能等。如果等的时间超过超时阈值,就报超时。
怎么判断? 在数据库端看当前活跃会话,找执行时间长的 SQL。如果发现有 SQL 执行了十几秒甚至几十秒还没结束,大概率就是它占住了连接。
怎么解决? 优化慢查询(参考上一篇的方法)。同时给连接池设置合理的 maxLifetime 和 idleTimeout,让占住太久的连接能被强制回收。
原因 3:外部依赖慢导致连接被占用
查询本身不慢,但应用层处理逻辑慢,连接也被占住了。
场景:应用从连接池拿到连接,执行了一条 SQL(耗时 10ms),然后拿着这个连接去做外部 HTTP 调用(比如调一个第三方 API,耗时 5 秒)。在这 5 秒里,连接被占住不能还给连接池。如果有大量这样的请求,连接池很快就被耗尽。
怎么判断? 看应用层的耗时分布。如果数据库查询只占一小部分时间,大部分时间花在外部调用上,就是这个原因。
怎么解决? 缩短连接的持有时间------先做外部处理,最后再开连接、执行 SQL、立刻释放。不要拿着数据库连接做外部调用。
原因 4:数据库端限制了连接数
连接池配置没问题,但数据库本身拒绝了新连接。
MySQL :max_connections 参数限制了最大连接数。默认是 151。如果多个应用连同一个数据库实例,总连接数很容易超过这个值。
PostgreSQL :max_connections 默认是 100。
怎么判断? 在数据库端看当前连接数和最大连接数。MySQL 用 SHOW VARIABLES LIKE 'max_connections' 和 SHOW STATUS LIKE 'Threads_connected'。
怎么解决? 调大数据库的 max_connections,但要注意这会增加数据库的内存消耗。更好的方式是在应用层做连接数管控,不要让应用无限制地连数据库。
三、"连太多"排查------连接为什么泄漏
数据库报连接数打满,根因通常是连接泄漏------连接被借走了,但没有还回来。
泄漏原因 1:异常路径没有释放连接
这是最常见的连接泄漏场景。
场景:代码里从连接池拿了连接,执行 SQL,然后还回去。正常路径没问题。但如果执行 SQL 时抛了异常,异常处理逻辑里没有释放连接,这个连接就"丢"了------既不在连接池里,也不在数据库的活跃会话里(它还在,但没人管它了)。
怎么判断? 看数据库端的活跃连接数。如果连接数持续增长,但应用层的并发量没有增长,大概率是连接泄漏。
怎么解决? 确保所有代码路径都释放连接。最可靠的方式是用 try-finally 或者 try-with-resources(Java)来管理连接生命周期。不管有没有异常,finally 里一定要释放连接。
这个问题在老代码里特别常见。很多年前的代码没有考虑到异常路径的连接释放,上线时不暴露,运行几个月后连接慢慢泄漏,最终打满。
泄漏原因 2:连接池的 leak detection 没开
连接泄漏发生了,但没人知道。
HikariCP 有一个 leakDetectionThreshold 参数,用来检测连接泄漏。如果连接被借走超过这个时间还没还回来,HikariCP 会打印一条警告日志,告诉你"哪个方法拿了连接没还"。
怎么配置? 设置 leakDetectionThreshold = 60000(60 秒)。意思是如果连接被借走超过 60 秒没还,就报警。
我的习惯:生产环境这个参数一定要开。它不解决泄漏问题,但能帮你快速定位泄漏的代码位置。没有它,你只能靠猜。
泄漏原因 3:长事务持锁
连接没有泄漏,但事务开得太久,连接一直被占用。
场景:应用开启了一个事务,执行了几条 SQL,然后等着用户操作(比如在 Web 界面上等用户确认)。在用户确认之前,这个事务一直开着,连接也一直占着。如果有大量用户同时操作,连接很快就被耗尽。
怎么判断? 在数据库端看长事务。MySQL 查询 information_schema.innodb_trx,找 trx_started 时间很早但还没提交的事务。
怎么解决? 缩短事务的生命周期。事务里不要等用户操作,不要做外部调用。先把外部处理做完,最后再开事务、快速更新、立刻提交。
四、连接池配置参数------关键参数说明
不管用哪种连接池(HikariCP、Druid、或者其他),核心参数就这几个。理解了它们的含义,配置就不会出错。
核心参数
| 参数 | 含义 | 建议值 |
|---|---|---|
| 最大连接数 | 连接池里最多能有多少个连接 | 根据数据库能力和并发量综合评估,一般 20-100 |
| 最小空闲连接数 | 连接池里至少保持多少个空闲连接 | 一般设为最大连接数的一半左右 |
| 连接超时时间 | 从连接池借连接时,最多等多久 | 3-10 秒。太短了容易误报,太长了用户等不起 |
| 连接最大生命周期 | 连接创建后最多存活多久 | 比数据库的 wait_timeout 短一些,避免拿到过期连接 |
| 空闲超时时间 | 空闲连接多久没被使用就回收 | 10-30 分钟。太长浪费资源,太短频繁创建销毁 |
配置建议
- 最大连接数不要设太大。一个数据库实例能高效支撑的连接数是有限的。设太大反而会导致连接争用和上下文切换开销。
- 连接超时时间设合理。3-10 秒是比较合理的范围。设成 30 秒或 60 秒的话,用户等不了那么久。
- 最大生命周期一定要设。很多连接池默认不设置这个参数,导致连接一直不回收。如果数据库端有超时断开机制(比如 MySQL 的 wait_timeout),连接池里的连接可能会拿到已经断开的连接。
- 开启泄漏检测。生产环境务必开启,阈值设 60 秒左右。
五、连接池监控------日常巡检清单
连接池问题最好的处理方式是在出问题之前发现。以下是日常巡检要点:
- 看连接使用率。活跃连接数占总连接数的比例。如果长期超过 80%,说明连接池偏小了。
- 看等待队列长度。如果有请求在排队等连接,说明连接不够了。
- 看连接创建/销毁频率。如果频繁创建和销毁连接,说明连接池大小不合适或者泄漏了。
- 看连接获取耗时。正常情况下应该是毫秒级。如果出现秒级的获取耗时,说明连接池快满了。
- 看数据库端连接数。和连接池的活跃连接数对比,如果有明显差异,可能有泄漏。
六、总结
连接池排查就两个方向:连不上看容量和慢查询,连太多看泄漏和长事务。
连接池配置的核心原则是:够用就好,不要贪大。连接数太大反而会带来性能问题。
预防连接问题的关键是:泄漏检测要开、事务要短、监控要看。
处理过上百次连接池问题,每次回到这套流程,问题都能解决。不需要什么高级工具,耐心和方法论就够了。
下一篇我会讲数据库备份恢复实战,把运维高频问题系列收个尾。
有问题评论区见。
十五年数据库领域老炮。关注我,一起把数据库这件事搞明白。