低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限

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

(0)
上一篇 2022年3月21日
下一篇 2022年3月21日

相关推荐