☰
Druid连接池报错排查:wait millis超时与createErrorCount暴增的根因与修复
2026/10/1 18:27:14 网站建设 项目流程

那天早上刚打开监控后台,就看到告警群里连着刷了十几条消息,服务的错误日志里全是同一行报错:

wait millis 60000, active 0, maxActive 50, creating 0, createErrorCount 9913

数据库连接池里一个连接都拿不到,而且连续创建了9913次都没成功,应用基本等于瘫痪状态。这条报错绝大多数人一看就慌了,因为里面既有wait millis又有maxActive,很容易让人误以为是连接池不够用,结果一上来就调连接池大小,调完照样报错。

实际上,这行日志是阿里巴巴Druid连接池的标准告警格式,它想告诉你的事情远不止"连接不够"这么简单。我后来把整个排查过程完整复盘了一遍,发现90%的人遇到这条告警都会走弯路,今天这篇就把这个问题的排查思路、根因分类和修复方案一次性讲清楚,给所有搞Java后端、跟MySQL打交道的人一个可以直接照着操作的排查手册。

1. 先看懂这行日志在说什么

排错的第一步永远是先搞懂报错信息本身。不是看到英文就慌,这行日志里的每一个字段都是有用信息,把它们拆开看,能帮你少走一大半弯路。

1.1 日志里每一个字段的真实含义

以我这次遇到的报错为例,一行日志拆开来看是这样的:

字段本次报错值真实含义
wait millis60000业务线程向连接池申请连接时,最长等待了60000毫秒(也就是60秒)
active0当前被业务占用的连接数量为0
maxActive50连接池配置的最大连接数为50
creating0当前正在创建连接的数量为0
createErrorCount9913启动以来累计创建连接失败的次数高达9913次

这里最容易让人误判的是active 0和maxActive 50这对组合。很多人一看就觉得"活跃连接是0,说明连接没被占用,那怎么还会拿不到连接呢?"然后就开始怀疑是不是连接池代码写错了。其实恰恰相反,active 0说明的不是"有连接可用",而是"当前一个连接都没能成功建立"。

打个比方,这就好比你去停车场取车,门口显示"场内空位0个,剩余车位50个",但每个车位上都停着一辆车,这50个空位全是虚的。放在连接池场景里,maxActive 50说的是连接池允许的最大连接数,active 0说的是业务正占用0个连接,而createErrorCount才是真正的关键——每次应用程序尝试跟MySQL建立新连接时,MySQL那边都没让它成功。

1.2 为什么这个报错会一直重复刷

Druid连接池的工作原理是:当业务线程来借连接时,如果池里有空闲连接就直接给;如果没有空闲连接且当前连接数还没到maxActive上限,就新建连接;如果连接数已经达到上限且所有连接都被占用,调用方就会进入等待。

正常情况下的等待时间是毫秒级的,因为连接创建很快。但是当createErrorCount开始疯狂上涨时,说明连接池每次尝试创建新连接都在失败,而且不是偶发失败,是持续失败。连接建不出来,池子里又没有空闲连接,业务线程就只能一直等到maxConnectingTimeout(也就是wait millis显示的时间,默认60秒)超时,然后异常抛出。

最坑的是,这种错误不是报一次就停。业务线程会不断来借连接,连接池会不断尝试创建,每次创建都失败,造成两个后果:一是createErrorCount持续累加,二是每条业务请求都要硬等60秒才报错,整个应用的响应时间全部被拖垮。

1.3 createErrorCount 9913 这个数字说明了什么

9913次创建失败是一个很大的数字,它说明这个故障不是刚刚发生的。连接池从应用启动开始就在反复尝试连接,在第一次报错之前,应用日志里很可能已经默默失败了几千次,只是一直没有触发告警条件,所以没人发现。

这也引出了第一个经验:连接池的报错日志优先级一定要设对,createErrorCount这类指标要做实时监控,不能等日志刷屏了才发现。很多团队的日志只关注ERROR级别,而Druid连接池的这种告警默认是在WARN级别输出的,很容易被日志系统过滤掉。

2. 踩坑现场:从看到告警到定位根因的完整过程

日志看懂了,下来才是真正的重头戏——怎么定位根因。这次排错我从看到告警到最终确认问题,前后花了差不多两个小时,中间走了不少弯路,我把整个过程还原出来,你以后遇到同类问题可以直接复用这条排查链路。

2.1 第一反应:先看连接池配置和数据库状态

我当时的第一个动作,是登录到应用服务器上用jstack抓了一下线程栈,确认是不是有死锁之类的问题导致连接一直被占用。结果线程栈显示得很清楚:所有业务线程全都阻塞在com.alibaba.druid.pool.DruidDataSource.getConnection方法上,等连接的锁,不是业务代码死锁。

然后我登录MySQL,执行了show processlist看数据库侧的状态,结果发现一个诡异的现象:MySQL的Threads_connected指标只有十几个,远没到max_connections上限,而且几乎看不到来自这台应用服务器的连接。

两边一对照,结论已经很明显了:问题不在MySQL端,也不在业务代码端,而是应用和数据库之间的连接链路出了问题。MySQL运行正常,连接池却在疯狂创建连接失败,那一定是"从应用服务器到MySQL这条路"本身不通了。

2.2 用排除法锁定问题层级

接下来我开始排查应用服务器和数据库之间的网络连通性。

第一步,我直接从应用服务器上执行:

telnet mysql-host 3306

结果端口是通的。我当时心里还纳闷,端口通的话怎么连接还失败呢?

第二步,我直接用命令行连一下MySQL试试:

mysql -h mysql-host -P 3306 -u app_user -p

结果输完密码之后直接报错:

ERROR 1040 (HY000): Too many connections

看到这个我整个人清醒了。端口通,但数据库拒绝新连接,这问题不在网络上,在MySQL的连接数限制上。

我再跑了一次show variables like 'max_connections',发现这个值配置的是200,而Threads_connected明明只有十几个,怎么会报"Too many connections"呢?后来才查到,MySQL的连接数限制不止max_connections一个,如果用户权限里单独限制了max_user_connections,同样会被拒绝。

当时我执行了:

SELECT user, host, max_user_connections FROM mysql.user WHERE user = 'app_user';

果然,这个账号的max_user_connections被设成了10,而其他来源的连接已经把10个名额占满了,应用服务器上的连接池再怎么创建都进不去。

2.3 我当时真正踩到的坑

这里必须说一下我走的弯路,因为很多人会跟我犯一模一样的错——一开始我把重心放在了连接池参数的调整上。看到wait millis 60000,我第一反应是连接等待时间是不是太长,又把maxActive从50改成100,觉得连接数不够用,实际上maxActive根本不是瓶颈。

还有一点容易被忽略:Druid连接池报错日志显示的wait millis 60000,这个60秒不是连接池配置的等待超时,而是Druid内部计算出的实际等待时间。也就是说,业务线程实实在在等了60秒才失败,不是某个配置项决定的,而是每次创建连接尝试到最终失败耗时加起来正好超过了60秒。

当时因为连接数被MySQL账号限制卡死,连接池每次创建连接都在MySQL侧被立即拒绝,按道理应该秒失败,但实际等了60秒才报错,原因在于Druid在创建连接失败后不是立即放弃,而是在同一个申请周期里会做多次重试,每次重试之间还有连接超时时间(connectTimeout)要等。

2.4 确认根因后的验证方式

定位到根因之后,修复方式就非常简单了——把app_user的max_user_connections从10调整为100,然后再从应用服务器上连接一次:

ALTER USER 'app_user'@'%' WITH MAX_USER_CONNECTIONS 100;

调整完之后,我观察了大概10分钟,发现createErrorCount不再增长,Druid连接池开始正常建立连接,业务线程阻塞消失,接口响应时间恢复到正常水平。

这里有一个验证的小技巧:改完配置后不要重启应用,直接观察Druid的监控页面。如果连接池状态从"反复创建失败"变成"连接数缓慢增长到稳定值",说明修复生效;如果改完配置仍然报错,那就继续往下查,不要急着重启应用掩盖问题。

3. 这类报错的几大常见根因和对应修复方案

我遇到的这次是MySQL账号连接数限制,但createErrorCount持续上升背后可能是一系列不同的问题。根据自己的排查经验,也参考了同行踩坑的案例,我把这类报错的常见根因做了一个分类汇总,你遇到问题的时候可以按这个清单逐项排查。

3.1 数据库连接数被打满

这是最常见的一种情形。数据库的max_connections默认值只有151,如果多个应用服务共用一个MySQL实例,连接数很容易被打满。现象就是新连接被拒绝,连接池里旧连接被回收后,新连接创建不出来,createErrorCount一路上涨。

排查命令:

show variables like 'max_connections'; show global status like 'Threads_connected'; show global status like 'Threads_created'; show global status like 'Connection_errors_max_connections';

如果Threads_connected已经接近max_connections,基本就是连接数打满了。修复方式有三种:

一是调大max_connections,但要注意这会增加MySQL的内存开销,不是越大越好;二是排查业务侧是否有连接泄漏,很多情况是代码里连接没关闭导致连接数缓慢上涨;三是对不同业务做账号级别的资源隔离,避免一个业务把连接全部占完。

3.2 账号权限或密码问题

第二种容易被忽视的情况是账号密码错误或者权限不匹配。连接池刚启动时可能还没到高峰期,或者Druid做了懒加载,不会立刻建立连接。等第一次业务请求过来,才触发连接创建,如果密码错误、账号被删除、权限被回收,就会不停地创建失败。

我朋友遇到过一例,项目上了K8s之后,密码是通过环境变量注入的,配置中心改了一次数据库密码,但应用Pod没有重新读取环境变量,还是用旧密码去连,结果连接池每隔一段时间就报一次这个错误,而且只在Pod重启后的某个时间段触发。

遇到这类问题,直接拿连接池的配置信息到命令行里试着连一次,如果命令行走不通,说明问题在账号密码本身。还要顺手检查一下权限:

SHOW GRANTS FOR 'app_user'@'%';

3.3 网络层问题:防火墙、NAT超时、DNS解析

第三种根因在网络层,也是最难排查的一类。TCP端口通,不代表连接一定能建立成功。常见的问题有:

  • 云安全组规则限制了特定IP的访问
  • 中间有NAT网关,长时间没有流量后空闲连接被回收,连接池不感知
  • DNS解析变更后,连接池缓存的旧IP已经不可达
  • MySQL侧设置了wait_timeout或interactive_timeout,空闲连接被服务端断开,客户端连接池里的连接变成死连接

我以前排查过一个诡异的问题:连接池的testOnBorrow没开,MySQL的interactive_timeout是60秒,连接池里的连接超过60秒没人用,MySQL就把连接关了。下次请求来的时候,连接池拿了一个已经失效的连接返回给业务,业务执行SQL时报"Communications link failure",但业务代码里有些地方处理了这个异常会重试,有些地方直接抛出去,导致问题时有时无,特别难查。

3.4 MySQL端的连接数限制或白名单策略

我这次遇到的就是典型情况——MySQL账号级别的max_user_connections限制。这个参数在云数据库RDS上还经常以"账号最大并发连接数"的形式出现,如果你用的是云数据库,控制台里就能看到。

另外,MySQL还有个skip-name-resolve参数,如果打开之后,用户的host字段需要配置成IP而不是域名,否则也会导致认证失败。曾经有个项目就是从自建MySQL迁移到云数据库之后疯狂报错,查了半天发现是云数据库默认开启了skip_name_resolve,而应用侧的用户授权是'app_user'@'%',这种情况下需要改为IP授权。

3.5 驱动版本和SSL握手问题

最后一种不太常见但真实存在的是MySQL驱动版本和数据库版本不匹配导致的握手失败。

比如MySQL 8.0之后默认开启了caching_sha2_password认证插件,如果你用的还是5.x版本的JDBC驱动,连接时会报认证协商失败。还有数据库的ssl配置问题,如果MySQL要求SSL连接而JDBC URL里没有加相关参数,或者版本对TLS协议支持不一致,也会出现连接建立失败。

排查方法就是看Druid日志里创建失败的堆栈信息,Druid会打出创建连接时的异常堆栈,仔细看异常类型就能判断是哪一类问题:

com.mysql.cj.exceptions.CJException: Access denied for user com.mysql.cj.exceptions.UnableToConnectException: Public Key Retrieval is not allowed

如果看到"Public Key Retrieval is not allowed",多半是MySQL 8.0 +caching_sha2_password的问题,JDBC URL里加allowPublicKeyRetrieval=true就能解决。

4. 连接池参数调优:不能只会看报错,还得会止损

说一句实在话,很多人看到createErrorCount上千的时候,第一反应都是想把连接池参数调大,但连接池参数不是拍脑袋就能定的,它跟数据库端配置、业务并发量是联动的。这个章节把Druid连接池几个关键参数的配置逻辑讲透,方便你在止损时有据可依。

4.1 参数之间是联动的

Druid连接池的几个核心参数不是孤立的,它们之间会互相影响:

  • initialSize:启动时创建的连接数,建议设置为2~5,不用太大,连接池是懒加载的,启动就塞几十个连接没有意义。
  • minIdle:最小空闲连接数,建议与initialSize一致,保证空闲时也有足够连接备用。
  • maxActive:最大活跃连接数,这个是连接池能创建连接的上限,建议值为"业务高峰期并发请求数的1.5~2倍",但必须小于MySQL账号和实例的连接数上限。
  • maxWait:获取连接的最大等待时间,默认-1是无限等待,生产环境建议设置为60000毫秒(60秒),避免业务线程无限阻塞。

这些参数联动的关键是:maxActive决定了连接池最多能建多少连接,但连接能不能建立成功取决于MySQL侧是否允许,所以你调完maxActive后如果createErrorCount还在涨,问题一定在MySQL这边。

4.2 我的参数设置顺序和经验值

在排查完根因之后,我还是对连接池参数做了一次规范化调整,顺序是这样的:

第一,先确认数据库端的实际承载能力,执行:

show variables like 'max_connections';

假设是500,再减去其他应用的占用,给当前应用分配一个合理的上限,比如200。

第二,调整Druid的maxActive为200,minIdle为20,initialSize为10,maxWait为60000。

第三,配置连接校验参数:

spring: datasource: druid: test-while-idle: true test-on-borrow: true test-on-return: false validation-query: SELECT 1 keep-alive: true phy-timeout-millis: 600000 time-between-eviction-runs-millis: 60000

这几个参数的具体含义是:test-while-idle在空闲连接被回收前做一次校验,test-on-borrow在连接被借出前校验一次,虽然会有性能损耗,但在稳定性优先的场景下强烈建议开启;keep-alive是Druid 1.1.14版本之后提供的参数,可以定期对空闲连接执行验证查询,解决MySQLwait_timeout关闭空闲连接导致连接失效的问题。

第四,开启Druid的监控,通过StatFilter和StatViewServlet实时观察连接池的运行状况,避免下次出问题只能靠猜。

4.3 监控和告警的兜底方案

连接池故障最大的问题是隐蔽性——它不会在第一时间报错,而是等业务线程全部卡住之后才通过wait millis超时暴露出来。所以,除了调优参数,建立一套连接池监控机制才能真正做到止损。

Druid内置了监控页,可以通过HTTP方式暴露出来,在Spring Boot项目里配置:

@Bean public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() { ServletRegistrationBean<StatViewServlet> registration = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); registration.addInitParameter("loginUsername", "admin"); registration.addInitParameter("loginPassword", "admin123"); registration.addInitParameter("resetEnable", "false"); return registration; }

打开http://应用地址/druid就能看到连接池的实时状态,包括当前连接数、活跃连接数、错误连接数等。更推荐的做法是把Druid的监控数据接入Prometheus,可以通过druid-spring-boot-starter结合micrometer实现,让监控平台定期采集druid_datasource_create_error_count和druid_datasource_active_count等指标,一旦createErrorCount在短时间内持续增长就立刻告警。

这套监控机制比调参重要得多。连接池参数的调整是事前的预防,监控告警才是事中快速响应的关键。如果没有监控,下一次连接池再出问题,你可能会错过最佳的止损窗口。

5. 后续思考:同类型问题怎么做到一次排查到位

这次故障排完之后,我专门花时间复盘了整个排查过程,也想清楚了一件事:像wait millis这种连接池报错,本质上不是"一个"问题,而是一类问题的入口。它背后可能是网络问题、认证问题、配额问题、驱动问题中的任何一种,如果把"报错"当成"问题"来修,永远只能头疼医头、脚疼医脚。

下面这套排查顺序,是我把这次的教训整理成的一个操作路径,以后再看到这类日志,按这个顺序来,基本可以一次性定位到位。

第一步,看Druid日志中的异常堆栈。连接创建失败时,Druid会在堆栈里打出一个SQLException,它的message字段基本能区分出是认证失败、连接拒绝还是超时。

第二步,检查MySQL侧的连接数状态。执行show global status like 'Threads_connected'、show variables like 'max_connections'、show variables like 'max_user_connections',先把"数据库还能不能接受新连接"这个问题搞清楚。

第三步,检查应用服务器到数据库的网络链路。telnet测端口、ping测主机、nc -vz测连通性,必要时用tcpdump抓包确认TCP握手是否有RST或FIN包提前终止。

第四步,检查MySQL端是否有正在执行的长事务或锁等待。一个长期未提交的事务会一直持有连接,连接迟迟不释放也会导致可用连接不足,这种问题在show processlist里能看到State为Waiting for table metadata lock或者Sleep时间特别长的连接。

第五步,如果以上都正常,再回头看驱动版本、JDBC URL配置、SSL协商等偏冷门的方向。

这次还有一个让我印象很深的点:连接池故障的定位效率,很大程度上取决于日志打印的完整度。createErrorCount只是结果,真正能帮你定位的是每次创建失败时的异常堆栈。如果你在日志里连堆栈都看不到,那排查难度会成倍放大。建议在项目里配置Druid的log-abandoned=true以及合理的connection-error-retry-attempts参数,让它出错时把详细原因打印出来,别只留给运维一个干巴巴的计数。基本上把这几步走完,这类报错就没有太多"玄学"成分了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询