声明:本文为博主原创文章,因为已受权部分平台发表该文章(知乎、云社区),可能形成发布时间方面的困扰。前端
近期由测试反馈的问题有点多,其中关于系统可靠性测试提出的问题使人感到头疼,一来这类问题有时候属于“偶发”现象,难以在环境上快速复现;二来则是可靠性问题的定位链条有时候变得很长,极端状况下可能要从 A 服务追踪到 Z 服务,或者是从应用代码追溯到硬件层面。java
本次分享的是一次关于 MySQL 高可用问题的定位过程,其中曲折颇多但问题自己却比较有些表明性,遂将其记录以供参考。node
首先,本系统以 MySQL 做为主要的数据存储部件。整一个是典型的微服务架构(SpringBoot + SpringCloud),持久层则采用了以下几个组件:mysql
在 MySQL 服务端部分,后端采用了双主架构,前端以 keepalived 结合浮动IP(VIP)作一层高可用。以下:算法
说明spring
MySQL 部署两台实例,设定为互为主备的关系。sql
为每台 MySQL 实例部署一个 keepalived 进程,由 keepalived 提供 VIP 高可用的故障切换。数据库
实际上,keepalived 和 MySQL 都实现了容器化,而 VIP 端口则映射到 VM 上的 nodePort 服务端口上。后端
业务服务一概使用 VIP 进行数据库访问。网络
Keepalived 是基于 VRRP 协议实现了路由层转换的,在同一时刻,VIP 只会指向其中的一个虚拟机(master)。当主节点发生故障时,其余的 keepalived 会检测到问题并从新选举出新的 master,此后 VIP 将切换到另外一个可用的 MySQL 实例节点上。这样一来,MySQL 数据库就拥有了基础的高可用能力。
另一点,Keepalived 还会对 MySQL 实例进行定时的健康检查,一旦发现 MySQL 实例不可用会将自身进程杀死,进而再触发 VIP 的切换动做。
本次的测试用例也是基于虚拟机故障的场景来设计的:
持续以较小的压力向业务服务发起访问,随后将其中一台 MySQL 的容器实例(master)重启。 按照原有的评估,业务可能会产生很小的抖动,但其中断时间应该保持在秒级。
然而通过屡次的测试后发现,在重启 MySQL 主节点容器以后,有必定的几率会出现业务却再也没法访问的状况!
在发生问题以后,开发同窗的第一反应是 MySQL 的高可用机制出了问题。因为此前曾经出现过因为 keepalived 配置不当致使 VIP 未能及时切换的问题,所以对其已经有所戒备。
先是通过一通的排查,而后并无找到 keepalived 任何配置上的毛病。
而后在没有办法的状况下,从新测试了几回,问题又复现了。
紧接着,咱们提出了几个疑点:
但在本次测试场景中,MySQL 容器销毁会致使 keepalived 的端口探测产生失败,这一样会致使 keepalived 失效。若是 keepalived 也发生了停止,那么 VIP 应该能自动发生抢占。而经过对比两台虚拟机节点的信息后,发现 VIP 的确发生了切换。
业务进程所在的容器是否发生了网络不可达的问题?
尝试进入容器,对当前发生切换后的浮动IP、端口执行 telnet 测试,发现仍然能访问成功。
在排查前面两个疑点以后,咱们只能将目光转向了业务服务的DB客户端上。
从日志上看,在产生故障的时刻,业务侧的确出现了一些异常,以下:
Unable to acquire JDBC Connection [n/a] java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms. at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669) ~[HikariCP-2.7.9.jar!/:?] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183) ~[HikariCP-2.7.9.jar!/:?] ...
这里提示的是业务操做获取链接超时了(超过了30秒)。那么,会不会是链接数不够用呢?
业务接入采用的是 hikariCP 链接池,这也是市面上流行度很高的一款组件了。
咱们随即检查了当前的链接池配置,以下:
//最小空闲链接数 spring.datasource.hikari.minimum-idle=10 //链接池最大大小 spring.datasource.hikari.maximum-pool-size=50 //链接最大空闲时长 spring.datasource.hikari.idle-timeout=60000 //链接生命时长 spring.datasource.hikari.max-lifetime=1800000 //获取链接的超时时长 spring.datasource.hikari.connection-timeout=30000
其中 注意到 hikari 链接池配置了 minimum-idle = 10,也就是说,就算在没有任何业务的状况下,链接池应该保证有 10 个链接。更况且当前的业务访问量极低,不该该存在链接数不够使用的状况。
除此以外,另一种可能性则多是出现了“僵尸链接”,也就是说在重启的过程当中,链接池一直没有释放这些不可用的链接,最终形成没有可用链接的结果。
开发同窗对"僵尸连接"的说法深信不疑,倾向性的认为这极可能是来自于 HikariCP 组件的某个 BUG..
因而开始走读 HikariCP 的源码,发现应用层向链接池请求链接的一处代码以下:
public class HikariPool{ //获取链接对象入口 public Connection getConnection(final long hardTimeout) throws SQLException { suspendResumeLock.acquire(); final long startTime = currentTime(); try { //使用预设的30s 超时时间 long timeout = hardTimeout; do { //进入循环,在指定时间内获取可用链接 //从 connectionBag 中获取链接 PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS); if (poolEntry == null) { break; // We timed out... break and throw exception } final long now = currentTime(); //链接对象被标记清除或不知足存活条件时,关闭该链接 if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && !isConnectionAlive(poolEntry.connection))) { closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); timeout = hardTimeout - elapsedMillis(startTime); } //成功得到链接对象 else { metricsTracker.recordBorrowStats(poolEntry, startTime); return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now); } } while (timeout > 0L); //超时了,抛出异常 metricsTracker.recordBorrowTimeoutStats(startTime); throw createTimeoutException(startTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(poolName + " - Interrupted during connection acquisition", e); } finally { suspendResumeLock.release(); } } }
getConnection() 方法展现了获取链接的整个流程,其中 connectionBag 是用于存放链接对象的容器对象。若是从 connectionBag 得到的链接再也不知足存活条件,那么会将其手动关闭,代码以下:
void closeConnection(final PoolEntry poolEntry, final String closureReason) { //移除链接对象 if (connectionBag.remove(poolEntry)) { final Connection connection = poolEntry.close(); //异步关闭链接 closeConnectionExecutor.execute(() -> { quietlyCloseConnection(connection, closureReason); //因为可用链接变少,将触发填充链接池的任务 if (poolState == POOL_NORMAL) { fillPool(); } }); } }
注意到,只有当链接知足下面条件中的其中一个时,会被执行 close。
isMarkedEvicted() 的返回结果是 true,即标记为清除
若是链接存活时间超出最大生存时间(maxLifeTime),或者距离上一次使用超过了idleTimeout,会被定时任务标记为清除状态,清除状态的链接在获取的时候才真正 close。
500ms 内没有被使用,且链接已经再也不存活,即 isConnectionAlive() 返回 false
因为咱们把 idleTimeout 和 maxLifeTime 都设置得很是大,所以需重点检查 isConnectionAlive 方法中的判断,以下:
public class PoolBase{ //判断链接是否存活 boolean isConnectionAlive(final Connection connection) { try { try { //设置 JDBC 链接的执行超时 setNetworkTimeout(connection, validationTimeout); final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000; //若是没有设置 TestQuery,使用 JDBC4 的校验接口 if (isUseJdbc4Validation) { return connection.isValid(validationSeconds); } //使用 TestQuery(如 select 1)语句对链接进行探测 try (Statement statement = connection.createStatement()) { if (isNetworkTimeoutSupported != TRUE) { setQueryTimeout(statement, validationSeconds); } statement.execute(config.getConnectionTestQuery()); } } finally { setNetworkTimeout(connection, networkTimeout); if (isIsolateInternalQueries && !isAutoCommit) { connection.rollback(); } } return true; } catch (Exception e) { //发生异常时,将失败信息记录到上下文 lastConnectionFailure.set(e); logger.warn("{} - Failed to validate connection {} ({}). Possibly consider using a shorter maxLifetime value.", poolName, connection, e.getMessage()); return false; } } }
咱们看到,在PoolBase.isConnectionAlive 方法中对链接执行了一系列的探测,若是发生异常还会将异常信息记录到当前的线程上下文中。随后,在 HikariPool 抛出异常时会将最后一次检测失败的异常也一同收集,以下:
private SQLException createTimeoutException(long startTime) { logPoolState("Timeout failure "); metricsTracker.recordConnectionTimeout(); String sqlState = null; //获取最后一次链接失败的异常 final Throwable originalException = getLastConnectionFailure(); if (originalException instanceof SQLException) { sqlState = ((SQLException) originalException).getSQLState(); } //抛出异常 final SQLException connectionException = new SQLTransientConnectionException(poolName + " - Connection is not available, request timed out after " + elapsedMillis(startTime) + "ms.", sqlState, originalException); if (originalException instanceof SQLException) { connectionException.setNextException((SQLException) originalException); } return connectionException; }
这里的异常消息和咱们在业务服务中看到的异常日志基本上是吻合的,即除了超时产生的 "Connection is not available, request timed out after xxxms" 消息以外,日志中还伴随输出了校验失败的信息:
Caused by: java.sql.SQLException: Connection.setNetworkTimeout cannot be called on a closed connection at org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper.getSqlException(ExceptionMapper.java:211) ~[mariadb-java-client-2.2.6.jar!/:?] at org.mariadb.jdbc.MariaDbConnection.setNetworkTimeout(MariaDbConnection.java:1632) ~[mariadb-java-client-2.2.6.jar!/:?] at com.zaxxer.hikari.pool.PoolBase.setNetworkTimeout(PoolBase.java:541) ~[HikariCP-2.7.9.jar!/:?] at com.zaxxer.hikari.pool.PoolBase.isConnectionAlive(PoolBase.java:162) ~[HikariCP-2.7.9.jar!/:?] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:172) ~[HikariCP-2.7.9.jar!/:?] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148) ~[HikariCP-2.7.9.jar!/:?] at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-2.7.9.jar!/:?]
到这里,咱们已经将应用得到链接的代码大体梳理了一遍,整个过程以下图所示:
从执行逻辑上看,链接池的处理并无问题,相反其在许多细节上都考虑到位了。在对非存活链接执行 close 时,一样调用了 removeFromBag 动做将其从链接池中移除,所以也不该该存在僵尸链接对象的问题。
那么,咱们以前的推测应该就是错误的!
在代码分析之余,开发同窗也注意到当前使用的 hikariCP 版本为 3.4.5,而环境上出问题的业务服务倒是 2.7.9 版本,这仿佛预示着什么.. 让咱们再次假设 hikariCP 2.7.9 版本存在某种未知的 BUG,致使了问题的产生。
为了进一步分析链接池对于服务端故障的行为处理,咱们尝试在本地机器上进行模拟,这一次使用了 hikariCP 2.7.9 版本进行测试,并同时将 hikariCP 的日志级别设置为 DEBUG。
模拟场景中,会由 由本地应用程序链接本机的 MySQL 数据库进行操做,步骤以下:
1. 初始化数据源,此时链接池 min-idle 设置为 10; 2. 每隔50ms 执行一次SQL操做,查询当前的元数据表; 3. 将 MySQL 服务中止一段时间,观察业务表现; 4. 将 MySQL 服务从新启动,观察业务表现。
最终产生的日志以下:
//初始化过程,创建10个链接 DEBUG -HikariPool.logPoolState - Pool stats (total=1, active=1, idle=0, waiting=0) DEBUG -HikariPool$PoolEntryCreator.call- Added connection MariaDbConnection@71ab7c09 DEBUG -HikariPool$PoolEntryCreator.call- Added connection MariaDbConnection@7f6c9c4c DEBUG -HikariPool$PoolEntryCreator.call- Added connection MariaDbConnection@7b531779 ... DEBUG -HikariPool.logPoolState- After adding stats (total=10, active=1, idle=9, waiting=0) //执行业务操做,成功 execute statement: true test time -------1 execute statement: true test time -------2 ... //中止MySQL ... //检测到无效链接 WARN -PoolBase.isConnectionAlive - Failed to validate connection MariaDbConnection@9225652 ((conn=38652) Connection.setNetworkTimeout cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value. WARN -PoolBase.isConnectionAlive - Failed to validate connection MariaDbConnection@71ab7c09 ((conn=38653) Connection.setNetworkTimeout cannot be called on a closed connection). Possibly consider using a shorter maxLifetime value. //释放链接 DEBUG -PoolBase.quietlyCloseConnection(PoolBase.java:134) - Closing connection MariaDbConnection@9225652: (connection is dead) DEBUG -PoolBase.quietlyCloseConnection(PoolBase.java:134) - Closing connection MariaDbConnection@71ab7c09: (connection is dead) //尝试建立链接失败 DEBUG -HikariPool.createPoolEntry - Cannot acquire connection from data source java.sql.SQLNonTransientConnectionException: Could not connect to address=(host=localhost)(port=3306)(type=master) : Socket fail to connect to host:localhost, port:3306. Connection refused: connect Caused by: java.sql.SQLNonTransientConnectionException: Socket fail to connect to host:localhost, port:3306. Connection refused: connect at internal.util.exceptions.ExceptionFactory.createException(ExceptionFactory.java:73) ~[mariadb-java-client-2.6.0.jar:?] ... //持续失败.. 直到MySQL重启 //重启后,自动建立链接成功 DEBUG -HikariPool$PoolEntryCreator.call -Added connection MariaDbConnection@42c5503e DEBUG -HikariPool$PoolEntryCreator.call -Added connection MariaDbConnection@695a7435 //链接池状态,从新创建10个链接 DEBUG -HikariPool.logPoolState(HikariPool.java:421) -After adding stats (total=10, active=1, idle=9, waiting=0) //执行业务操做,成功(已经自愈) execute statement: true
从日志上看,hikariCP 仍是能成功检测到坏死的链接并将其踢出链接池,一旦 MySQL 从新启动,业务操做又能自动恢复成功了。根据这个结果,基于 hikariCP 版本问题的设想也再次落空,研发同窗再次陷入焦灼。
多方面求证无果以后,咱们最终尝试在业务服务所在的容器内进行抓包,看是否能发现一些蛛丝马迹。
进入故障容器,执行 tcpdump -i eth0 tcp port 30052 进行抓包,而后对业务接口发起访问。
此时使人诡异的事情发生了,没有任何网络包产生!而业务日志在 30s 以后也出现了获取链接失败的异常。
咱们经过 netstat 命令检查网络链接,发现只有一个 ESTABLISHED 状态的 TCP 链接。
也就是说,当前业务实例和 MySQL 服务端是存在一个建好的链接的,但为何业务仍是报出可用链接呢?
推测可能缘由有二:
对于缘由一,很快就能够被推翻,一来当前服务并无什么定时器任务,二来就算该链接被占用,按照链接池的原理,只要没有达到上限,新的业务请求应该会促使链接池进行新链接的创建,那么不管是从 netstat 命令检查仍是 tcpdump 的结果来看,不该该一直是只有一个链接的情况。
那么,状况二的可能性就很大了。带着这个思路,继续分析 Java 进程的线程栈。
执行 kill -3 pid 将线程栈输出后分析,果不其然,在当前 thread stack 中发现了以下的条目:
"HikariPool-1 connection adder" #121 daemon prio=5 os_prio=0 tid=0x00007f1300021800 nid=0xad runnable [0x00007f12d82e5000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141) at java.io.FilterInputStream.read(FilterInputStream.java:133) at org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream.fillBuffer(ReadAheadBufferedStream.java:129) at org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream.read(ReadAheadBufferedStream.java:102) - locked <0x00000000d7f5b480> (a org.mariadb.jdbc.internal.io.input.ReadAheadBufferedStream) at org.mariadb.jdbc.internal.io.input.StandardPacketInputStream.getPacketArray(StandardPacketInputStream.java:241) at org.mariadb.jdbc.internal.io.input.StandardPacketInputStream.getPacket(StandardPacketInputStream.java:212) at org.mariadb.jdbc.internal.com.read.ReadInitialHandShakePacket.<init>(ReadInitialHandShakePacket.java:90) at org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol.createConnection(AbstractConnectProtocol.java:480) at org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol.connectWithoutProxy(AbstractConnectProtocol.java:1236) at org.mariadb.jdbc.internal.util.Utils.retrieveProxy(Utils.java:610) at org.mariadb.jdbc.MariaDbConnection.newConnection(MariaDbConnection.java:142) at org.mariadb.jdbc.Driver.connect(Driver.java:86) at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:358) at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:477)
这里显示 HikariPool-1 connection adder 这个线程一直处于 socketRead 的可执行状态。从命名上看该线程应该是 HikariCP 链接池用于创建链接的任务线程,socket 读操做则来自于 MariaDbConnection.newConnection() 这个方法,即 mariadb-java-client 驱动层创建 MySQL 链接的一个操做,其中 ReadInitialHandShakePacket 初始化则属于 MySQL 建链协议中的一个环节。
简而言之,上面的线程恰好处于建链的一个过程态,关于 mariadb 驱动和 MySQL 建链的过程大体以下:
MySQL 建链首先是创建 TCP 链接(三次握手),客户端会读取 MySQL 协议的一个初始化握手消息包,内部包含 MySQL 版本号,鉴权算法等等信息,以后再进入身份鉴权的环节。
这里的问题就在于 ReadInitialHandShakePacket 初始化(读取握手消息包)一直处于 socket read 的一个状态。
若是此时 MySQL 远端主机故障了,那么该操做就会一直卡住。而此时的链接虽然已经创建(处于 ESTABLISHED 状态),但却一直没能完成协议握手和后面的身份鉴权流程,即该链接只能算一个半成品(没法进入 hikariCP 链接池的列表中)。从故障服务的 DEBUG 日志也能够看到,链接池持续是没有可用链接的,以下:
DEBUG HikariPool.logPoolState --> Before cleanup stats (total=0, active=0, idle=0, waiting=3)
另外一个须要解释的问题则是,这样一个 socket read 操做的阻塞是否就形成了整个链接池的阻塞呢?
通过代码走读,咱们再次梳理了 hikariCP 创建链接的一个流程,其中涉及到几个模块:
HouseKeeper 在链接池初始化后的 100ms 触发执行,其调用 fillPool() 方法完成链接池的填充,例如 min-idle 是10,那么初始化就会建立10个链接。ConnectionBag 维护了当前链接对象的列表,该模块还维护了请求链接者(waiters)的一个计数器,用于评估当前链接数的需求。
其中,borrow 方法的逻辑以下:
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException { // 尝试从 thread-local 中获取 final List<Object> list = threadList.get(); for (int i = list.size() - 1; i >= 0; i--) { ... } // 计算当前等待请求的任务 final int waiting = waiters.incrementAndGet(); try { for (T bagEntry : sharedList) { if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { //若是得到了可用链接,会触发填充任务 if (waiting > 1) { listener.addBagItem(waiting - 1); } return bagEntry; } } //没有可用链接,先触发填充任务 listener.addBagItem(waiting); //在指定时间内等待可用链接进入 timeout = timeUnit.toNanos(timeout); do { final long start = currentTime(); final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS); if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return bagEntry; } timeout -= elapsedNanos(start); } while (timeout > 10_000); return null; } finally { waiters.decrementAndGet(); } }
注意到,不管是有没有可用链接,该方法都会触发一个 listener.addBagItem() 方法,HikariPool 对该接口的实现以下:
public void addBagItem(final int waiting) { final boolean shouldAdd = waiting - addConnectionQueueReadOnlyView.size() >= 0; // Yes, >= is intentional. if (shouldAdd) { //调用 AddConnectionExecutor 提交建立链接的任务 addConnectionExecutor.submit(poolEntryCreator); } else { logger.debug("{} - Add connection elided, waiting {}, queue {}", poolName, waiting, addConnectionQueueReadOnlyView.size()); } }
PoolEntryCreator 则实现了建立链接的具体逻辑,以下:
public class PoolEntryCreator{ @Override public Boolean call() { long sleepBackoff = 250L; //判断是否须要创建链接 while (poolState == POOL_NORMAL && shouldCreateAnotherConnection()) { //建立 MySQL 链接 final PoolEntry poolEntry = createPoolEntry(); if (poolEntry != null) { //创建链接成功,直接返回。 connectionBag.add(poolEntry); logger.debug("{} - Added connection {}", poolName, poolEntry.connection); if (loggingPrefix != null) { logPoolState(loggingPrefix); } return Boolean.TRUE; } ... } // Pool is suspended or shutdown or at max size return Boolean.FALSE; } }
因而可知,AddConnectionExecutor 采用了单线程的设计,当产生新链接需求时,会异步触发 PoolEntryCreator 任务进行补充。其中 PoolEntryCreator. createPoolEntry() 会完成 MySQL 驱动链接创建的全部事情,而咱们的状况则偏偏是 MySQL 建链过程产生了永久性阻塞。所以不管后面怎么获取链接,新来的建链任务都会一直排队等待,这便致使了业务上一直没有链接可用。
下面这个图说明了 hikariCP 的建链过程:
好了,让咱们在回顾一下前面关于可靠性测试的场景:
首先,MySQL 主实例发生故障,而紧接着 hikariCP 则检测到了坏的链接(connection is dead)并将其释放,在释放关闭链接的同时又发现链接数须要补充,进而当即触发了新的建链请求。而问题就恰好出在这一次建链请求上,TCP 握手的部分是成功了(客户端和 MySQL VM 上 nodePort 的完成链接),但在接下来因为当前的 MySQL 容器已经中止(此时 VIP 也切换到了另外一台 MySQL 实例上),所以客户端再也没法得到原 MySQL 实例的握手包响应(该握手属于MySQL应用层的协议),此时便陷入了长时间的阻塞式 socketRead 操做。而建链请求任务偏偏好采用了单线程运做,进一步则致使了全部业务的阻塞。
在了解了事情的前因后果以后,咱们主要考虑从两方面进行优化:
对于优化点一,咱们一致认为用处并不大,若是链接出现了挂死那么至关于线程资源已经泄露,对服务后续的稳定运行十分不利,并且 hikariCP 在这里也已经将其写死了。所以关键的方案仍是避免阻塞式的调用。
查阅了 mariadb-java-client 官方文档后,发现能够在 JDBC URL 中指定网络IO 的超时参数,以下:
Parameter | Description |
---|---|
socketTimeout | Defined the network socket timeout (SO_TIMEOUT) in milliseconds. Value of 0 disables this timeout.Default: 0 (standard configuration) or 10000ms (using "aurora" failover configuration). since 1.1.7 |
如描述所说的,socketTimeout 能够设置 socket 的 SO_TIMEOUT 属性,从而达到控制超时时间的目的。默认是 0,即不超时。
咱们在 MySQL JDBC URL 中加入了相关的参数,以下:
spring.datasource.url=jdbc:mysql://10.0.71.13:33052/appdb?socketTimeout=60000&connectTimeout=30000&serverTimezone=UTC
此后对 MySQL 可靠性场景进行屡次验证,发现链接挂死的现象已经再也不出现,此时问题获得解决。
本次分享了一次关于 MySQL 链接挂死问题排查的心路历程,因为环境搭建的工做量巨大,并且该问题复现存在偶然性,整个分析过程仍是有些坎坷的(其中也踩了坑)。的确,咱们很容易被一些表面的现象所迷惑,而以为问题很难解决时,更容易带着偏向性思惟去处理问题。例如本例中曾一致认为链接池出现了问题,但实际上倒是因为 MySQL JDBC 驱动(mariadb driver)的一个不严谨的配置所致使。
从原则上讲,应该避免一切可能致使资源挂死的行为。若是咱们能在前期对代码及相关配置作好充分的排查工做,相信 996 就会离咱们愈来愈远。