目录
- getlastpacketreceivedtimems()方法调用时机
现象
应用升级mysql驱动8.0后,在并发量较高时,查看监控打点,druid连接池拿到连接并执行sql的时间大部分都超过200ms
对系统进行压测,发现出现大量线程阻塞的情况,线程dump信息如下:
"http-nio-5366-exec-48" #210 daemon prio=5 os_prio=0 tid=0x00000000023d0800 nid=0x3be9 waiting for monitor entry [0x00007fa4c1400000] java.lang.thread.state: blocked (on object monitor) at org.springframework.boot.web.embedded.tomcat.tomcatembeddedwebappclassloader.loadclass(tomcatembeddedwebappclassloader.java:66) - waiting to lock <0x0000000775af0960> (a java.lang.object) at org.apache.catalina.loader.webappclassloaderbase.loadclass(webappclassloaderbase.java:1186) at com.alibaba.druid.util.utils.loadclass(utils.java:220) at com.alibaba.druid.util.mysqlutils.getlastpacketreceivedtimems(mysqlutils.java:372)
根因分析
public class mysqlutils { public static long getlastpacketreceivedtimems(connection conn) throws sqlexception { if (class_connectionimpl == null && !class_connectionimpl_error) { try { class_connectionimpl = utils.loadclass("com.mysql.jdbc.mysqlconnection"); } catch (throwable error){ class_connectionimpl_error = true; } } if (class_connectionimpl == null) { return -1; } if (method_getio == null && !method_getio_error) { try { method_getio = class_connectionimpl.getmethod("getio"); } catch (throwable error){ method_getio_error = true; } } if (method_getio == null) { return -1; } if (class_mysqlio == null && !class_mysqlio_error) { try { class_mysqlio = utils.loadclass("com.mysql.jdbc.mysqlio"); } catch (throwable error){ class_mysqlio_error = true; } } if (class_mysqlio == null) { return -1; } if (method_getlastpacketreceivedtimems == null && !method_getlastpacketreceivedtimems_error) { try { method method = class_mysqlio.getdeclaredmethod("getlastpacketreceivedtimems"); method.setaccessible(true); method_getlastpacketreceivedtimems = method; } catch (throwable error){ method_getlastpacketreceivedtimems_error = true; } } if (method_getlastpacketreceivedtimems == null) { return -1; } try { object connimpl = conn.unwrap(class_connectionimpl); if (connimpl == null) { return -1; } object mysqlio = method_getio.invoke(connimpl); long ms = (long) method_getlastpacketreceivedtimems.invoke(mysqlio); return ms.longvalue(); } catch (illegalargumentexception e) { throw new sqlexception("getlastpacketreceivedtimems error", e); } catch (illegalaccessexception e) { throw new sqlexception("getlastpacketreceivedtimems error", e); } catch (invocationtargetexception e) { throw new sqlexception("getlastpacketreceivedtimems error", e); } }
mysqlutils中的getlastpacketreceivedtimems()方法会加载com.mysql.jdbc.mysqlconnection这个类,但在mysql驱动8.0中类名改为com.mysql.cj.jdbc.connectionimpl,所以mysql驱动8.0中加载不到com.mysql.jdbc.mysqlconnection
getlastpacketreceivedtimems()方法实现中,如果utils.loadclass(“com.mysql.jdbc.mysqlconnection”)加载不到类并抛出异常,会修改变量class_connectionimpl_error,下次调用不会再进行加载
public class utils { public static class<?> loadclass(string classname) { class<?> clazz = null; if (classname == null) { return null; } try { return class.forname(classname); } catch (classnotfoundexception e) { // skip } classloader ctxclassloader = thread.currentthread().getcontextclassloader(); if (ctxclassloader != null) { try { clazz = ctxclassloader.loadclass(classname); } catch (classnotfoundexception e) { // skip } } return clazz; }
但是,在utils的loadclass()方法中同样catch了classnotfoundexception,这就导致loadclass()在加载不到类的时候,并不会抛出异常,从而会导致每调用一次getlastpacketreceivedtimems()方法,就会加载一次mysqlconnection这个类
线程dump信息中可以看到是在调用tomcatembeddedwebappclassloader的loadclass()方法时,导致线程阻塞的
public class tomcatembeddedwebappclassloader extends parallelwebappclassloader {
public class<?> loadclass(string name, boolean resolve) throws classnotfoundexception { synchronized (jrecompat.isgraalavailable() ? this : getclassloadinglock(name)) { class<?> result = findexistingloadedclass(name); result = (result != null) ? result : doloadclass(name); if (result == null) { throw new classnotfoundexception(name); } return resolveifnecessary(result, resolve); } }
这是因为tomcatembeddedwebappclassloader在加载类的时候,会加synchronized锁,这就导致每调用一次getlastpacketreceivedtimems()方法,就会加载一次com.mysql.jdbc.mysqlconnection,而又始终加载不到,在加载类的时候会加synchronized锁,所以会出现线程阻塞,性能下降的现象
getlastpacketreceivedtimems()方法调用时机
public abstract class druidabstractdatasource extends wrapperadapter implements druidabstractdatasourcembean, datasource, datasourceproxy, serializable { protected boolean testconnectioninternal(druidconnectionholder holder, connection conn) { string sqlfile = jdbcsqlstat.getcontextsqlfile(); string sqlname = jdbcsqlstat.getcontextsqlname(); if (sqlfile != null) { jdbcsqlstat.setcontextsqlfile(null); } if (sqlname != null) { jdbcsqlstat.setcontextsqlname(null); } try { if (validconnectionchecker != null) { boolean valid = validconnectionchecker.isvalidconnection(conn, validationquery, validationquerytimeout); long currenttimemillis = system.currenttimemillis(); if (holder != null) { holder.lastvalidtimemillis = currenttimemillis; holder.lastexectimemillis = currenttimemillis; } if (valid && ismysql) { // unexcepted branch 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 + ", jdbcurl : " + jdbcurl + ", lastpacketreceivedidlemillis : " + mysqlidlemillis; log.error(errormsg); return false; } } } if (valid && onfatalerror) { lock.lock(); try { if (onfatalerror) { onfatalerror = false; } } finally { lock.unlock(); } } return valid; } if (conn.isclosed()) { return false; } if (null == validationquery) { return true; } statement stmt = null; resultset rset = null; try { stmt = conn.createstatement(); if (getvalidationquerytimeout() > 0) { stmt.setquerytimeout(validationquerytimeout); } rset = stmt.executequery(validationquery); if (!rset.next()) { return false; } } finally { jdbcutils.close(rset); jdbcutils.close(stmt); } if (onfatalerror) { lock.lock(); try { if (onfatalerror) { onfatalerror = false; } } finally { lock.unlock(); } } return true; } catch (throwable ex) { // skip return false; } finally { if (sqlfile != null) { jdbcsqlstat.setcontextsqlfile(sqlfile); } if (sqlname != null) { jdbcsqlstat.setcontextsqlname(sqlname); } } }
只有druidabstractdatasource的testconnectioninternal()方法中会调用getlastpacketreceivedtimems()方法
testconnectioninternal()是用来检测连接是否有效的,在获取连接和归还连接时都有可能会调用该方法,这取决于druid检测连接是否有效的参数
druid检测连接是否有效的参数:
- testonborrow:每次获取连接时执行validationquery检测连接是否有效(会影响性能)
- testonreturn:每次归还连接时执行validationquery检测连接是否有效(会影响性能)
- testwhileidle:申请连接的时候检测,如果空闲时间大于timebetweenevictionrunsmillis,执行validationquery检测连接是否有效
- 应用中设置了testonborrow=true,每次获取连接时,都会去抢占synchronized锁,所以性能下降的很明显
解决方案
经验证,使用druid 1.x版本<=1.1.22会出现该bug,解决方案就是升级至druid 1.x版本>=1.1.23或者druid 1.2.x版本
github issue:
到此这篇关于低版本druid连接池+mysql驱动8.0导致线程阻塞、性能受限的文章就介绍到这了,更多相关mysql驱动8.0低版本druid连接池内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!