目錄
- 現(xiàn)象
- 根因分析
- getLastPacketReceivedTimeMs()方法調(diào)用時(shí)機(jī)
- 解決方案
現(xiàn)象
應(yīng)用升級(jí)MySQL驅(qū)動(dòng)8.0后,在并發(fā)量較高時(shí),查看監(jiān)控打點(diǎn),Druid連接池拿到連接并執(zhí)行SQL的時(shí)間大部分都超過(guò)200ms
對(duì)系統(tǒng)進(jìn)行壓測(cè),發(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()方法會(huì)加載com.mysql.jdbc.MySQLConnection這個(gè)類(lèi),但在MySQL驅(qū)動(dòng)8.0中類(lèi)名改為com.mysql.cj.jdbc.ConnectionImpl,所以MySQL驅(qū)動(dòng)8.0中加載不到com.mysql.jdbc.MySQLConnection
getLastPacketReceivedTimeMs()方法實(shí)現(xiàn)中,如果Utils.loadClass("com.mysql.jdbc.MySQLConnection")加載不到類(lèi)并拋出異常,會(huì)修改變量class_connectionImpl_Error,下次調(diào)用不會(huì)再進(jìn)行加載
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,這就導(dǎo)致loadClass()在加載不到類(lèi)的時(shí)候,并不會(huì)拋出異常,從而會(huì)導(dǎo)致每調(diào)用一次getLastPacketReceivedTimeMs()方法,就會(huì)加載一次MySQLConnection這個(gè)類(lèi)
線程dump信息中可以看到是在調(diào)用TomcatEmbeddedWebappClassLoader的loadClass()方法時(shí),導(dǎo)致線程阻塞的
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);
}
}
這是因?yàn)門(mén)omcatEmbeddedWebappClassLoader在加載類(lèi)的時(shí)候,會(huì)加synchronized鎖,這就導(dǎo)致每調(diào)用一次getLastPacketReceivedTimeMs()方法,就會(huì)加載一次com.mysql.jdbc.MySQLConnection,而又始終加載不到,在加載類(lèi)的時(shí)候會(huì)加synchronized鎖,所以會(huì)出現(xiàn)線程阻塞,性能下降的現(xiàn)象
getLastPacketReceivedTimeMs()方法調(diào)用時(shí)機(jī)
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()方法中會(huì)調(diào)用getLastPacketReceivedTimeMs()方法
testConnectionInternal()是用來(lái)檢測(cè)連接是否有效的,在獲取連接和歸還連接時(shí)都有可能會(huì)調(diào)用該方法,這取決于Druid檢測(cè)連接是否有效的參數(shù)
Druid檢測(cè)連接是否有效的參數(shù):
- testOnBorrow:每次獲取連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效(會(huì)影響性能)
- testOnReturn:每次歸還連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效(會(huì)影響性能)
- testWhileIdle:申請(qǐng)連接的時(shí)候檢測(cè),如果空閑時(shí)間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測(cè)連接是否有效
- 應(yīng)用中設(shè)置了testOnBorrow=true,每次獲取連接時(shí),都會(huì)去搶占synchronized鎖,所以性能下降的很明顯
解決方案
經(jīng)驗(yàn)證,使用Druid 1.x版本=1.1.22會(huì)出現(xiàn)該bug,解決方案就是升級(jí)至Druid 1.x版本>=1.1.23或者Druid 1.2.x版本
GitHub issue:https://github.com/alibaba/druid/issues/3808
到此這篇關(guān)于低版本Druid連接池+MySQL驅(qū)動(dòng)8.0導(dǎo)致線程阻塞、性能受限的文章就介紹到這了,更多相關(guān)MySQL驅(qū)動(dòng)8.0低版本Druid連接池內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- MySQL 8.0 驅(qū)動(dòng)與阿里druid版本兼容問(wèn)題解決
- MySql 8.0及對(duì)應(yīng)驅(qū)動(dòng)包匹配的注意點(diǎn)說(shuō)明
- 關(guān)于Mysql8.0版本驅(qū)動(dòng)getTables返回所有庫(kù)的表問(wèn)題淺析
- 詳解Mybatis逆向工程中使用Mysql8.0版本驅(qū)動(dòng)遇到的問(wèn)題