Druid连接池报错排查:wait millis超时与createErrorCount暴增的根因与修复

发布时间:2026/10/1 18:27:17
Druid连接池报错排查:wait millis超时与createErrorCount暴增的根因与修复 那天早上刚打开监控后台就看到告警群里连着刷了十几条消息服务的错误日志里全是同一行报错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当前被业务占用的连接数量为0maxActive50连接池配置的最大连接数为50creating0当前正在创建连接的数量为0createErrorCount9913启动以来累计创建连接失败的次数高达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里加allowPublicKeyRetrievaltrue就能解决。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为200minIdle为20initialSize为10maxWait为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 ServletRegistrationBeanStatViewServlet druidStatViewServlet() { ServletRegistrationBeanStatViewServlet 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-abandonedtrue以及合理的connection-error-retry-attempts参数让它出错时把详细原因打印出来别只留给运维一个干巴巴的计数。基本上把这几步走完这类报错就没有太多玄学成分了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询