Post

简单排查druid数据库连接检测没有生效原因

通过Druid相关源码, 排查Druid活性检查没有生效的原因

前言

最近排查项目中线上报错的数据库连接失效问题Communications link failure, 开始排查导致的原因

排查过程

找到了公司相关的dataSource配置, 发现并没有做对应的活性检查配置

1
2
3
4
5
6
7
8
9
10
11
12
public DataSource slaveDataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    List<String> initSQLs = new ArrayList<>();
    initSQLs.add("SET NAMES utf8mb4");
    dataSource.setConnectionInitSqls(initSQLs);
    dataSource.setDriverClassName(driverClass);
    dataSource.setUrl(url);
    dataSource.setUsername(user);
    dataSource.setPassword(password);
    dataSource.setMaxActive(200);
    return dataSource;
}

于是查询了tomcat官方链接池文档1, 我们可以知道以下几个是可以进行活性检查的相关配置:

testOnBorrow(boolean) The indication of whether objects will be validated before being borrowed from the pool. If the object fails to validate, it will be dropped from the pool, and we will attempt to borrow another. In order to have a more efficient validation, see validationInterval. Default value is false是否在从对象池中借用对象前进行验证的标志。若验证不通过,该对象将被移除,并尝试借用其他对象。为提高验证效率,可参考validationInterval设置。默认情况下不进行验证。
testOnConnect(boolean) The indication of whether objects will be validated when a connection is first created. If an object fails to validate, it will be throw SQLException. Default value is false此设置决定连接建立时是否检查对象的有效性。若检查失败,系统将报错 SQLException。默认情况下,此检查是关闭的。
testOnReturn(boolean) The indication of whether objects will be validated before being returned to the pool. The default value is false. (布尔值) 控制对象在归还池时是否需要验证。默认设置为不验证。
testWhileIdle(boolean) The indication of whether objects will be validated by the idle object evictor (if any). If an object fails to validate, it will be dropped from the pool. The default value is false and this property has to be set in order for the pool cleaner/test thread is to run (also see timeBetweenEvictionRunsMillis) 该设置决定对象在归还到池中之前是否需要经过验证。默认情况下,此功能是关闭的。
timeBetweenEvictionRunsMillis(int) The number of milliseconds to sleep between runs of the idle connection validation/cleaner thread. This value should not be set under 1 second. It dictates how often we check for idle, abandoned connections, and how often we validate idle connections. The default value is 5000 (5 seconds). (整数) 设置空闲连接验证与清理线程每次执行之间的休眠时间,单位为毫秒。该时间应至少为1秒,以确保定期检查并处理数据库中的空闲或废弃连接。默认情况下,此时间为5秒(即5000毫秒)。
validationInterval(long) avoid excess validation, only run validation at most at this frequency - time in milliseconds. If a connection is due for validation, but has been validated previously within this interval, it will not be validated again. The default value is 3000 (3 seconds). 设置验证频率上限,单位为毫秒。若连接需验证,但在指定时间内已验证过,则跳过此次验证。默认每3秒验证一次。
validationQuery(String) The SQL query that will be used to validate connections from this pool before returning them to the caller. If specified, this query does not have to return any data, it just can’t throw a SQLException. The default value is null. If not specified, connections will be validation by the isValid() method. Example values are SELECT 1(mysql), select 1 from dual(oracle), SELECT 1(MS Sql Server)在将连接池中的连接提供给调用者之前,用于确认这些连接有效性的SQL查询语句。若设置此查询,它无需返回任何结果,只要不引发SQLException错误即可。默认情况下,此查询为空。若未设置,则通过isValid()方法来验证连接的有效性。常见的查询示例有:MySQL的SELECT 1、Oracle的select 1 from dual以及MS SQL Server的SELECT 1

从上面表格看出来, testOnBorrowtestOnConnecttestOnReturn都是需要在每次链接,或者返回的时候做一些相关链接查询的校验, 这种显然会对性能造成一些影响, 看起来最适合的就是testWhileIdle属性配置, 也就是在空闲时校验, 针对testWhileIdle属性还需要配置timeBetweenEvictionRunsMillis属性, 也就是定期检查的间隔时间, 这是我修改之后的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public DataSource slaveDataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    List<String> initSQLs = new ArrayList<>();
    initSQLs.add("SET NAMES utf8mb4");
    dataSource.setConnectionInitSqls(initSQLs);
    dataSource.setDriverClassName(driverClass);
    dataSource.setUrl(url);
    dataSource.setUsername(user);
    dataSource.setPassword(password);
    dataSource.setMaxActive(200);
  	// 以下为新增配置
    dataSource.setTestWhileIdle(true);
    dataSource.setValidationQuery("SELECT 1");
    dataSource.setTimeBetweenEvictionRunsMillis(60000);
    return dataSource;
}

Druid源码查看

通过查看druid的源码发现核心的检验是在这个方法中com.alibaba.druid.pool.DruidAbstractDataSource#testConnectionInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
protected boolean testConnectionInternal(Connection conn) {
    String sqlFile = JdbcSqlStat.getContextSqlFile();
    String sqlName = JdbcSqlStat.getContextSqlName();

    if (sqlFile != null) {
        JdbcSqlStat.setContextSqlFile(null);
    }
    if (sqlName != null) {
        JdbcSqlStat.setContextSqlName(null);
    }
    try {
      	// 判断是否有内部实现的Chcecker, 这是一个核心的方法,大部分常见数据库走的该逻辑
        if (validConnectionChecker != null) {
            return validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);
        }

      	// 如果没有checker则往下走
        if (conn.isClosed()) {
            return false;
        }

      	// 这就是我们为什么需要设置validationQuery, 如果不设置就会直接返回true
        if (null == validationQuery) {
            return true;
        }

      	// 如果设置了则执行sql
        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);
        }

        return true;
    } catch (Exception ex) {
        // skip
        return false;
    } finally {
        if (sqlFile != null) {
            JdbcSqlStat.setContextSqlFile(sqlFile);
        }
        if (sqlName != null) {
            JdbcSqlStat.setContextSqlName(sqlName);
        }
    }
}

从上面的逻辑我们可以看到validConnectionChecker非常的重要, 默认是一个null, 查看这个类ValidConnectionChecker是一个接口, 查看对应的实现类, 如下图1

图1

我们可以很清晰的看出来, 如果我们的dataSource是mysql那么这里就会返回mySqlValidConnectionChecker, 由于我们系统使用的是Mysql, 则我看的是mysql源码, 如果用的是其他数据源, 可以看各自的实现; 如果没有的话, 则是用的上面抽象父类的逻辑.

MySqlValidConnectionChecker源码排查

查看MySqlValidConnectionChecker实现的源码, 可以看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) {
        try {
            if (conn.isClosed()) {
                return false;
            }
        } catch (SQLException ex) {
            // skip
            return false;
        }

  		// 如果开通了这个usePingMethod默认开通, 则会通过ping的方式去校验数据库连接
        if (usePingMethod) {
            if (conn instanceof DruidPooledConnection) {
                conn = ((DruidPooledConnection) conn).getConnection();
            }

            if (conn instanceof ConnectionProxy) {
                conn = ((ConnectionProxy) conn).getRawObject();
            }

            if (clazz.isAssignableFrom(conn.getClass())) {
                if (validationQueryTimeout < 0) {
                    validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
                }

                try {
                    ping.invoke(conn, true, validationQueryTimeout);
                    return true;
                } catch (InvocationTargetException e) {
                    Throwable cause = e.getCause();
                    if (cause instanceof SQLException) {
                        return false;
                    }

                    LOG.warn("Unexpected error in ping", e);
                    return false;
                } catch (Exception e) {
                    LOG.warn("Unexpected error in ping", e);
                    return false;
                }
            }
        }

  		// 否则去执行我们的validateQuery方法
        Statement stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.createStatement();
            if (validationQueryTimeout > 0) {
                stmt.setQueryTimeout(validationQueryTimeout);
            }
            rs = stmt.executeQuery(validateQuery);
            return true;
        } catch (SQLException e) {
            return false;
        } catch (Exception e) {
            LOG.warn("Unexpected error in ping", e);
            return false;
        } finally {
            JdbcUtils.close(rs);
            JdbcUtils.close(stmt);
        }

    }

可以从上面的代码看出来usePingMethod非常的重要, 从下面源码可以看出来我们的usePingMethod默认是false, 但是我们再往下看构造方法, 就发现, 如果我们的com.mysql.jdbc.MySQLConnectio这个class里面包含了pingInternal方法的话, 则会被设置为true;

可以看到这段逻辑显然是对MySQLConnection的一个兼容, 如果有这个方法, 则用ping的逻辑, 这是mysql后期升级的一个功能, 但是这个功能对我们系统的错误是不兼容的, 我们系统用的是阿里云数据库, 是可以ping的只不过连接失效了, 所以不能用这个ping的方式

1
private boolean  usePingMethod = false;
1
2
3
4
5
6
7
8
9
10
11
12
13
public MySqlValidConnectionChecker(){
    try {
        clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
        ping = clazz.getMethod("pingInternal", boolean.class, int.class);
        if (ping != null) {
            usePingMethod = true;
        }
    } catch (Exception e) {
        LOG.warn("Cannot resolve com.mysq.jdbc.Connection.ping method.  Will use 'SELECT 1' instead.", e);
    }

    configFromProperties(System.getProperties());
}

继续往下看代码可以看到configFromProperties方法的源码中, 单独设置了这个usePingMethod属性, 我们可以通过在System.Properties里面单独再把usePingMethod设置为false, 这样就可以走查询sql检测连接是否有效的方式了

1
2
3
4
5
6
7
8
9
10
@Override
public void configFromProperties(Properties properties) {
    String property = properties.getProperty("druid.mysql.usePingMethod");
    if ("true".equals(property)) {
        setUsePingMethod(true);
    } else if ("false".equals(property)) {
        setUsePingMethod(false);
    }
}

总结和解决方案

在druid连接源下, 我们可以通过设置testOnBorrowtestOnConnecttestOnReturntestWhileIdle来进行数据库连接检测, 根据不同的性能测试, 最终我选择的是testWhileIdle检测模式, 并且设置的是1分钟检测一次.

如果系统中使用的是Mysql的话, 可能会使用ping的方式来检查数据库连接, 如果ping的方式也不适合你们系统, 最好通过配置以下bean来解决这个问题. 也就是在System. property里面设置usePingMethod=false, 来关闭ping的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class DruidConfig {

  /*
   * 解决druid使用ping的方式来测试mysql连接, 改为默认使用sql查询的方式
   * */
  @PostConstruct
  public void setProperties(){
      System.setProperty("druid.mysql.usePingMethod","false");
  }
}


*封面图由bing image creator创建

  1. https://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html 

This post is licensed under CC BY 4.0 by the author.