一、前言
最近在捣鼓演示利用的时候发现一个druid连接池引起的线程blocked问题。先申明下,这个问题是druid 1.1.23版本之前的一个bug,并不是说druid存在显著的性能问题。
github上其实曾经有相干的issue:https://github.com/alibaba/dr...;也能够关注下druid各版本的release信息:https://github.com/alibaba/dr...。
而我在发现这个问题的时候并不知道这其实是个曾经修复了的bug,因而记录了一些剖析过程的线索,明天次要分享下问题排查的思路。
二、Let‘s Go
(一)重大的Thread Blocked
事件起源于筹备客户演示环境时的一轮压测(验证环境)。在压测过程中,发现有一个拜访数据库的接口响应工夫异样的高,尽管成心做了循环查库【一次http申请,10次MySQL查问】的操作,然而对于只是进行简略查问的SQL来说,接口的响应工夫显得很不失常。
- 首先查看了下应用服务的资源应用状况,一切正常;
- 查看jvm运行状况。在热点办法【办法耗费CPU资源的状况】中,看到zip压缩相干的办法始终在耗费CPU资源时排在首位:把办法调用链开展,发现是类加载引起的;【能够必定,利用哪里必定有问题】
- 线程dump,查看线程都在干什么,并看看类加载相干残缺的线程栈。在thread dump中发现了大问题,97%的业务线程都处于blocked状态:
- 剖析线程dump,寻找blocked的次要起因。发现就是类加载操作导致的大量线程blocked。而类加载操作居然是druid相干的办法导致的(前面会提到,其实连贯检测导致的);
我的项目中应用的druid连接池局部配置参数:
initialSize: 5 minIdle: 5 maxIdle: 5 maxActive: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL poolPreparedStatements: true testWhileIdle: true // 获取连贯时检测 testOnBorrow: true // 偿还连贯时检测 testOnReturn: true # 配置监控统计拦挡的filters,去掉后监控界面sql无奈统计,'wall'用于防火墙 filters: stat,wall,slf4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000
能够看到,这边把连贯检测的配置都加上,也就是在获取连贯,偿还连贯,以及连贯闲暇阶段都会进行连贯检测。【思考到性能影响:个别不开启testOnReturn
和testOnReturn
,而应用testWhileIdle
配合timeBetweenEvictionRunsMillis
进行闲暇连贯检测】
接下来,简略剖析下druid的局部源码,以及druid 1.1.23版本之前的bug的相干源码。
或者能够间接跳转至上面 getLastPacketReceivedTimeMs章节,间接切入正题;
(二)连贯检测局部源码
1. getConnectionDirect - 连贯检测入口
调用getConnection()
获取连贯后,最终会调用com.alibaba.druid.pool.DruidDataSource#getConnectionDirect
办法。在getConnectionDirect
办法中,就会判断是否走连贯检测的逻辑,上面是次要源码(篇幅起因其余分支逻辑细节去掉了,可自行浏览):
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException { int notFullTimeoutRetryCnt = 0; for (;;) { // DruidPooledConnection封装了物理连贯对象 DruidPooledConnection poolableConnection; try { // 这是连贯检测的外围代码所在 poolableConnection = getConnectionInternal(maxWaitMillis); } catch (GetConnectionTimeoutException ex) { if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) { notFullTimeoutRetryCnt++; if (LOG.isWarnEnabled()) { LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt); } continue; } throw ex; } // 是否开启了获取连贯时检测(依照下面的配置,首先会进入这段代码) if (testOnBorrow) { boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); } discardConnection(poolableConnection.holder); continue; } } else { if (poolableConnection.conn.isClosed()) { discardConnection(poolableConnection.holder); // 传入null,防止反复敞开 continue; } // 是否开启了闲暇检测,会依据闲暇工夫,判断是否进行连贯检测 if (testWhileIdle) { // ...... } } // 是否开启了removeAbandoned参数(不举荐开启,会获取线程栈信息) if (removeAbandoned) { // ...... } if (!this.defaultAutoCommit) { poolableConnection.setAutoCommit(false); } return poolableConnection; } }
2. testConnectionInternal - 连贯检测外围代码
com.alibaba.druid.pool.DruidAbstractDataSource#testConnectionInternal
是连贯检测的外围代码。开启了testOnReturn
和testOnReturn
后,外围的连贯测试逻辑都在这。
两个要害办法:
validConnectionChecker.isValidConnection()
,实际上调用的是com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker#isValidConnection()
。次要做两件事:1、先发送ping信息给mysql服务器,检测tcp连贯可用性;2、执行validationQuery配置的SQL查问语句;
MySqlUtils.getLastPacketReceivedTimeMs()
,用于获取连贯的闲暇工夫。这个办法也是明天thread blocked的首恶;
protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) { // ......其余 try { if (validConnectionChecker != null) { // isValidConnection真正做检测的办法,会做两件事 // 1. 先发送ping信息给mysql服务器,检测tcp连贯可用性; // 2. 执行validationQuery配置的SQL查问语句; boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout); long currentTimeMillis = System.currentTimeMillis(); if (holder != null) { holder.lastValidTimeMillis = currentTimeMillis; holder.lastExecTimeMillis = currentTimeMillis; } // 如果是mysql数据库,且通过了连贯测试,就进入上面的代码 if (valid && isMySql) { // unexcepted branch // 1.1.23之前的bug所在 long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn); if (lastPacketReceivedTimeMs > 0) { long mysqlIdleMillis = currentTimeMillis - lastPacketReceivedTimeMs; if (lastPacketReceivedTimeMs > 0 // && mysqlIdleMillis >= timeBetweenEvictionRunsMillis) { discardConnection(holder); String errorMsg = "discard long time none received connection. " + ", jdbcUrl : " + jdbcUrl + ", version : " + VERSION.getVersionNumber() + ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis; LOG.warn(errorMsg); return false; } } } if (valid && onFatalError) { lock.lock(); try { if (onFatalError) { onFatalError = false; } } finally { lock.unlock(); } } return valid; } // ......其余 }}
3. getLastPacketReceivedTimeMs - 线程阻塞的首恶,bug所在
com.alibaba.druid.util.MySqlUtils#getLastPacketReceivedTimeMs
- 1.1.23版本之前的实现
/** * druid 1.1.23版本之前的源码实现 */public static long getLastPacketReceivedTimeMs(Connection conn) throws SQLException { // 第一次执行这段代码,class_connectionImpl是初始值null,因而会进入if逻辑 if (class_connectionImpl == null && !class_connectionImpl_Error) { try { // 写死mysql-connector-java 5的类 // 因而应用6+版本的驱动,会存在ClassNotFound的问题。 class_connectionImpl = Utils.loadClass("com.mysql.jdbc.MySQLConnection"); } catch (Throwable error){ class_connectionImpl_Error = true; } }}
- 修复后的代码实现
新增了对mysql-connector-java 6的反对;
/** * druid 1.1.23版本及之后的源码实现 * @param conn :如果配置了自定义Filter,传入的conn就是ConnectionProxyImpl类型,否则就是ConnectionImpl类型 */public static long getLastPacketReceivedTimeMs(Connection conn) throws SQLException { // 如果配置了自定义Filter,这边class_connectionImpl就是null if (class_connectionImpl == null && !class_connectionImpl_Error) { try { // 加载mysql连贯类 class_connectionImpl = Utils.loadClass("com.mysql.jdbc.MySQLConnection"); if (class_connectionImpl == null) { class_connectionImpl = Utils.loadClass("com.mysql.cj.MysqlConnection"); if (class_connectionImpl != null) { mysqlJdbcVersion6 = true; } } } catch (Throwable error) { class_connectionImpl_Error = true; } } ..... }
发现没有,其实次要问题是MySQL驱动包版本的问题,druid 1.1.23之前的版本是写死的mysql-connector-java 5
的类名,如果工程中配置了com.mysql.cj.*
相干的包,就会导致一系列的问题:
loadClass
是同步办法,多线程下导致线程阻塞;
- Spring Boot动静类加载的时候最终会调用其自定义类加载器
LaunchedURLClassLoader
,在加载过程中会遍历BOOT-INF/lib/ 下所有的 jar 包 以及 BOOT-INF/classes/下的字节码文件;【jar形式部署】
- 解析外部jar时,会进行解压缩操作;
com.mysql.cj.*
相干的包不存在,因而每次连贯测试都会反复以上操作;
三、结尾
码完了,也没什么别的想说的了。哈,能够看看本人的druid版本,版本<1.1.23的话连忙降级吧。 有问题欢送留言探讨。