本文详细分享了通过分析Druid组件源码,排查和解决了Druid连接偶发性失效问题的过程。
某日,新机房部署了A应用,并拉入了少量的流量进行验证。随后,实例开始偶现JDBC CommunicationException异常。具体报错信息如下:
Cause: com.mysql.cj.jdbc.exceptions.CommunicationsException: The last packet successfully received from the server was 1,838,049 milliseconds ago. The last packet sent successfully to the server was 1,838,050 milliseconds ago. is longer than the server configured value of 'wait_timeout'. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts or using the Connector/J connection property 'autoReconnect=true' to avoid this problem.
根据上述报错信息,我们可以看出是因为A应用实例设置的Druid连接空闲时间时长超过了MySQL的连接空闲时间(wait_timeout),导致虽然MySQL服务端主动断开了连接,但是客户端A应用未能感知到连接已失效,仍然在使用,最终报错。
Druid的报错信息也同时给出了如下修复建议:
为保证客户端应用的连接空闲超时时间比MySQL服务端的短,我们需确保应用中druid的timeBetweenEvictionRunsMillis、minEvictableIdleTimeMillis和MySQL的wait_timeout这三个配置满足以下公式:
timeBetweenEvictionRunsMillis + minEvictableIdleTimeMillis < wait_timeout
timeBetweenEvictionRunsMillis + maxEvictableIdleTimeMillis < wait_timeout
timeBetweenEvictionRunsMillis 1分钟 + maxEvictableIdleTimeMillis 7小时 > wait_timeout 30分钟
很显然,A应用的相关配置并不满足我们上面总结的两个公式,这样就导致了当应用A的Druid与MySQL之间的连接超过30分钟无请求时,MySQL端废弃了连接,但Druid连接池并没有废弃此连接,仍然在使用,最终导致了上述的偶发报错。
修复完成后,经过运行测试,我们发现每天还是会零星出现了1到2次类似报错(下述称作问题2)。难道上面解决方案没生效?我们接着分析、排查。
通过对比数据库连接空闲时间与Druid连接空闲时间的情况,确实发现Druid执行了检测SQL,但是MySQL数据库连接并没有被激活的情况。到底是哪里出现问题了呢?此时我们仔细观察Druid检测SQL执行情况图可以看到,Druid是上午5点-6点半左右一直没有执行检查SQL,此时正好存在问题2报错,为什么时间这个巧合?上午5点整,难道是有什么定时任务导致此问题?
Druid检测SQL执行情况
MySQL数据库连接空闲时间
带着这些疑问,我们看下A应用上午5点钟确实有个定时任务在执行某个接口,既然找到了怀疑对象,那我们分析、测试下这个接口,看它有是否会导致数据库Druid连接被激活,MySQL连接未被激活的问题。
接口执行情况如下图所示:
此接口使用到了sharding-jdbc进行了分库分表查询,仅查询了sharding0、sharding1、sharding2、sharding3数据库中的某表。
接下来我们尝试请求此接口,然后持续观察应用的Druid与MySQL的连接空闲时间,看下有什么发现。
通过测试,我们确实发现在持续请求此接口时,确实存在Druid连接被激活,MySQL数据库的连接未被激活的场景。
3.6、Debug接口代码
既然找到了罪魁祸首,那我们在逐步debug这个接口查询过程的代码有何特殊之处,会导致出现如此特殊的情况.
结合上面的debug,发现个奇怪的现象,此接口仅查询了sharding0、sharding1、sharding2、sharding3数据库中的某表,并没有查询ds数据库,为什么会查询这个库?原来是shardingJDBC将不分库分表的库与分库分表的库合并为一个统一的逻辑数据源,导致在MyBatis中获取多结果集时激活了错误数据库(不分库分表的数据库ds)的连接,进而导致Druid连接维护相关配置的失效。
既然问题找到了,我们看下对于部分数据库分库分表的场景,xxl-job官方是如何建议的,官方给出了两个方法如下图。既然我们使用了方法1出现了上述问题2,于是我们按照方法2将不参与分库分表的数据源独立于ShardingSphere之外,在应用中使用多个数据源分别处理分片和不分片的情况,问题得到彻底解决。
上述组件版本
组件名 | 版本 |
---|---|
druid | 1.2.5 |
sharding | 3.0.0.M3 |
mybatis | 3.4.4 |
Druid重要参数介绍
组件名 | 参数 | 作用 | 默认 |
---|---|---|---|
Druid | testWhileIdle | 建议配置为true,不影响性能,并且保证安全性。在连接池中申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 | 大多数版本为True |
Druid | timeBetweenEvictionRunsMillis | 1) Destroy线程会检测连接的间隔时间,每隔这个值的时间就会执行一次DestroyTask。2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明。 | 大多数版本是1分钟 |
Druid | keepAlive | 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行探活操作此参数在1.0.28以上的版本才支持 详细说明参考官方文档。 | false |
Druid | validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。 | select 1 |
Druid | minEvictableIdleTimeMillis | 连接空闲时间大于该值时关闭空闲连接大于minIdle的连接,类似hikaricp的idleTimeout。 | 30分钟 |
Druid | maxEvictableIdleTimeMillis | 连接空闲时间大于该值时不管minIdle都关闭该连接,类似hikaricp的maxlifetime(低版本不支持)。 | 7小时 |
Mossad-K,基础框架研发专家,主要负责信也科技容器云相关工作。
druid源码:
https://github.com/alibaba/druid
xxl-job官方文档:
https://www.xuxueli.com/xxl-job