低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限
作者:邋遢的流浪剑客 发布时间:2024-01-20 05:11:49
目录
现象
根因分析
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:https://github.com/alibaba/druid/issues/3808
来源:https://blog.csdn.net/qq_40378034/article/details/117851207


猜你喜欢
- 症状 在 Service Pack 4 (SP 4) 运行 Microsoft Windows Server 2003、 Microsoft
- 前言二维码现在是随处度可以看到,买东西,支付,添加好友只要你扫一扫就能完成整个工作,简单且方便。所以利用这个新春佳节做一个带着新春祝福的二维
- append() 方法向列表的尾部添加一个新的元素。只接受一个参数。>>> num = [1,2]>>>
- 官方文档中关于super的定义说的不是很多,大致意思是返回一个代理对象让你能够调用一些继承过来的方法,查找的机制遵循mro规则,最常用的情况
- XHTML结构: <div id="myFocus-wrap"> <div id="myFo
- 在 HTML 中使用JavaScriptJavaScript能以两种方式嵌入HTML:作为语句和函数使用时,用 SCRIPT 标记作为事件处
- 大家知道,Microsoft为了更好地预防恶意用户和攻击者的攻击,在默认情况下,没有将 IIS6.0 安装到 Windows Server
- 通过官方文档https://docs.djangoproject.com/zh-hans/3.1/topics/db/multi-db/和c
- 完整性约束是对字段进行限制,从而符合该字段达到我们期望的效果比如字段含有默认值,不能是NULL等 直观点说:如果插入的数据不满足限制要求,数
- 前言:在日常开发的过程中,有时候会临时用到一些简单的函数,她们的业务逻辑会相对简单,简单到并不值得让他们留下名字,这个时候,就很值得使用匿名
- 前言今天给大家写一个个打地鼠小游戏,废话不多说直接开始~开发工具Python版本: 3.6.4相关模块:pygame模块;以及一些
- 一、简单介绍正则表达式是一种小型的、高度专业化的编程语言,并不是python * 有的,是许多编程语言中基础而又重要的一部分。在python中
- Javascript刷新页面的几种方法: 1. history.go(0) 2. location.reload() 3. location
- 一、单循环动态设置ref1.设置:【:ref=“‘XXX’ + index&am
- 本文文章主要介绍了Python内置函数—vars的具体使用方法,分享给大家,具体如下:英文文档:vars([object])Return t
- 对于所有的需求,当你不知道怎么处理的时候,你就先用最简单的方法,或者说的明白一点,用最原始的方法,先实现业务需求再说。一、对提现队列数据表“
- 一、matplotlib库1、基本绘图命令import matplotlib.pyplot as pltplt.figure(figsize
- Asp中Server.ScriptTimeOut属性需要注意的一点Server.ScriptTimeout?这个属性给定Asp脚本执行的最大
- Python安装流程:一.下载在网上(输入网址https://www.python.org/downloads/)下载Windows最新的p
- 摘要在这篇文章里,我将以反模式的角度来直接讨论Django的低级ORM查询方法的使用。作为一种替代方式,我们需要在包含业务逻辑的