目录
- 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!