低版本Druid連接池+MySQL驅動8.0導致線程阻塞、性能受限
現(xiàn)象
應用升級MySQL驅動8.0后,在并發(fā)量較高時,查看監(jiān)控打點,Druid連接池拿到連接并執(zhí)行SQL的時間大部分都超過200ms
對系統(tǒng)進行壓測,發(fā)現(xiàn)出現(xiàn)大量線程阻塞的情況,線程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()方法實現(xiàn)中,如果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鎖,所以會出現(xiàn)線程阻塞,性能下降的現(xiàn)象
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檢測連接是否有效的參數(shù)
Druid檢測連接是否有效的參數(shù):
- testOnBorrow:每次獲取連接時執(zhí)行validationQuery檢測連接是否有效(會影響性能)
- testOnReturn:每次歸還連接時執(zhí)行validationQuery檢測連接是否有效(會影響性能)
- testWhileIdle:申請連接的時候檢測,如果空閑時間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測連接是否有效
- 應用中設置了testOnBorrow=true,每次獲取連接時,都會去搶占synchronized鎖,所以性能下降的很明顯
解決方案
經(jīng)驗證,使用Druid 1.x版本<=1.1.22會出現(xiàn)該bug,解決方案就是升級至Druid 1.x版本>=1.1.23或者Druid 1.2.x版本
GitHub issue:https://github.com/alibaba/druid/issues/3808
到此這篇關于低版本Druid連接池+MySQL驅動8.0導致線程阻塞、性能受限的文章就介紹到這了,更多相關MySQL驅動8.0低版本Druid連接池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
MySql Installer 8.0.18可視化安裝教程圖文詳解
這篇文章主要介紹了MySql Installer 8.0.18可視化安裝教程,本文圖文并茂給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12mysql數(shù)據(jù)庫之count()函數(shù)和sum()函數(shù)用法及區(qū)別說明
這篇文章主要介紹了mysql數(shù)據(jù)庫之count()函數(shù)和sum()函數(shù)用法及區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06MySQL的源碼安裝及使用UDFs進行數(shù)據(jù)自動更新的教程
UDFs即是MySQL的用戶自定義函數(shù)的縮寫,配合觸發(fā)器可以自動更新Memcached與MySql的數(shù)據(jù),這里我們就來總結一下MySQL的源碼安裝及使用UDFs進行數(shù)據(jù)自動更新的教程:2016-07-07