PAYATHON 2026

Azure 中 Hikari 5.1.0 连接全部丢失且无法恢复

排障赵师傅

结论

total=0, active=0, idle=0 表示 Hikari 当时已经没有受管理的物理连接。这和业务线程占满连接池时的状态不同:

  • 连接池耗尽通常表现为 total=maximumPoolSize, active=maximumPoolSize, idle=0。
  • 日志中的 total=0 说明原有连接很可能已被判定为失效并移除,同时连接池没有成功创建新连接。
  • waiting=0 只是异常生成时的瞬时统计。请求超时后,当前线程可能已经从等待计数中移除,因此不能据此判断此前是否有线程等待。

Azure 故障转移、服务重配置或短暂的网络中断,都可能导致旧连接集中失效。这可以解释连接为何消失,却无法解释连接池为何迟迟没有补充连接。如果 total=0 持续较长时间,应重点排查新建物理连接是否反复失败,以及 Hikari 的连接创建线程是否卡在 JDBC 驱动的网络调用中。

把两个连接池从 150 调到 120,可以避免两个服务直接占满 300 个连接。不过,240 + 60 = 300 依然没有安全余量。socketTimeout=30000 也无法保证 Hikari 恢复连接。它主要控制已建立连接上的 socket I/O,既不限制业务线程获取池连接的等待时间,也不能替代登录超时。

为什么连接池会降到 0

以下情况会导致 Hikari 从池中移除连接:

  • JDBC 驱动报告连接已关闭或发生致命 SQL 异常。
  • 连接验证失败。
  • 连接达到 maxLifetime。
  • keepalive 检测发现连接不可用。
  • Azure 故障转移、网关重配置或网络设备重置连接。
  • 应用主动驱逐连接、关闭连接池或重新加载数据源。

正常情况下,Hikari 会继续创建连接,直到数量恢复至 minimumIdle。连接数长期停留在 0,通常有以下几种原因。

新连接无法建立

常见原因有:

  • Azure SQL 暂时不可用或正在进行故障转移。
  • 数据库已达到会话、资源或服务层限制。
  • DNS、路由、防火墙、SNAT 或 Azure 网络链路出现问题。
  • 凭据、访问令牌或认证服务异常。
  • JDBC 驱动建立连接时反复收到瞬态错误。

这时通常还能在日志中找到 Microsoft JDBC Driver 抛出的原始异常,例如 login timeout、TCP reset、connection refused、database unavailable,或 Azure SQL 的瞬态错误码。

建连线程卡住

connectionTimeout=30000 限制的是业务线程等待 Hikari 返回连接的时间,不限制 JDBC 驱动建立物理连接所需的时间。

如果没有为驱动的登录或网络操作设置明确的超时,一次 getConnection() 就可能阻塞很久,导致 Hikari 的连接补充任务无法继续。业务线程则会每隔 30 秒收到一次:

Connection is not available, request timed out after 30000ms

这种情况无法通过调整 connectionTimeout 解决。需要为 JDBC 驱动配置 loginTimeout,并在故障发生时抓取线程转储。

连接创建持续失败

Hikari 会反复尝试补充连接。如果数据库一直拒绝连接,连接总数仍可能长时间保持为 0。ENV2 曾在两分钟后恢复,只能说明当时后续的建连请求成功了,不能证明 ENV2 有不同的恢复机制。之后 ENV2 也出现无法恢复的情况,进一步支持了这一判断。

当前配置中的风险点

minimumIdle 与 maximumPoolSize

实验配置为:

maximumPoolSize=1000
minimumIdle=1000

这会让连接池尝试维持大量连接,而数据库上限只有 300。即使 Hikari 或驱动会校正这组配置,也不应在该环境中继续使用这一实验值。

如果正常配置同时设置了 maximumPoolSize=120 和 minimumIdle=120,连接池实际上是固定大小的。两个 server 进程启动后,会尝试长期维持约 240 个连接。

计算 client.jar 的连接需求时,还要乘以实际进程数。假设同时运行两个 client.jar,每个进程最多使用 60 个连接,总需求可能达到:

2 × 120 + 2 × 60 = 360

数据库的 300 个连接也不能全部分配给这些应用。运维工具、部署重叠、其他服务,以及故障恢复期间的新旧会话都需要预留容量。

maxLifetime=300000

300000 毫秒等于 5 分钟。如果基础设施没有明确要求连接必须在 5 分钟内退出,这个值通常过于激进。

较短的生命周期会导致大量连接频繁轮换。一个包含 120 个连接的固定池,在 5 分钟生命周期下需要持续关闭并重建连接。两个服务同时运行时,建连和认证压力会明显增加。遇到网络抖动时,这种配置也更容易引发集中重连。

keepaliveTime=30000

30 秒的 keepalive 会频繁检测大量空闲连接。它能发现已经断开的连接,但无法解决数据库不可达、登录超时或建连线程阻塞。

对于 240 个长期连接,这还会带来额外的周期性流量。具体数值应根据 Azure SQL、网络设备的空闲连接策略和实际故障周期来设置,不宜一味缩短。

socketTimeout=30000

Microsoft JDBC Driver 的 socketTimeout 通常以毫秒为单位,用来控制 socket 读写的等待时间。设置为 30 秒会带来两方面影响:

  • 死连接可以更快报错,并被 Hikari 移除。
  • 正常但执行时间超过 30 秒的数据库操作,也可能被驱动中断。

socketTimeout 不会直接加快连接池的重建速度,也不负责控制登录超时。如果不设置 socketTimeout,部分网络操作可能等待很久;如果设置得太短,慢查询或数据库压力又可能导致连接集中失败。

ENV1 和 ENV2 的配置差异可能影响故障发生的时间和具体表现,但现有信息不足以认定 socketTimeout 是根因。

建议的配置方向

以下数值只能作为排查起点,并非适用于所有环境的最佳配置。最终的连接池大小应结合数据库服务层、查询耗时、事务时长和实测吞吐量确定。

SQLServerDataSource dataSource = new SQLServerDataSource();
dataSource.setServerName("example.database.windows.net");
dataSource.setPortNumber(1433);
dataSource.setDatabaseName("example");
dataSource.setUser("user");
dataSource.setPassword("password");

// 单位分别以当前 Microsoft JDBC Driver API 定义为准。
dataSource.setLoginTimeout(15);
dataSource.setSocketTimeout(60000);

// 驱动版本支持这些属性时再启用。
dataSource.setConnectRetryCount(3);
dataSource.setConnectRetryInterval(10);

HikariConfig config = new HikariConfig();
config.setDataSource(dataSource);

config.setMaximumPoolSize(40);
config.setMinimumIdle(10);
config.setConnectionTimeout(30000);
config.setValidationTimeout(5000);
config.setIdleTimeout(600000);
config.setKeepaliveTime(120000);
config.setMaxLifetime(1800000);

HikariDataSource hikariDataSource = new HikariDataSource(config);

配置时需要注意以下几点:

  1. 为 JDBC 登录过程设置有限的超时时间,防止连接创建任务无限阻塞。
  2. 为数据库的 300 个连接名额留出余量。
  3. 避免使用 5 分钟的 maxLifetime,以免连接频繁轮换。
  4. 将 keepaliveTime 设置为小于 maxLifetime,同时避免检测过于频繁。
  5. 确保 socketTimeout 大于正常业务允许的最长数据库操作时间。
  6. 针对 Azure SQL 的瞬态故障,在业务层进行有限次数且带退避的重试。

如果需要固定大小的连接池,可以将 minimumIdle 设为与 maximumPoolSize 相同,但总连接数仍要留出足够余量。如果 client.jar 只偶尔执行查询,使用较小的连接池更合适,无需预留 60 个常驻连接。

应用层还需要重试

连接池可以重新创建连接,却无法安全重放已经失败的事务。Azure 故障转移期间,即使 Hikari 很快获取了新连接,原事务仍可能失败。

只应重试能够安全重试的操作。每次尝试都要重新获取连接并重新开始事务:

for (int attempt = 1; attempt <= 3; attempt++) {
    try (Connection connection = dataSource.getConnection()) {
        try {
            connection.setAutoCommit(false);

            executeDatabaseWork(connection);

            connection.commit();
            break;
        } catch (SQLException e) {
            connection.rollback();

            if (!isTransient(e) || attempt == 3) {
                throw e;
            }
        }
    }

    Thread.sleep(attempt * 1000L);
}

重试写操作前必须确认其幂等性。连接断开时,客户端有时无法判断服务器是否已经提交事务,直接重试可能产生重复数据。

当前配置使用 autoCommit=false,因此每条执行路径都要明确调用 commit() 或 rollback(),并及时关闭 Connection。普通的连接泄漏通常表现为 active 达到上限,不会让 total 降到 0。不过,事务长期占用连接仍会加重数据库压力。

推荐的诊断顺序

1. 保留 Hikari 和 JDBC 驱动的完整异常

不要只记录最外层的 SQLTransientConnectionException。日志中应包含完整的异常链、SQLState、错误码和 cause:

catch (SQLException e) {
    for (SQLException current = e; current != null;
         current = current.getNextException()) {
        logger.error(
            "SQLState={}, errorCode={}, message={}",
            current.getSQLState(),
            current.getErrorCode(),
            current.getMessage(),
            current
        );
    }
}

同时临时启用 com.zaxxer.hikari 的 DEBUG 日志,重点检查:

  • Failed to create/setup connection
  • Connection ... marked as broken
  • Closing connection
  • Pool stats
  • Thread starvation or clock leap detected
  • 数据源或连接池是否被关闭

2. 在 total=0 时立即抓取线程转储

连续抓取两到三份线程转储,每份间隔几秒,检查 Hikari connection adder、housekeeper 和 Microsoft JDBC Driver 线程。

如果连接创建线程始终停在 socket connect、socket read、TLS 握手或登录响应处,就能说明连接池为何没有补充连接。如果 housekeeper 已停止运行,则需继续排查线程饥饿、进程暂停、严重 GC 或连接池生命周期管理问题。

3. 对照同一时段的 Azure 事件

检查故障发生前后的:

  • Azure SQL Resource Health 和 Service Health。
  • 数据库连接数、CPU、worker、session、DTU 或 vCore 使用率。
  • 登录失败、限流、故障转移和瞬态错误。
  • 应用所在网络的 SNAT、DNS、防火墙和连接重置记录。

Azure 的动态重配置确实可能中断连接,但需要用同一时间窗口内的 Azure 事件或 JDBC 原始错误加以确认。仅凭“Azure 外没有复现”无法锁定根因。

4. 监控连接池状态变化

通过 Micrometer、JMX 或 Hikari metrics 持续记录:

  • TotalConnections
  • ActiveConnections
  • IdleConnections
  • PendingThreads
  • connection acquire time
  • connection creation failure
  • 数据库实际 session 数

监控时应记录连接池从正常状态降到 0 的完整过程,不能只看超时发生后的单条日志。

关于缩减连接池的最终判断

将每个 server 的连接池从 150 调到 120,可以降低达到数据库连接上限的概率。不过,这无法解决所有连接失效后连接池不能重建的问题,而且 240 个常驻连接很可能仍然偏多。

应用线程多,不代表需要同样多的数据库连接。数据库能够有效并行处理的请求通常远少于应用线程数。连接过多还会增加锁竞争、上下文切换、内存消耗,以及故障后的重连压力。

应先通过实际指标确认并发数据库操作数和连接持有时间,再决定每个实例的连接池大小。同时要为 client.jar、部署重叠、管理连接和故障恢复预留容量。如果连接池长期满载,还应检查慢查询、长事务和连接持有范围,继续扩大连接池通常解决不了这些问题。

备注:内容仅供参考。