作者:刘开洋

爱可生交付服务团队北京 DBA,对数据库及周边技术有浓厚的学习兴趣,喜欢看书,追求技术。

本文来源:原创投稿

*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。


最近有研究到 Druid 的问题,在进行探活检测时无法正常输出 select x,解决问题后就跑来跟大家分享下 Druid 探活机制的实现。

一、Druid 是什么?Druid 对连接的探活又是怎么实现的呢?

Druid 是阿里巴巴开源的一款 JDBC 组件,是一款数据库连接池,对MySQL的适配性和功能很强大,包括监控数据库访问性能、 数据库密码加密、SQL执行日志、以及拓展监控的实现等等,应用到MySQL还是很香的。

通过对官方源码(详见参考)进行一些简单分析了解到,使用 Druid 在对连接进行探活时,涉及到以下两个参数的调整:

参数 说明
druid.validationQuery = select 1 用来检测连接是否有效的sql,要求是一个查询语句,常用select ‘x’。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
druid.mysql.usePingMethod = false 关闭 mysql com_ping 探活机制,需启用 validationQuery = select x,开启 validationQuery 探活机制

对其他参数的说明参考配置属性列表:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

在源码中得到的信息是 Druid 依次初始化加载 initValidConnectionChecker(); 和 validationQueryCheck(); 在 ValidConnectionChecker 中 默认 com_ping 是开启的,就选用了 com_ping 作为默认探活,下面我们来分别观测一下 com_ping 和 validationquery 的输出。

我们接着往下读看看这两个参数的功能具体是怎么实现的。

二、验证

在测试中来敲定这两种探活机制参数的作用吧。这里使用的版本是 Druid 1.2.5

1、com_ping方式需要通过抓包方式验证

通过 tcpdump 抓包得到在 Druid 连接中网络包的传输,之后使用 wireshark 进行分析查看 Druid 发送到 MySQL 的 Request 包。

在MySQL Protocol 的 Request Command Ping 中得到 Ping (14)。

2、validationquery 方式通过使用 MySQL general log 来验证

public MySqlValidConnectionChecker(){
    try {
        clazz = Utils.loadClass("com.mysql.jdbc.MySQLConnection");
        if (clazz == null) {
            clazz = Utils.loadClass("com.mysql.cj.jdbc.ConnectionImpl");
        }
   
        if (clazz != null) {
            ping = clazz.getMethod("pingInternal", boolean.class, int.class);
        }
   
        if (ping != null) {
            usePingMethod = true;
        }
    } catch (Exception e) {
        LOG.warn("Cannot resolve com.mysql.jdbc.Connection.ping method.  Will use 'SELECT 1' instead.", e);
    }
    //注意这里是从系统变量中获取的 System.getProperties()。
    configFromProperties(System.getProperties());
}
   
@Override
public void configFromProperties(Properties properties) {
   //从系统变量中获取的,所以应该是在项目的启动脚本中添加 usePingMethod=false
   String property = properties.getProperty("druid.mysql.usePingMethod");
   if ("true".equals(property)) {
       setUsePingMethod(true);
   } else if ("false".equals(property)) {
       setUsePingMethod(false);
   }
}
    
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
  if (conn.isClosed()) {
      return false;
  }
   
  if (usePingMethod) {
      if (conn instanceof DruidPooledConnection) {
          conn = ((DruidPooledConnection) conn).getConnection();
      }
   
      if (conn instanceof ConnectionProxy) {
          conn = ((ConnectionProxy) conn).getRawObject();
      }
   
       // 当前的 conn 是否是 com.mysql.jdbc.MySQLConnection(or com.mysql.cj.jdbc.ConnectionImpl)
      if (clazz.isAssignableFrom(conn.getClass())) {
          if (validationQueryTimeout <= 0) {
              validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT;
          }
   
          try {
              // 使用反射调用MySQLConnection.pingInternal 方法,检查连接有效性,并且会刷新连接的空闲时间,如果失败则会抛出异常,上层捕获
              ping.invoke(conn, true, validationQueryTimeout * 1000);
          } catch (InvocationTargetException e) {
              Throwable cause = e.getCause();
              if (cause instanceof SQLException) {
                  throw (SQLException) cause;
              }
              throw e;
          }
          return true;
      }
  }
   
  String query = validateQuery;
  // 当usePingMethod=false 或者 conn 不是 com.mysql.jdbc.MySQLConnection (or com.mysql.cj.jdbc.ConnectionImpl)会执行一下方法
  if (validateQuery == null || validateQuery.isEmpty()) {
      query = DEFAULT_VALIDATION_QUERY;
  }
   
  Statement stmt = null;
  ResultSet rs = null;
  try {
      stmt = conn.createStatement();
      if (validationQueryTimeout > 0) {
          stmt.setQueryTimeout(validationQueryTimeout);
      }
      // 执行 select x 的query ,并且会刷新连接的空闲时间
      //  如果失败则会抛出异常,上层捕获
      rs = stmt.executeQuery(query);
      return true;
  } finally {
      JdbcUtils.close(rs);
      JdbcUtils.close(stmt);
  }
}

druid.validationQuery = SELECT 1 启用无法直接使用validationquery,需要通过配置关闭com_ping(druid.mysql.usePingMethod = false)来实现。这个参数可以直接加在配置文件中,但是使用需要注意一点,如果配置关闭com_ping也无法使用validationquery进行探活查询,则可能是程序本身的问题。

程序代码可能存在参数值只拉取 configFromPropety 的参数信息导致(druid.mysql.usePingMethod = false)参数失效,以下是我程序修改后的连接示意图:

// 原程序
public DruidDriverTest() {
   logger = Logger.getLogger("druid_driver_test");
   this.dataSource = new DruidDataSource();
 
   // Druid 配置文件地址.
   this.configPath = "./config.properties";
···
 
#############################################
// 修改后
public DruidDriverTest() {
    logger = Logger.getLogger("druid_driver_test");
 
    // Druid 配置文件地址.
    this.configPath = "config.properties";
 
    try (BufferedReader bufferedReader = new BufferedReader(new FileReader(configPath))) {
            // 将配置文件读入到 system.config 中
            System.getProperties().load(bufferedReader);
    } catch (IOException e) {
            e.printStackTrace();
            return;
    }
···

原程序中:Druid 默认从 config 配置文件 中拉配置参数信息到 DruidDataSource 中,而 usePingMethod 参数需要使用 MySqlValidConnectionChecker 插件加载读取到 DruidDataSource 中,但是config没有加载到System. getProperties()中,因此 Druid 不能识别 config 配置文件 中的 usePingMethod 参数。 Druid 加载 DruidDataSource 中的配置信息进行一系列行为。

修改后:建立 config配置文件加载到 system 变量中的连接,再使用 MySqlValidConnectionChecker 插件 加载到 DruidDataSource 中。

[root@yang-02 druid_demo-master]# mvn exec:java -Dexec.mainClass="test.App"
[INFO] Scanning for projects...                                                                
[INFO] ------------------------------------------------------------------------
[INFO] Building druid-demo 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ druid-demo ---
[2021-04-28 17:23:06] [SEVERE] minEvictableIdleTimeMillis should be greater than 30000
[2021-04-28 17:23:06] [SEVERE] keepAliveBetweenTimeMillis should be greater than 30000
[2021-04-28 17:23:06] [INFO] start test
[2021-04-28 17:23:06] [INFO] ------------------ status --------------------
[2021-04-28 17:23:06] [INFO] initial size: 3
[2021-04-28 17:23:06] [INFO] min idle: 2
[2021-04-28 17:23:06] [INFO] max active: 20
[2021-04-28 17:23:06] [INFO] current active: 0
[2021-04-28 17:23:06] [INFO] max wait: 6000
[2021-04-28 17:23:06] [INFO] time between eviction runs millis: 2000
[2021-04-28 17:23:06] [INFO] validation query: SELECT 1
[2021-04-28 17:23:06] [INFO] keepAlive: true
[2021-04-28 17:23:06] [INFO] testWhileIdle: false
[2021-04-28 17:23:06] [INFO] testOnBorrow: false
[2021-04-28 17:23:06] [INFO] testOnReturn: false
[2021-04-28 17:23:06] [INFO] keepAliveBetweenTimeMillis: 4000
[2021-04-28 17:23:06] [INFO] MinEvictableIdleTimeMillis: 2000
[2021-04-28 17:23:06] [INFO] MaxEvictableIdleTimeMillis: 25200000
[2021-04-28 17:23:06] [INFO] RemoveAbandoned: false
[2021-04-28 17:23:06] [INFO] RemoveAbandonedTimeoutMillis: 300000
[2021-04-28 17:23:06] [INFO] RemoveAbandonedTimeout: 300
[2021-04-28 17:23:06] [INFO] LogAbandoned: false
 
  
// 通过开启MySQL general log 观测Druid下发查询的命令输出
// mysql general log output
2021-04-28T17:23:01.435944+08:00     7048 Connect   root@127.0.0.1 on druid_demo using TCP/IP
2021-04-28T17:23:01.441663+08:00     7048 Query /* mysql-connector-java-5.1.40 ( Revision: 402933ef52cad9aa82624e80acbea46e3a701ce6 )
*/SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client,
@@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results,
@@character_set_server AS character_set_server,@@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout,
@@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet,
@@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@query_cache_size AS query_cache_size,
@@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone,
@@tx_isolation AS tx_isolation, @@wait_timeout AS wait_timeout
2021-04-28T17:23:01.467362+08:00     7048 Query SHOW WARNINGS
2021-04-28T17:23:01.469893+08:00     7048 Query SET NAMES utf8mb4
2021-04-28T17:23:01.470325+08:00     7048 Query SET character_set_results = NULL
2021-04-28T17:23:01.470681+08:00     7048 Query SET autocommit=1
2021-04-28T17:23:01.580189+08:00     7048 Query SELECT 1
2021-04-28T17:23:01.584444+08:00     7048 Query select @@session.tx_read_only
2021-04-28T17:23:01.584964+08:00     7048 Query SELECT @@session.tx_isolation
······
2021-04-28T17:23:10.621839+08:00     7052 Quit
2021-04-28T17:23:12.623470+08:00     7051 Query SELECT 1
2021-04-28T17:23:12.624380+08:00     7053 Query SELECT 1
2021-04-28T17:23:14.625555+08:00     7053 Query SELECT 1
2021-04-28T17:23:14.626719+08:00     7051 Query SELECT 1
2021-04-28T17:23:16.627945+08:00     7051 Query SELECT 1
2021-04-28T17:23:16.628719+08:00     7053 Query SELECT 1
2021-04-28T17:23:18.629940+08:00     7053 Query SELECT 1
2021-04-28T17:23:18.630674+08:00     7051 Query SELECT 1

如果翻阅文章的老师们有对 Druid 探活或其他参数的研究欢迎后台留言联系小编,水平有限,敬请您的赐教。

参考

https://github.com/alibaba/druid/blob/1.2.5/src/main/java/com/alibaba/druid/pool/DruidDataSource.java

https://github.com/alibaba/druid/blob/1.2.5/src/main/java/com/alibaba/druid/pool/vendor/MySqlValidConnectionChecker.java

鸣谢:

爱可生CTO-黄炎先生 以及 爱可生研发-孙健先生,感谢两位老师对 Druid 测试提供的帮助。