展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 业界资讯 > MySQL连接离奇挂死,竟不是连接池的锅……

MySQL连接离奇挂死,竟不是连接池的锅……

一、背景近期由测试反馈的问题有点多,其中关于系统可靠性测试提出的问题令人感到头疼,一来这类问题有时候属于“偶发”现象,难以在环境上快速复现;二来则是可靠性问题的定位链条有时候变得很长,极端情况下可能要从A服务追踪到Z服务,或者是从应用代码追溯到硬件层面。本次分享的是一次关于MySQL高可用问题的定位过程,其中曲折颇多但问题本身却比较有些代表性,遂将其记录以供参考。1、架构首先,本系统以MySQL作为主要的数据存储部件。整一个是典型的微服务架构(SpringBoot+SpringClou

本文目录

    一、背景

    近期由测试反馈的问题有点多,其中关于系统可靠性测试提出的问题令人感到头疼,一来这类问题有时候属于“偶发”现象,难以在环境上快速复现;二来则是可靠性问题的定位链条有时候变得很长,极端情况下可能要从 A 服务追踪到 Z 服务,或者是从应用代码追溯到硬件层面。

    本次分享的是一次关于 MySQL 高可用问题的定位过程,其中曲折颇多但问题本身却比较有些代表性,遂将其记录以供参考。

    1、架构

    首先,本系统以 MySQL 作为主要的数据存储部件。整一个是典型的微服务架构(SpringBoot + SpringCloud),持久层则采用了如下几个组件:

    • mybatis,实现 SQL <-> Method 的映射
    • hikaricp,实现数据库连接池
    • mariadb-java-client,实现 JDBC 驱动

    在 MySQL 服务端部分,后端采用了双主架构,前端以 keepalived 结合浮动IP(VIP)做一层高可用。如下:

    MySQL连接离奇挂死,竟不是连接池的锅……

    • MySQL 部署两台实例,设定为互为主备的关系。
    • 为每台 MySQL 实例部署一个 keepalived 进程,由 keepalived 提供 VIP 高可用的故障切换。
    • 实际上,keepalived 和 MySQL 都实现了容器化,而 VIP 端口则映射到 VM 上的 nodePort 服务端口上。
    • 业务服务一律使用 VIP 进行数据库访问。

    Keepalived 是基于 VRRP 协议实现了路由层转换的,在同一时刻,VIP 只会指向其中的一个虚拟机(master)。

    当主节点发生故障时,其他的 keepalived 会检测到问题并重新选举出新的 master,此后 VIP 将切换到另一个可用的 MySQL 实例节点上。

    这样一来,MySQL 数据库就拥有了基础的高可用能力。

    另外一点,Keepalived 还会对 MySQL 实例进行定时的健康检查,一旦发现 MySQL 实例不可用会将自身进程杀死,进而再触发 VIP 的切换动作。

    2、问题现象

    本次的测试用例也是基于虚拟机故障的场景来设计的:

    持续以较小的压力向业务服务发起访问,随后将其中一台 MySQL 的容器实例(master)重启。

    按照原有的评估,业务可能会产生很小的抖动,但其中断时间应该保持在秒级。

    然而经过多次的测试后发现,在重启 MySQL 主节点容器之后,有一定的概率会出现业务却再也无法访问的情况!

    二、分析过程

    在发生问题之后,开发同学的第一反应是 MySQL 的高可用机制出了问题。由于此前曾经出现过由于 keepalived 配置不当导致 VIP 未能及时切换的问题,因此对其已经有所戒备。

    先是经过一通的排查,然后并没有找到 keepalived 任何配置上的毛病。

    然后在没有办法的情况下,重新测试了几次,问题又复现了。

    紧接着,我们提出了几个疑点:

    1)Keepalived 会根据 MySQL 实例的可达性进行判断,会不会是健康检查出了问题?

    但在本次测试场景中,MySQL 容器销毁会导致 keepalived 的端口探测产生失败,这同样会导致 keepalived 失效。如果 keepalived 也发生了中止,那么 VIP 应该能自动发生抢占。而通过对比两台虚拟机节点的信息后,发现 VIP 的确发生了切换。

    2)业务进程所在的容器是否发生了网络不可达的问题?

    尝试进入容器,对当前发生切换后的浮动IP、端口执行 telnet 测试,发现仍然能访问成功。

    1、连接池排查

    在排查前面两个疑点之后,我们只能将目光转向了业务服务的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!/:?]

    到这里,我们已经将应用获得连接的代码大致梳理了一遍,整个过程如下图所示:

    MySQL连接离奇挂死,竟不是连接池的锅……

    从执行逻辑上看,连接池的处理并没有问题,相反其在许多细节上都考虑到位了。在对非存活连接执行 close 时,同样调用了 removeFromBag 动作将其从连接池中移除,因此也不应该存在僵尸连接对象的问题。

    那么,我们之前的推测应该就是错误的!

    2、陷入焦灼

    在代码分析之余,开发同学也注意到当前使用的 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 版本问题的设想也再次落空,研发同学再次陷入焦灼。

    3、拨开云雾见光明

    多方面求证无果之后,我们最终尝试在业务服务所在的容器内进行抓包,看是否能发现一些蛛丝马迹。

    进入故障容器,执行 tcpdump -i eth0 tcp port 30052 进行抓包,然后对业务接口发起访问。

    此时令人诡异的事情发生了,没有任何网络包产生!而业务日志在 30s 之后也出现了获取连接失败的异常。

    我们通过 netstat 命令检查网络连接,发现只有一个 ESTABLISHED 状态的 TCP 连接。

    MySQL连接离奇挂死,竟不是连接池的锅……

    也就是说,当前业务实例和 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.(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连接离奇挂死,竟不是连接池的锅……

    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 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连接离奇挂死,竟不是连接池的锅……


    好了,让我们在回顾一下前面关于可靠性测试的场景:

    首先,MySQL 主实例发生故障,而紧接着 hikariCP 则检测到了坏的连接(connection is dead)并将其释放,在释放关闭连接的同时又发现连接数需要补充,进而立即触发了新的建链请求。而问题就刚好出在这一次建链请求上,TCP 握手的部分是成功了(客户端和 MySQL VM 上 nodePort 的完成连接),但在接下来由于当前的 MySQL 容器已经停止(此时 VIP 也切换到了另一台 MySQL 实例上),因此客户端再也无法获得原 MySQL 实例的握手包响应(该握手属于MySQL应用层的协议),此时便陷入了长时间的阻塞式 socketRead 操作。而建链请求任务恰恰好采用了单线程运作,进一步则导致了所有业务的阻塞。

    三、解决方案

    在了解了事情的来龙去脉之后,我们主要考虑从两方面进行优化:

    1)优化一,增加 HirakiPool 中 AddConnectionExecutor 线程的数量,这样即使第一个线程出现挂死,还有其他的线程能参与建链任务的分配。

    2)优化二,出问题的 socketRead 是一种同步阻塞式的调用,可通过 SO_TIMEOUT 来避免长时间挂死。

    对于优化点一,我们一致认为用处并不大,如果连接出现了挂死那么相当于线程资源已经泄露,对服务后续的稳定运行十分不利,而且 hikariCP 在这里也已经将其写死了。因此关键的方案还是避免阻塞式的调用。

    查阅了 mariadb-java-client 官方文档后,发现可以在 JDBC URL 中指定网络IO 的超时参数,如下:

    MySQL连接离奇挂死,竟不是连接池的锅……

    如描述所说的,socketTimeout 可以设置 socket 的 SO_TIMEOUT 属性,从而达到控制超时时间的目的。默认是 0,即不超时。

    我们在 MySQL JDBC URL 中加入了相关的参数,如下:

    spring.datasource.url=jdbc:mysql://10.0.71.13:33052/appdb?socketTimeout=60000&cnotallow=30000&serverTimeznotallow=UTC

    此后对 MySQL 可靠性场景进行多次验证,发现连接挂死的现象已经不再出现,此时问题得到解决。

    四、小结

    本次分享了一次关于 MySQL 连接挂死问题排查的心路历程,由于环境搭建的工作量巨大,而且该问题复现存在偶然性,整个分析过程还是有些坎坷的(其中也踩了坑)。

    的确,我们很容易被一些表面的现象所迷惑,而觉得问题很难解决时,更容易带着偏向性思维去处理问题。

    例如本例中曾一致认为连接池出现了问题,但实际上却是由于 MySQL JDBC 驱动(mariadb driver)的一个不严谨的配置所导致。

    从原则上讲,应该避免一切可能导致资源挂死的行为。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    业界资讯 MySQL
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    IntelliJ IDEA macOS版
    IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    PyCharm macOS版
    PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    WebStorm macOS版
    WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    After Effects macOS版
    After Effects macOS版

    Adobe After Effects 是面向动态图形、影视特效与视频合成的专业创作软件。它提供关键帧动画、遮罩抠像、运动跟踪、三维空间、文字动画和表达式等工具,并可与 Premiere Pro、Photoshop、Illustrator

    Audition macOS版
    Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Bridge macOS版
    Bridge macOS版

    Adobe Bridge 是面向摄影师、设计师及内容团队的免费数字资产管理工具,可集中预览照片、视频和 Adobe 项目文件,并利用评级、标签、关键词、元数据、筛选与收藏集完成分类检索。它还支持批量重命名、格式导出、相机导入及 Camera

    Illustrator macOS版
    Illustrator macOS版

    Adobe Illustrator 是 Adobe 推出的专业矢量图形设计软件,适合在 Mac 上制作标志、图标、插画、包装、信息图及印刷版面。它以路径和锚点构建可无损缩放的图形,并提供钢笔、形状、文字、图像描摹、渐变、画板及生成式功能。

    InDesign macOS版
    InDesign macOS版

    Adobe InDesign 是面向印刷品与数字出版物的专业版面设计软件,可在 Mac 上制作书籍、杂志、宣传册、海报、报告及交互式文档。它提供精细的文字样式、网格、母版、长文档管理和印前输出能力。

    Lightroom Classic macOS版
    Lightroom Classic macOS版

    Adobe Lightroom Classic 是面向桌面摄影工作流的照片管理与后期处理软件,可将原始照片保存在本地硬盘,通过目录、关键词、评分和收藏夹高效整理图库,并提供 RAW 冲印、蒙版、镜头校正、批量同步及多格式导出等功能。

    Media Encoder macOS版
    Media Encoder macOS版

    Adobe Media Encoder 是 Adobe 推出的专业音视频编码与转码工具,可通过队列、预设和监视文件夹批量完成格式转换、代理文件创建及多平台成片输出,并与 Premiere Pro、After Effects 等软件紧密协作。

    Photoshop macOS版
    Photoshop macOS版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理软件,提供图层、蒙版、选区、修复、调色、文字排版、智能对象及生成式编辑等能力。Mac 版支持 Intel 与 Apple Silicon 处理器。

    Premiere Pro macOS版
    Premiere Pro macOS版

    Adobe Premiere Pro 是面向专业视频制作的非线性剪辑软件,提供多轨时间线、调色、音频处理、字幕、代理工作流和多格式输出能力,并可与 After Effects、Audition、Photoshop 及 Frame.io 协同

    WINDOWS 更多
    IntelliJ IDEA Windows版
    IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    PhpStorm Windows版
    PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    PyCharm Windows版
    PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    WebStorm Windows版
    WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    Photoshop Windows版
    Photoshop Windows版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许

    InDesign Windows版
    InDesign Windows版

    Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator

    Illustrator Windows版
    Illustrator Windows版

    Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。

    Bridge Windows版
    Bridge Windows版

    Adobe Bridge 是 Adobe 推出的创意资产管理工具,可集中预览、分类和查找照片、视频、PDF及多种设计文件,并支持元数据、关键词、评级、批量重命名和格式导出。它可与 Photoshop、Illustrator 等应用衔接,

    Lightroom Classic Windows版
    Lightroom Classic Windows版

    Adobe Lightroom Classic 是面向 Windows 桌面摄影工作流的照片管理与无损编辑工具,可通过目录集中整理大型图库,并完成 RAW 显影、局部蒙版、批量调整、打印和多格式导出。它强调本地文件管理与精细控制,

    Acrobat Pro Windows版
    Acrobat Pro Windows版

    Adobe Acrobat Pro 是面向专业 PDF 工作的综合解决方案,可在 Windows 中创建、编辑、转换、比较、整理和保护文档,并支持表单、电子签名、扫描件文字识别及敏感信息永久遮盖。订阅还包含网页、移动服务和云端协作能力,

    After Effects Windows版
    After Effects Windows版

    Adobe After Effects 是面向动态图形、动画与影视视觉特效制作的专业工具,可完成关键帧动画、图层合成、遮罩跟踪、抠像、文字动画和二维三维元素整合。Windows 版通过 Adobe Creative Cloud 安装及更新,

    Media Encoder Windows版
    Media Encoder Windows版

    Adobe Media Encoder 是 Adobe 面向视频与音频制作流程提供的专业编码和转码工具,可批量处理媒体文件、创建代理、套用输出预设并监控文件夹自动执行任务。它与 Premiere、After Effects、Audition

    网站备案号:苏ICP备2026018738号-1 联系邮箱:bd@zhengruan.com

    Copyright ©2018-2026