ElIauk

数据库连接池调优与故障

连接池不是越大越好 连接池最常见的误用是把它当成“并发上限调节旋钮”:接口慢了就把池子从 20 调到 100。结果往往是数据库 CPU 打满、锁等待变长、整体 QPS 反而下降。 原因很简单:一个数据库连接在后端对应一个线程和一份内存,数据库能真正并行执行的会话数是有限的,通常和核数同量级。池子开到

评论
数据库连接池调优与故障

连接池不是越大越好

连接池最常见的误用是把它当成“并发上限调节旋钮”:接口慢了就把池子从 20 调到 100。结果往往是数据库 CPU 打满、锁等待变长、整体 QPS 反而下降。

原因很简单:一个数据库连接在后端对应一个线程和一份内存,数据库能真正并行执行的会话数是有限的,通常和核数同量级。池子开到 200,200 个会话在数据库里排队抢 CPU 和 latch,应用的等待时间只会更长。连接池的作用是复用连接、限制并发,不是提升数据库的处理能力。

容量规划要按“全集群总量”算:实例数 × 每实例池大小。20 个实例每个配 30,就是 600 个连接,而 MySQL 的 max_connections 往往只有 500,第 501 个连接开始直接报 Too many connections,而且报错的通常是最后一个扩容的实例,看起来像“新机器有问题”。

HikariCP 的几个关键参数

参数 默认 建议与原因
maximumPoolSize 10 按数据库核数与实例数反推,不是越大越好
minimumIdle 等于最大池 保持固定大小,避免频繁建连与销毁
connectionTimeout 30s 改到 1–3s,快速失败优于长时间挂起
maxLifetime 30min 必须小于数据库 wait_timeout 和中间层空闲超时
leakDetectionThreshold 关闭 设成 20s 左右,帮助发现未归还的连接

connectionTimeout 是最值得改的一个。默认 30 秒意味着请求会卡在获取连接上,用户端早超时了,线程还占着不放,最终把 Tomcat 线程池也拖满。改成 2 秒后,快速失败至少能让上游熔断,问题不会层层放大。

maxLifetime 和 wait_timeout 的关系是“通信链路失败”的高发原因:MySQL 默认 8 小时断开空闲连接,中间还可能有负载均衡的 5 分钟空闲超时。连接池认为连接还活着,取出来用时才发现已被对端关闭,报错 Communications link failure。把 maxLifetime 设得比所有空闲超时都短,并打开 keepaliveTime,这类问题基本消失。

spring:
  datasource:
    url: jdbc:mysql://10.0.3.11:3306/shop?useSSL=false&serverTimezone=Asia/Shanghai
    username: shop_rw
    password: ${DB_PASSWORD}
    hikari:
      pool-name: order-db-pool
      maximum-pool-size: 16
      minimum-idle: 16
      connection-timeout: 2000
      idle-timeout: 300000
      max-lifetime: 1200000
      keepalive-time: 60000
      leak-detection-threshold: 20000

连接泄漏与获取超时

“获取连接超时”有两个完全不同的根因,处理方式相反:

连接泄漏:借了没还。典型来源是手写的 JDBC 代码忘了 close、Connection 被塞进静态变量、或者异常分支里没有 finally。现代框架基本都帮你关闭,但 @Transactional 方法里做长时间 HTTP 调用会“合法地”占着连接——事务不结束,连接就不归还。

连接不够用:池子确实小了,或者下游太慢。区别方法是看活跃连接数与获取等待时间:活跃数长期等于最大值且等待队列非空,是容量问题;活跃数不高但偶尔打满,更像泄漏或长事务。

// 错误:事务里做远程调用,连接被占住几秒
@Transactional
public void createOrder(OrderCommand command) {
    orderMapper.insert(command.toOrderDO());
    String token = remotePayClient.prepare();   // 网络调用可达数秒
    orderMapper.updateToken(command.getOrderId(), token);
}

// 正确:远程调用移到事务外,事务只包住数据库操作
public void createOrder(OrderCommand command) {
    String token = remotePayClient.prepare();
    orderTransactionalService.save(command, token);
}

另外,@Transactional 的自调用失效、嵌套事务传播级别设置不当,也会让连接持有时间远超预期。REQUIRES_NEW 在外层事务未结束时开新事务,会额外占用一个连接,高并发下很容易把池子翻倍消耗。

排查步骤

# 1. 当前池的实时状态:待获取、活跃、空闲
curl -s localhost:8080/actuator/metrics/hikaricp.connections.pending
curl -s localhost:8080/actuator/metrics/hikaricp.connections.active
curl -s localhost:8080/actuator/metrics/hikaricp.connections.acquire

# 2. 数据库侧的会话分布,按来源主机聚合
mysql -h 10.0.3.11 -e "
  SELECT SUBSTRING_INDEX(host, ':', 1) AS client, COUNT(*) AS conn
    FROM information_schema.processlist
   GROUP BY client ORDER BY conn DESC LIMIT 10;"

# 3. 看有没有长事务卡住连接
mysql -h 10.0.3.11 -e "
  SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS secs, trx_mysql_thread_id
    FROM information_schema.innodb_trx ORDER BY secs DESC LIMIT 10;"

第 2 步能直接定位“哪个实例占的连接最多”,第 3 步能看出连接是被事务卡住的。如果 innodb_trx 里有个事务跑了几百秒,先别调池子,去看那条 SQL。

再补一个必查项:连接池的指标要接入监控并配告警。hikaricp.connections.pending 持续大于 0、acquire 的 P99 超过 100ms,就是容量或慢 SQL 的早期信号,比等用户投诉要早得多。

常见坑

每个实例都按单机压测值配置。 单实例 30 个连接跑得很好,20 个实例就是 600,数据库直接拒绝。容量要按总量算。

池子大 + 慢 SQL 组合。 慢查询占住连接,池子越大堆积越多,最后数据库 CPU 100%,整个集群雪崩。

maxLifetime 大于 wait_timeout。 空闲一段时间后必然出现链路失效,重启后恢复,形成“每天早高峰第一波请求失败”的规律性故障。

读写分离共用一个池配置。 从库通常承载更多读请求,主库连接数应该更少更精,两者混在一个池里,主库被读流量拖慢。

忽略数据库的 max_connections 与代理层限制。 用了数据库中间件之后,真正的上限在中间件侧,应用的池大小之和超过它,一样会被拒。

没有 pool-name。 多数据源的应用里,日志和指标全是 HikariPool-1,出问题时根本分不清是哪个库。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者