写点什么

MySQL 使用 ReplicationConnection 导致的连接失效分析与解决

  • 2022 年 6 月 23 日
  • 本文字数:5356 字

    阅读完需:约 18 分钟

MySQL 数据库读写分离,是提高服务质量的常用手段之一,而对于技术方案,有很多成熟开源框架或方案,例如:sharding-jdbc、spring 中的 AbstractRoutingDatasource、MySQL-Router 等,而 mysql-jdbc 中的 ReplicationConnection 亦可支持。本文暂不对读写分离的技术选型做过多的分析,只是探索在使用 druid 作为数据源、结合 ReplicationConnection 做读写分离时,连接失效的原因,并找到一个简单有效的解决方案。

问题背景

由于历史原因,某几个服务出现连接失效异常,关键报错如下:



从日志不难看出,这是由于该连接长时间未和 MySQL 服务端交互,服务端已将连接关闭,典型的连接失效场景。

涉及的主要配置如下:

jdbc 配置


jdbc:mysql:replication://master_host:port,slave_host:port/database_name


druid 配置


testWhileIdle=true(即,开启了空闲连接检查);timeBetweenEvictionRunsMillis=6000L(即,对于获取连接的场景,如果某连接空闲时间超过 1 分钟,将会进行检查,如果连接无效,将抛弃后重新获取)。


附:DruidDataSource.getConnectionDirect 中,处理逻辑如下:


if (testWhileIdle) {    final DruidConnectionHolder holder = poolableConnection.holder;    long currentTimeMillis             = System.currentTimeMillis();    long lastActiveTimeMillis          = holder.lastActiveTimeMillis;    long lastExecTimeMillis            = holder.lastExecTimeMillis;    long lastKeepTimeMillis            = holder.lastKeepTimeMillis;
if (checkExecuteTime && lastExecTimeMillis != lastActiveTimeMillis) { lastActiveTimeMillis = lastExecTimeMillis; }
if (lastKeepTimeMillis > lastActiveTimeMillis) { lastActiveTimeMillis = lastKeepTimeMillis; }
long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis;
if (timeBetweenEvictionRunsMillis <= 0) { timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; }
if (idleMillis >= timeBetweenEvictionRunsMillis || idleMillis < 0 // unexcepted branch ) { boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { if (LOG.isDebugEnabled()) { LOG.debug("skip not validate connection."); }
discardConnection(poolableConnection.holder); continue; } }}
复制代码


mysql 超时参数配置


wait_timeout=3600(3600 秒,即:如果某连接超过一个小时和服务端没有交互,该连接将会被服务端 kill)。显而易见,基于如上配置,按照常规理解,不应该出现“The last packet successfully received from server was xxx,xxx,xxx milliseconds ago”的问题。(当然,当时也排除了人工介入 kill 掉数据库连接的可能)。


当“理所应当”的经验解释不了问题所在,往往需要跳出可能浮于表面经验束缚,来一次追根究底。那么,该问题的真正原因是什么呢?

本质原因

当使用 druid 管理数据源,结合 mysql-jdbc 中原生的 ReplicationConnection 做读写分离时,ReplicationConnection 代理对象中实际存在 master 和 slaves 两套连接,druid 在做连接检测时候,只能检测到其中的 master 连接,如果某个 slave 连接长时间未使用,会导致连接失效问题。

原因分析

mysql-jdbc 中,数据库驱动对连接的处理过程

结合 com.mysql.jdbc.Driver 源码,不难看出 mysql-jdbc 中获取连接的主体流程如下:



对于以“jdbc:mysql:replication://”开头配置的 jdbc-url,通过 mysql-jdbc 获取到的连接,其实是一个 ReplicationConnection 的代理对象,默认情况下,“jdbc:mysql:replication://”后的第一个 host 和 port 对应 master 连接,其后的 host 和 port 对应 slaves 连接,而对于存在多个 slave 配置的场景,默认使用随机策略进行负载均衡。


ReplicationConnection 代理对象,使用 JDK 动态代理生成的,其中 InvocationHandler 的具体实现,是 ReplicationConnectionProxy,关键代码如下:


public static ReplicationConnection createProxyInstance(List<String> masterHostList, Properties masterProperties, List<String> slaveHostList,            Properties slaveProperties) throws SQLException {      ReplicationConnectionProxy connProxy = new ReplicationConnectionProxy(masterHostList, masterProperties, slaveHostList, slaveProperties);      return (ReplicationConnection) java.lang.reflect.Proxy.newProxyInstance(ReplicationConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy); }
复制代码

ReplicationConnectionProxy 的重要组成

关于数据库连接代理,ReplicationConnectionProxy 中的主要组成如下图:



ReplicationConnectionProxy 存在 masterConnection 和 slavesConnection 两个实际连接对象,currentConnetion(当前连接)可以切换成 mastetConnection 或者 slavesConnection,切换方式可以通过设置 readOnly 实现。业务逻辑中,实现读写分离的核心也在于此,简单来说:使用 ReplicationConnection 做读写分离时,只要做一个“设置 connection 的 readOnly 属性的”aop 即可。基于 ReplicationConnectionProxy,业务逻辑中获取到的 Connection 代理对象,数据库访问时的主要逻辑是什么样的呢?

ReplicationConnection 代理对象处理过程

对于业务逻辑而言,获取到的 Connection 实例,是 ReplicationConnection 代理对象,该代理对象通过 ReplicationConnectionProxy 和 ReplicationMySQLConnection 相互协同完成对数据库访问的处理,其中 ReplicationConnectionProxy 在实现 InvocationHandler 的同时,还充当对连接管理的角色,核心逻辑如下图:



对于 prepareStatement 等常规逻辑,ConnectionMySQConnection 获取到当前连接进行处理(普通的读写分离的处理的重点正是在此);此时,重点提及 pingInternal 方法,其处理方式也是获取当前连接,然后执行 pingInternal 逻辑。


对于 ping()这个特殊逻辑,图中描述相对简单,但主体含义不变,即:对 master 连接和 sleves 连接都要进行 ping()的处理。


图中,pingInternal 流程和 druid 的 MySQ 连接检查有关,而 ping 的特殊处理,也正是解决问题的关键。

druid 数据源对 MySQ 连接的检查

druid 中对 MySQL 连接检查的默认实现类是 MySqlValidConnectionChecker,其中核心逻辑如下:


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(); }
if (clazz.isAssignableFrom(conn.getClass())) { if (validationQueryTimeout <= 0) { validationQueryTimeout = DEFAULT_VALIDATION_QUERY_TIMEOUT; }
try { 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; 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); } rs = stmt.executeQuery(query); return true; } finally { JdbcUtils.close(rs); JdbcUtils.close(stmt); }
}
复制代码


对应服务中使用的 mysql-jdbc(5.1.45 版),在未设置“druid.mysql.usePingMethod”系统属性的情况下,默认 usePingMethod 为 true,如下:


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); }
configFromProperties(System.getProperties());}
@Overridepublic void configFromProperties(Properties properties) { String property = properties.getProperty("druid.mysql.usePingMethod"); if ("true".equals(property)) { setUsePingMethod(true); } else if ("false".equals(property)) { setUsePingMethod(false); }}
复制代码


同时,可以看出 MySqlValidConnectionChecker 中的 ping 方法使用的是 MySQLConnection 中的 pingInternal 方法,而该方法,结合上面对 ReplicationConnection 的分析,当调用 pingInternal 时,只是对当前连接进行检验。执行检验连接的时机是通过 DrduiDatasource 获取连接时,此时未设置 readOnly 属性,检查的连接,其实只是 ReplicationConnectionProxy 中的 master 连接。


此外,如果通过“druid.mysql.usePingMethod”属性设置 usePingMeghod 为 false,其实也会导致连接失效的问题,因为:当通过 valideQuery(例如“select 1”)进行连接校验时,会走到 ReplicationConnection 中的普通查询逻辑,此时对应的连接依然是 master 连接。


题外一问:ping 方法为什么使用“pingInternal”,而不是常规的 ping?原因:pingInternal 预留了超时时间等控制参数。

解决方式

调整依赖版本

服务中使用的 mysql-jdbc 版本为 5.1.45,druid 版本为 1.1.20。经过对其他高版本依赖的了解,依然存在该问题。

修改读写分离实现

修改的工作量主要在于数据源配置和 aop 调整,但需要一定的整体回归验证成本,鉴于涉及该问题的服务重要性一般,暂不做大调整。

拓展 mysql-jdbc 驱动

基于原有 ReplicationConnection 的功能,拓展 pingInternal 调整为普通的 ping,集成原有 Driver 拓展新的 Driver。方案可行,但修改成本不算小。

基于 druid,拓展 MySQL 连接检查

为简单高效解决问题,选择拓展 MySqlValidConnectionChecker,并在 druid 数据源中加上对应配置即可。拓展如下:


public class MySqlReplicationCompatibleValidConnectionChecker extends MySqlValidConnectionChecker {

private static final Log LOG = LogFactory.getLog(MySqlValidConnectionChecker.class); /** * */ private static final long serialVersionUID = 1L;
@Override public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
if (conn.isClosed()) { return false; }
if (conn instanceof DruidPooledConnection) { conn = ((DruidPooledConnection) conn).getConnection(); }
if (conn instanceof ConnectionProxy) { conn = ((ConnectionProxy) conn).getRawObject(); }
if (conn instanceof ReplicationConnection) {
try { ((ReplicationConnection) conn).ping(); LOG.info("validate connection success: connection=" + conn.toString()); return true; } catch (SQLException e) { LOG.error("validate connection error: connection=" + conn.toString(), e); throw e; }
}
return super.isValidConnection(conn, validateQuery, validationQueryTimeout); }
}
复制代码


ReplicatoinConnection.ping()的实现逻辑中,会对所有 master 和 slaves 连接进行 ping 操作,最终每个 ping 操作都会调用到 LoadBalancedConnectionProxy.doPing 进行处理,而此处,可在数据库配置 url 中设置 loadBalancePingTimeout 属性设置超时时间。




转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。关注公众号「转转技术」,各种干货实践,欢迎交流分享~

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2019.04.30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
MySQL使用ReplicationConnection导致的连接失效分析与解决_MySQL_转转技术团队_InfoQ写作社区