背景
7月28日晚,公司服务数据库降配重启,重启过程在10s之内,但是却发现Communications link failure异常却一直在持续,过了接近2小时还没有完全消失,初步断定是连接池中的无用连接没有被清理,于是通过重启应用服务重置数据连接,报错消失
讨论
有同事表示为数据库的validation-query添加select 1属性,就可以解决连接失效但是没有被回收的问题,该属性是设置链接检查异常语句,当请求数据库的时候会通过该语句检查链接的可用性。但出于好奇心我debug了下代码,最后发现事情并没有这么简单.
研究
先抛结论
- druid和hikari默认配置都会检测连接的有效性,不过默认策略不同
- 不论是druid还是hikari,在默认配置+mysql的情况下使用的都是jdbc自带的ping方式请求数据库,单纯配validation-query在mysql下不会生效
- druid默认只有线程空闲时间大于timeBetweenEvictionRunsMillis时并且新请求使用,才会ping数据库,检查连接,如果连接失效,则抛弃该连接,并自动重试。或者被DestroyTask(需要配置)扫描到,连接也会被抛弃。
- hikari在该连接请求间隔大于500ms的时候会去ping数据库,若失败则新建连接重试(有超时时间),由于500ms比较短,所以理论上使用hikari连接池的服务不太会因为数据库重启导致大量Communications link failure,除非启动时间太长超过hikari的重试时间或者并发较大。所以运维说别的服务没遇见过这个问题,我合理推测是这个原因
源码
druid校验请求有效性
com.alibaba.druid.pool.DruidDataSource#getConnectionDirect
druid可以配置三处校验连接,分别是“获取连接时”,“连接空闲时”,“连接归还时”。其对应的配置是testOnBorrow、testWhileIdle、testOnReturn。1.1.21版本默认是testWhileIdle。
当testWhileIdle为true时,会对空闲超过timeBetweenEvictionRunsMillis(默认1分钟)的线程,在下次请求时,先进行连接校验,失败则抛弃连接并重试;如果在timeBetweenEvictionRunsMillis内请求(被保活任务校验通过,也算被请求,这个很重要),则不会校验连接的可用性。因此,假如连接在timeBetweenEvictionRunsMillis内失效,并且有请求使用了该连接,则请求会直接失败,连接被抛弃,并抛出异常。
注意上图lastActiveTimeMills的取值逻辑,它会在lastActiveTimeMills和lastKeepTimeMills中取一个较大值。
下面展示下druid校验连接的代码
com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker
可以看到mysql中usePingMethod变量决定了是用ping还是自定义的validationQuery,那么usePingMethod是哪里来的捏,看下面该MySqlValidConnectionChecker的构造函数。
usePingMethod是否为true取决于使用的jdbc版本是否支持ping。实测用的是com.mysql.cj.jdbc.ConnectionImpl,它是有pingInternal方法的,因此该值为true
hikari检测连接有效性
com.zaxxer.hikari.pool.HikariPool#getConnection
在连接空闲超过500ms(默认)的时候,就会ping一下数据库
如果isUseJdbc4Validation为true则通过jdbc驱动自带的ping方法校验连接可用性,否则使用connectiontestQuery
com.mysql.cj.jdbc.ConnectionImpl#isValid
那么hikari怎么让isUseJdbc4Validation为false呢,观其源码,答案是设置hiari的connectinTestQuery
druid的bug
那么问题来了,到底是什么导致了线上废弃连接一直没有被回收呢?
答案是druid的线程保活任务对连接校验时忽略了非SQLException异常,且默认校验boolean值为true
该问题存在于1.2.6以下版本的druid,并且在druid的github上可以查到该issue
https://github.com/alibaba/druid/issue/4227
下图红框部分可以清楚地看到,假如数据库主动断开连接,抛出IO异常或者socket异常,则会被isValidConnection直接跑出来,并且被catch块吞掉,此时result变量还是为true,即校验成功。
一旦保活任务校验连接成功,则会刷新当前连接的lastKeepTime,由于lastKeepTime被刷新,所以testWhileIdle也没有生效,导致连接直到被请求使用抛出异常了,才被丢弃
在升级版本到1.2.6以上后,可恢复这个问题~~