• 背景
  • 症状
  • 排查
  • 修复

背景

最近在陆续做机房升级相关工作,配合DBA对产线数据库链接方式做个调整,将原来直接链接读库的地址切换到统一的读负载均衡的代理 haproxy 上,方便机柜和服务器的搬迁。

切换之后线上时不时的会发生 discard connection 错误,导致程序报 500 错误,但不是每次都必现的。

开发框架: spring boot+mybatis+druid+shardingJDBC

网络架构:

appserver->mysql(master) 写

appserver->haproxy->mysql(slave)/n 读

第一反应肯定是因为这次的读库地址的变动引起的问题,觉得问题应该是 druid 链接池中的 connection 保活策略没起作用,只要做下配置修改应该就可以了。结果这个问题让我们排查了好几天,我们竟然踩到了千年难遇的深坑。

这个问题排查的很坎坷,一次次的吐血,最终我们定位到问题并且优雅的修复了,我们一起来体验下这个一次一次让你绝望一次一次打脸的过程。

症状

先说故障症状,经常出现如下错误:

discard connection

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 72,557 milliseconds ago. The last packet sent successfully to the server was 0 milliseconds ago.

根据错误日志初步判断肯定是与 db 之间的链接已经断开,尝试使用了一个已经断开的链接才会引起这个错误发生。但是根据我们对 druid 了解,druid 有链接检查功能,按理不会拿到一个无效链接才对,带着这个线索我们上路了。

排查

为了准确的知道 db 的链接的存活时间,了解到 haproxy 对转发的 db tcp 链接空闲时间在 1m 之内,超过 1m 不活动就会被关掉。也就说我们与 db 之间的原来的长链接在 1m 之内会被断开。我们先不管这个时间设置的是否符合所有的大并发场景,至少在 druid 的链接池里会有有效链接检查,应该不会拿到无效链接才对,我们做了配置调整。

我们看下 druid 跟链接时间相关的配置:

datasource.druid.validationQuery=SELECT 1
datasource.druid.validationQueryTimeout=2000
datasource.druid.testWhileIdle=true
datasource.druid.minEvictableIdleTimeMillis=100000
datasource.druid.timeBetweenEvictionRunsMillis=20000

配置的每项的意思这里就不解释了。

我们启用了 testWhileIdle 配置,让每次拿取链接的时候发起检查。根据 timeBetweenEvictionRunsMillis 的配置只有大于这个时间 druid 才会发起检查,所以可能的场景是拿到一个即将过期的链接,根据这个线索我们调整这个时间为 20000ms,也就是超过 20s 会检查当前拿取的链接确定是否有效,检查的方式应该是使用 validationQuery 配置的 sql 语句才对,但是发现我们并找不到任何有关于 SELECT 1 的痕迹。

为什么你死活找不到 SELECT 1

首先要搞清楚 validationQuery 为什么没起作用,带着这个疑问开始 debug druid 源码。

if (isTestWhileIdle()) {
final long currentTimeMillis = System.currentTimeMillis();
final long lastActiveTimeMillis = poolableConnection.getConnectionHolder().getLastActiveTimeMillis();
final long idleMillis = currentTimeMillis - lastActiveTimeMillis;
long timeBetweenEvictionRunsMillis = this.getTimeBetweenEvictionRunsMillis();
if (timeBetweenEvictionRunsMillis <= 0) {
timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
} if (idleMillis >= timeBetweenEvictionRunsMillis) {
boolean validate = testConnectionInternal(poolableConnection.getConnection());
if (!validate) {
if (LOG.isDebugEnabled()) {
LOG.debug("skip not validate connection.");
} discardConnection(realConnection);
continue;
}
}
}
}

闲置时间肯定会有大于 timeBetweenEvictionRunsMillis 时间的,会发起 testConnectionInternal 方法检查。我们继续跟进去看,

protected boolean testConnectionInternal(DruidConnectionHolder holder, Connection conn) {
boolean valid = validConnectionChecker.isValidConnection(conn, validationQuery, validationQueryTimeout);

内部会使用 validConnectionChecker 检查对象发起检查。

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);
} }

debug 这里才发现,druid 默认采用的是 mysql.ping 来做链接有效性检查。

druid 默认采用msyql.ping 协议检查

那是不是用 msyql.ping 协议并不会让 mysql 重新滑动 session 闲置时间,带着这个问题打开 information_schema.processlist 进程列表查看会不会刷新会话时间,通过 debug发现是会刷新时间的,说明没有问题,这条线索算是断了。

haproxy tiemout主动close上下游链接

调整方向,开始怀疑是不是 haproxy 的一些策略导致链接失效,开始初步怀疑 haproxy 的轮训转发后端链接是不是有相关会话保持方式,是不是我们配置有误导致 haproxy 的链接和 mysql 链接篡位了。

当然这个猜想有点夸张,但是没办法,技术人员就要有怀疑一切的态度。

为了还原产线的网络路线,我在本地搭了一个 haproxy,了解下他的工作原理和配置,图方便我就用了yum顺手装了一个,版本是 HA-Proxy version 1.5.18 不知道是我本地环境问题还是这个版本的 bug,我们配置的 mode tcp 活动检查一直不生效。

listen service 127.0.0.1:60020
mode tcp
balance roundrobin
option tcplog
server server1 192.168.36.66:3306 check inter 2000 rise 2 fall 3
server server2 192.168.36.66:3306 check inter 2000 rise 2 fall 3

由于 haproxy 活动检查一直不通过,所以无法转发我的链接,搞了半天我只能手动装了一个低版本的 haproxy HA-Proxy version 1.4.14

完整的配置:

defaults
mode tcp
retries 3
option redispatch
option abortonclose
maxconn 32000
timeout connect 2s
timeout client 5m
timeout server 5m listen test1
bind 0.0.0.0:60000
mode tcp
balance roundrobin
server s1 192.168.36.66:3306 weight 1 maxconn 10000 check inter 10s
server s2 192.168.36.66:3306 weight 1 maxconn 10000 check inter 10s
server s3 192.168.36.66:3306 weight 1 maxconn 10000 check inter 10s

1.4 的版本顺利完成活动检查。

我使用 haproxy 进行debug,调试下来也都没有问题,也翻了下 haproxy 如何转发链接的,内部通过会话的方式保持两个链接的关系,如果是 tcp 长链接应该不会出现什么问题。haproxyhttp 模式下有会话保持方式,tcp 应该是直接捆绑的方式,一旦到 timeout 时间会主动 closemysql 的链接,而且没有出现篡位的问题。到这里线索又断了。

自定义 ValidConnectionChecker 埋点日志

没有办法,只能试着埋点 druid 的检查日志,排查内部上一次的 check和报错之间的时间差和 connectionId 是不是一致的。

public class MySqlValidConnectionCheckerDebug extends MySqlValidConnectionChecker {

    @Override
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) { Long connId = 0L;
try {
Field connField = ConnectionImpl.class.getDeclaredField("connectionId");
connField.setAccessible(true);
connId = (Long) connField.get(((ConnectionProxyImpl) conn).getConnectionRaw());
} catch (Exception e) {
log.error("valid connection error", e);
} finally {
log.info("valid connection ok. conn:" + connId);
} return true;
}

为了拿到 connectionId 只能反射获取,在本地debug下没问题,能正常拿到 connectionId,但是发到验证环境进行验证的时候报错了,觉得奇怪,仔细看了下原来开发环境的配置和验证和生产的不一样,开发环境没有走读写分离。

验证和生产都是使用了 mysqlreplication 的机制,所以导致我反射获取的代码报错。

datasource.druid.url=jdbc:mysql:replication

通过debug发现,原来 __druid__的 connectionJDBC4Connection ,变成了 ReplicationConnection ,而且里面包装了两个 connection ,一个 masterconnection ,一个 slaveconnection ,似乎问题有点浮现了。

通过debug发现 druid 的检查还是会正常走到,当走到 ReplicationConnection 内部的时候 ReplicationConnection 有一个 currentConnection ,这个链接是会在 masterConnectionslaveConnection 之间切换,切换的依据是 readOnly 参数。

在检查的时候由于 druid 并不感知上层的参数,readOnly 也就不会设置。所以走的是 masterConnection ,但是在程序里用的时候通过 springTransactionManagerreadOnly 传播到了 ShardingJDBCShardingJDBC 在设置到 ReplicationConnection 上,最后导致真正在使用的时候其实使用的是 slaveConnection

找到这个问题之后去 druid github Issues 搜索了下果然有人提过这个问题,在高版本的 druid 中已经修复这个问题了。

修复

修复这个问题有两个方法,第一个方法,建议升级 druid,里面已经有 MySqlReplicationValidConnectionChecker 检查器专门用来解决这个问题。第二个方法就是自己实现 ValidConnectionChecker 检查器,但是会有在将来出现bug的可能性。

由于时间关系文章只讲了主要的排查路线,事实上我们陆续花了一周多时间,再加上周末连续趴上十几个小时才找到这根本问题。

这个问题之所以难定位的原因主要是牵扯的东西太多,框架层面、网络链接层面、mysql服务器层面,haproxy代理等等,当然其中也绕了很多弯路。。

下面分享在这个整个排查过程中的一些技术收获。

相关技术问题

1.mysqlConenction提供了ping方法用来做活动检查,默认MySqlValidConnectionChecker使用的是pinginternal。

ping = clazz.getMethod("pingInternal", boolean.class, int.class);

2.低版本的druid不支持自定义 ValidConnectionChecker 来做个性化的检查。

3.druid 的test方法使用注意事项,testOnBorrow 在获取链接的时候进行检查,与testWhileIdle是护持关系。

if (isTestOnBorrow()) {
} else {
if (isTestWhileIdle()) {

3.kill mysql processlist 进程会话到链接端tcp状态有延迟,这是tcp的四次断开延迟。

4.haproxy 1.5.18 版本 mode tcp check不执行,健康检查设置无效。

5.mysql replication connection master/slave切换逻辑需要注意,会不会跟上下油的链接池组合使用出现bug,尤其是分库不表、读写分离、自定义分片。

6.排查mysql服务器的问题时,打开各种日志,操作日志,binlog日志。

7.springtransactionmanagenent 事务传播特性会影响下游数据源的选择,setreadonly、setautocommit。

8.低版本的 druid MySqlValidConnectionChecker 永远执行不到 ReplicationConnection ping 方法。

作者:王清培(沪江网资深架构师)

诡异的druid链接池链接断开故障经验总结的更多相关文章

  1. Spring中Druid链接池的配置

    本文记录了使用Druid的方法, 包括Spring和Spring boot中使用Druid的配置方法. Spring中配置Druid连接池 以链接mysql为例 1 添加druid依赖 <dep ...

  2. java 代码实现使用Druid 链接池获取数据库链接

    因为原先使用的c3p0链接池,时常出现:APPARENT DEADLOCK!!! Creating emergency threads for unassigned pending tasks,以及出 ...

  3. JDBC核心技术(获取数据库链接、数据库事务、数据库链接池)

    @ 目录 前言 数据的持久化 Java数据存储技术 JDBC介绍 JDBC体系结构 获取数据库链接 Driver接口 加载注册JDBC驱动 获取数据库链接 数据库链接方式(实例) 方式一:代码中显示出 ...

  4. 通过dbcp链接池对数据库操作报 Cannot create PoolableConnectionFactory (Could not create connection to database server. Attempted reconnect 3 times. Giving up.)--解决方案

    org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for ...

  5. c3p0链接池

    频繁的链接数据库是非常消耗性能的,所以就采用了将一定量的链接保存在一个池中,这个池我们叫做链接池. 详细请看:http://baike.baidu.com/link?url=dlTW-fTS3N_-j ...

  6. ServiceStack.Redis 使用链接池方法

    PooledRedisClientManager 1.RedisManage.cs public static class RedisManager { private static PooledRe ...

  7. 自定义JDBC链接池

    上篇简单介绍了jdbc链接数据库: 本篇就说一下自定义连接池以及增删改查的测试: 自定义连接池 自定义链接池的原因 JDBC连接中用到Connection   在每次对数据进行增删查改 都要 开启  ...

  8. 数据库链接池c3p0的配置

    由于我看的是远古教程,所以里面各种驱动jar包还有c3p0包都是远古版本,对于最新版本的jdbc已经失去的作用,所以我在这里重写一下! 1.首先是c3p0的位置,package的外面,src的里面 2 ...

  9. SpringBoot使用Druid数据库加密链接完整方案

    网上的坑 springboot 使用 Druid 数据库加密链接方案,不建议采用网上的一篇文章<springboot 结合 Druid 加密数据库密码遇到的坑!>介绍的方式来进行加密链接实 ...

随机推荐

  1. xml方式将dataset导出excel

    using System;using System.Collections;using System.Collections.Generic;using System.Data;using Syste ...

  2. XXX系统项目目标文档课堂讨论

    XXXX重大技术征集系统 1.讨论结果: 2.项目目标文档 A目标: 1. 实现普通用户在线需求填报,个人信息管理,需求结果查看. 2. 实现审核员用户的需求审核,需求查看浏览和生成图表结果. 3. ...

  3. 06. Matplotlib 2 |折线图| 柱状图| 堆叠图| 面积图| 填图| 饼图| 直方图| 散点图| 极坐标| 图箱型图

    1.基本图表绘制 plt.plot() 图表类别:线形图.柱状图.密度图,以横纵坐标两个维度为主同时可延展出多种其他图表样式 plt.plot(kind='line', ax=None, figsiz ...

  4. 043 hive数据同步到mysql

    一:意义 1.意义 如果可以实现这个功能,就可以使用spark代替sqoop,功能程序就实现这个功能. 二:hive操作 1.准备数据 启动hive 否则报错,因为在hive与spark集成的时候,配 ...

  5. Constructing Roads-最小生成树(kruskal)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1102 题目描述: #include<cstdio> #include<cstring ...

  6. JAVA项目中常用的异常处理情况

    1.数学运算异常( java.lang.arithmeticexception) 程序中出现了除以零这样的运算就会出这样的异常,对这种异常,大家就要好好检查一下自己程序中涉及到数学运算的地方,公式是不 ...

  7. C# SQLiteHelper

    using System; using System.Data; using System.Data.Common; using System.Data.SQLite; using System.IO ...

  8. PHPstorm配置SVN的问题

    开始尝试用PHPstorm做项目开发,在集成SVN的时候碰到了问题. 1. PHPstorm Cannot run program "svn" 2. Subversion comm ...

  9. 第13届景驰-埃森哲杯广东工业大学ACM程序设计大赛.B.跳一跳,很简单的(Hash 倍增)

    题目链接 首先变换的周期是\(26\),而所有字符是同时变的,所以一共就只有\(26\)种树,我们对\(26\)棵树分别处理. 求某节点到根路径上的字符串,可以从根往下哈希,\(O(n)\)预处理出. ...

  10. 潭州课堂25班:Ph201805201 django 项目 第二十一课 文章主页 新闻列表页面功能 (课堂笔记)

    新闻列表页功能 1.分析 业务处理流程: 判断前端传的标签分类id是否为空,是否为整数.是否超过范围 判断前端传的当前文章页数是否为空,是否为整数.是否超过范围 请求方法:GET url定义:/new ...