共计 16291 个字符,预计需要花费 41 分钟才能阅读完成。
摘要: 本次分享的是一次对于 MySQL 高可用问题的定位过程,其中波折颇多但问题自身却比拟有些代表性,遂将其记录以供参考。
一、背景
近期由测试反馈的问题有点多,其中对于系统可靠性测试提出的问题令人感到头疼,一来这类问题有时候属于“偶发”景象,难以在环境上疾速复现;二来则是可靠性问题的定位链条有时候变得很长,极其状况下可能要从 A 服务追踪到 Z 服务,或者是从利用代码追溯到硬件层面。
本次分享的是一次对于 MySQL 高可用问题的定位过程,其中波折颇多但问题自身却比拟有些代表性,遂将其记录以供参考。
架构
首先,本零碎以 MySQL 作为次要的数据存储部件。整一个是典型的微服务架构(SpringBoot + SpringCloud),长久层则采纳了如下几个组件:
- mybatis,实现 SQL <-> Method 的映射
- hikaricp,实现数据库连接池
- mariadb-java-client,实现 JDBC 驱动
在 MySQL 服务端局部,后端采纳了双主架构,前端以 keepalived 联合浮动 IP(VIP)做一层高可用。如下:
阐明
- MySQL 部署两台实例,设定为互为主备的关系。
- 为每台 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 任何配置上的故障。
而后在没有方法的状况下,从新测试了几次,问题又复现了。
紧接着,咱们提出了几个疑点:
1.Keepalived 会依据 MySQL 实例的可达性进行判断,会不会是健康检查出了问题?
但在本次测试场景中,MySQL 容器销毁会导致 keepalived 的端口探测产生失败,这同样会导致 keepalived 生效。如果 keepalived 也产生了停止,那么 VIP 应该能主动产生抢占。而通过比照两台虚拟机节点的信息后,发现 VIP 确实产生了切换。
2. 业务过程所在的容器是否产生了网络不可达的问题?
尝试进入容器,对以后产生切换后的浮动 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 数据库进行操作,步骤如下:
- 初始化数据源,此时连接池 min-idle 设置为 10;
- 每隔 50ms 执行一次 SQL 操作,查问以后的元数据表;
- 将 MySQL 服务进行一段时间,察看业务体现;
- 将 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 建设连贯的一个流程,其中波及到几个模块:
- HikariPool,连接池实例,由该对象连接的获取、开释以及连贯的保护。
- ConnectionBag,连贯对象容器,寄存以后的连贯对象列表,用于提供可用连贯。
- AddConnectionExecutor,增加连贯的执行器,命名如“HikariPool-1 connection adder”,是一个单线程的线程池。
- PoolEntryCreator,增加连贯的工作,实现创立连贯的具体逻辑。
- HouseKeeper,外部定时器,用于实现连贯的超时淘汰、连接池的补充等工作。
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 操作。而建链申请工作恰恰好采纳了单线程运作,进一步则导致了所有业务的阻塞。
三、解决方案
在理解了事件的前因后果之后,咱们次要思考从两方面进行优化:
- 优化一,减少 HirakiPool 中 AddConnectionExecutor 线程的数量,这样即便第一个线程呈现挂死,还有其余的线程能参加建链工作的调配。
- 优化二,出问题的 socketRead 是一种同步阻塞式的调用,可通过 SO_TIMEOUT 来防止长时间挂死。
对于优化点一,咱们统一认为用途并不大,如果连贯呈现了挂死那么相当于线程资源曾经泄露,对服务后续的稳固运行非常不利,而且 hikariCP 在这里也曾经将其写死了。因而要害的计划还是防止阻塞式的调用。
查阅了 mariadb-java-client 官网文档后,发现能够在 JDBC URL 中指定网络 IO 的超时参数,如下:
具体参考:https://mariadb.com/kb/en/about-mariadb-connector-j/
如形容所说的,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 就会离咱们越来越远。
本文分享自华为云社区《MySQL 连贯为什么挂死了?》,原文作者:TANG 卓章。
相干举荐
面试蚂蚁(P7)竟被 MySQL 难倒!
看完三件事❤️
=========
如果你感觉这篇内容对你还蛮有帮忙,我想邀请你帮我三个小忙:
点赞,转发,有你们的『点赞和评论』,才是我发明的能源。
关注公众号『Java 斗帝』,不定期分享原创常识。
同时能够期待后续文章 ing????