一、前言

  最近在捣鼓演示利用的时候发现一个druid连接池引起的线程blocked问题。先申明下,这个问题是druid 1.1.23版本之前的一个bug,并不是说druid存在显著的性能问题。

  github上其实曾经有相干的issue:https://github.com/alibaba/dr...;也能够关注下druid各版本的release信息:https://github.com/alibaba/dr...。

  而我在发现这个问题的时候并不知道这其实是个曾经修复了的bug,因而记录了一些剖析过程的线索,明天次要分享下问题排查的思路。

二、Let‘s Go

(一)重大的Thread Blocked

  事件起源于筹备客户演示环境时的一轮压测(验证环境)。在压测过程中,发现有一个拜访数据库的接口响应工夫异样的高,尽管成心做了循环查库【一次http申请,10次MySQL查问】的操作,然而对于只是进行简略查问的SQL来说,接口的响应工夫显得很不失常。

  1. 首先查看了下应用服务的资源应用状况,一切正常;
     
  2. 查看jvm运行状况。在热点办法【办法耗费CPU资源的状况】中,看到zip压缩相干的办法始终在耗费CPU资源时排在首位:把办法调用链开展,发现是类加载引起的;【能够必定,利用哪里必定有问题】

     
  3. 线程dump,查看线程都在干什么,并看看类加载相干残缺的线程栈。在thread dump中发现了大问题,97%的业务线程都处于blocked状态:

     
  4. 剖析线程dump,寻找blocked的次要起因。发现就是类加载操作导致的大量线程blocked。而类加载操作居然是druid相干的办法导致的(前面会提到,其实连贯检测导致的);

  我的项目中应用的druid连接池局部配置参数:

    initialSize: 5    minIdle: 5    maxIdle: 5    maxActive: 5    maxWait: 60000    timeBetweenEvictionRunsMillis: 60000    minEvictableIdleTimeMillis: 300000    validationQuery: SELECT 1 FROM DUAL    poolPreparedStatements: true    testWhileIdle: true    // 获取连贯时检测    testOnBorrow: true    // 偿还连贯时检测    testOnReturn: true    #   配置监控统计拦挡的filters,去掉后监控界面sql无奈统计,'wall'用于防火墙    filters: stat,wall,slf4j    maxPoolPreparedStatementPerConnectionSize: 20    useGlobalDataSourceStat: true    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000

  能够看到,这边把连贯检测的配置都加上,也就是在获取连贯,偿还连贯,以及连贯闲暇阶段都会进行连贯检测。【思考到性能影响:个别不开启testOnReturntestOnReturn,而应用testWhileIdle配合timeBetweenEvictionRunsMillis进行闲暇连贯检测】

  接下来,简略剖析下druid的局部源码,以及druid 1.1.23版本之前的bug的相干源码。

或者能够间接跳转至上面 getLastPacketReceivedTimeMs章节,间接切入正题;

(二)连贯检测局部源码

1. getConnectionDirect - 连贯检测入口

  调用getConnection()获取连贯后,最终会调用com.alibaba.druid.pool.DruidDataSource#getConnectionDirect办法。在getConnectionDirect办法中,就会判断是否走连贯检测的逻辑,上面是次要源码(篇幅起因其余分支逻辑细节去掉了,可自行浏览):

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {        int notFullTimeoutRetryCnt = 0;        for (;;) {            // DruidPooledConnection封装了物理连贯对象            DruidPooledConnection poolableConnection;            try {                // 这是连贯检测的外围代码所在                poolableConnection = getConnectionInternal(maxWaitMillis);            } catch (GetConnectionTimeoutException ex) {                if (notFullTimeoutRetryCnt <= this.notFullTimeoutRetryCount && !isFull()) {                    notFullTimeoutRetryCnt++;                    if (LOG.isWarnEnabled()) {                        LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);                    }                    continue;                }                throw ex;            }            // 是否开启了获取连贯时检测(依照下面的配置,首先会进入这段代码)            if (testOnBorrow) {                boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);                if (!validate) {                    if (LOG.isDebugEnabled()) {                        LOG.debug("skip not validate connection.");                    }                    discardConnection(poolableConnection.holder);                    continue;                }            } else {                if (poolableConnection.conn.isClosed()) {                    discardConnection(poolableConnection.holder); // 传入null,防止反复敞开                    continue;                }                // 是否开启了闲暇检测,会依据闲暇工夫,判断是否进行连贯检测                if (testWhileIdle) {                    // ......                }            }            // 是否开启了removeAbandoned参数(不举荐开启,会获取线程栈信息)            if (removeAbandoned) {                // ......            }            if (!this.defaultAutoCommit) {                poolableConnection.setAutoCommit(false);            }            return poolableConnection;        }    }

2. testConnectionInternal - 连贯检测外围代码

com.alibaba.druid.pool.DruidAbstractDataSource#testConnectionInternal是连贯检测的外围代码。开启了testOnReturntestOnReturn后,外围的连贯测试逻辑都在这。

两个要害办法

  1. validConnectionChecker.isValidConnection(),实际上调用的是com.alibaba.druid.pool.vendor.MySqlValidConnectionChecker#isValidConnection()。次要做两件事:1、先发送ping信息给mysql服务器,检测tcp连贯可用性;2、执行validationQuery配置的SQL查问语句;
     
  2. MySqlUtils.getLastPacketReceivedTimeMs(),用于获取连贯的闲暇工夫。这个办法也是明天thread blocked的首恶;
protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {    // ......其余    try {        if (validConnectionChecker != null) {            // isValidConnection真正做检测的办法,会做两件事            // 1. 先发送ping信息给mysql服务器,检测tcp连贯可用性;            // 2. 执行validationQuery配置的SQL查问语句;            boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);            long currentTimeMillis = System.currentTimeMillis();            if (holder != null) {                holder.lastValidTimeMillis = currentTimeMillis;                holder.lastExecTimeMillis = currentTimeMillis;            }            // 如果是mysql数据库,且通过了连贯测试,就进入上面的代码            if (valid && isMySql) { // unexcepted branch                // 1.1.23之前的bug所在                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                                + ", version : " + VERSION.getVersionNumber()                                + ", lastPacketReceivedIdleMillis : " + mysqlIdleMillis;                        LOG.warn(errorMsg);                        return false;                    }                }            }            if (valid && onFatalError) {                lock.lock();                try {                    if (onFatalError) {                        onFatalError = false;                    }                } finally {                    lock.unlock();                }            }            return valid;        }        // ......其余    }}

3. getLastPacketReceivedTimeMs - 线程阻塞的首恶,bug所在

  com.alibaba.druid.util.MySqlUtils#getLastPacketReceivedTimeMs

  • 1.1.23版本之前的实现
/** * druid 1.1.23版本之前的源码实现 */public static long getLastPacketReceivedTimeMs(Connection conn) throws SQLException {        // 第一次执行这段代码,class_connectionImpl是初始值null,因而会进入if逻辑        if (class_connectionImpl == null && !class_connectionImpl_Error) {        try {            // 写死mysql-connector-java 5的类            // 因而应用6+版本的驱动,会存在ClassNotFound的问题。            class_connectionImpl = Utils.loadClass("com.mysql.jdbc.MySQLConnection");        } catch (Throwable error){        class_connectionImpl_Error = true;        }        }}
  • 修复后的代码实现
     
    新增了对mysql-connector-java 6的反对;
/** * druid 1.1.23版本及之后的源码实现 * @param conn :如果配置了自定义Filter,传入的conn就是ConnectionProxyImpl类型,否则就是ConnectionImpl类型 */public static long getLastPacketReceivedTimeMs(Connection conn) throws SQLException {        // 如果配置了自定义Filter,这边class_connectionImpl就是null        if (class_connectionImpl == null && !class_connectionImpl_Error) {            try {                // 加载mysql连贯类                class_connectionImpl = Utils.loadClass("com.mysql.jdbc.MySQLConnection");                if (class_connectionImpl == null) {                    class_connectionImpl = Utils.loadClass("com.mysql.cj.MysqlConnection");                    if (class_connectionImpl != null) {                        mysqlJdbcVersion6 = true;                    }                }            } catch (Throwable error) {                class_connectionImpl_Error = true;            }        }        .....      }

  发现没有,其实次要问题是MySQL驱动包版本的问题,druid 1.1.23之前的版本是写死的mysql-connector-java 5的类名,如果工程中配置了com.mysql.cj.*相干的包,就会导致一系列的问题:

  1. loadClass是同步办法,多线程下导致线程阻塞;
     
  2. Spring Boot动静类加载的时候最终会调用其自定义类加载器LaunchedURLClassLoader,在加载过程中会遍历BOOT-INF/lib/ 下所有的 jar 包 以及 BOOT-INF/classes/下的字节码文件;【jar形式部署】
     
  3. 解析外部jar时,会进行解压缩操作;
     
  4. com.mysql.cj.*相干的包不存在,因而每次连贯测试都会反复以上操作;

三、结尾

  码完了,也没什么别的想说的了。哈,能够看看本人的druid版本,版本<1.1.23的话连忙降级吧。 有问题欢送留言探讨。