后端开发:数据库连接池配置踩坑实录——HikariCP、Druid、Go database/sql 深度对比与调优参数

🔑 关键词:后端开发,数据库连接池,HikariCP,Druid,Go database/sql

📖 摘要:线上服务突发雪崩,线程全部 WAITING,CPU 却只有 15%。排查 6 小时发现是 HikariCP 连接池配置不当。本文对比 HikariCP、Druid、Go database/sql 的默认参数和底层设计,给出 3 个压测验证的调优公式,以及一个反直觉结论:连接池越小,系统可能越稳。

后端开发:数据库连接池配置踩坑实录

图片

去年双十一前,我们订单服务突然大量超时。监控上 QPS 从 8000 掉到 200,但 CPU 使用率只有 15%,GC 也正常。我登上机器 jstack 一看,200 个 Tomcat 线程全部卡在 HikariPool.getConnection 上。当时我就懵了——连接池最大连接数明明设了 50,数据库 PostgreSQL 的 max_connections 是 500,怎么会拿不到连接?后来翻了 HikariCP 的源码才发现,connectionTimeout 默认 30 秒,而我们的慢 SQL 平均要跑 3 秒,50 个连接被 200 个线程抢,排队时间直接爆炸。更坑的是,Druid 在这种情况下会抛 GetConnectionTimeoutException,而 HikariCP 会一直等,直到超时。那次事故后,我花了整整一周,把 HikariCP、Druid、Go 的 database/sql 的源码和文档全撸了一遍,还做了 2000 万次请求的压测。

图片

先看 HikariCP 的默认值:maximumPoolSize=10minimumIdle=10connectionTimeout=30000msidleTimeout=600000msmaxLifetime=1800000ms。注意 maximumPoolSize 默认只有 10,不是 50 也不是 100。HikariCP 官方文档里有个公式:connections = ((core_count * 2) + effective_spindle_count)。我拿一台 4 核 8G 的机器算,4*2+1=9,所以默认 10 是合理的。Druid 的默认值不一样:initialSize=0minIdle=0maxActive=8maxWait=-1(无限等待)。Druid 的 maxWait=-1 是个大坑,它会让你线程永远挂起,而不是快速失败。我们当时从 Druid 迁到 HikariCP,就是因为这个。但 HikariCP 也不是没毛病,它的 maxLifetime 默认 30 分钟,而 MySQL 的 wait_timeout 默认 8 小时,所以一般没问题;但如果你用云数据库,比如 AWS RDS 的 wait_timeout 可能是 300 秒,那 HikariCP 的 maxLifetime 必须小于这个值,否则会拿到已被服务端关闭的连接,报 Communications link failure

图片

Go 的 database/sql 是标准库,参数更少:SetMaxOpenConns 默认 0(无限制),SetMaxIdleConns 默认 2,SetConnMaxLifetime 默认 0(永不过期)。听起来很美好?但无限制的 MaxOpenConns 在流量突增时会打爆数据库。我压测过:用 Go 写一个简单的 HTTP 服务,每个请求查一次 PostgreSQL,当并发从 100 涨到 5000 时,MaxOpenConns=0 的情况下,PostgreSQL 的 max_connections 直接被打满,报 FATAL: sorry, too many clients already。后来我把 MaxOpenConns 设成 100,MaxIdleConns 设成 100,ConnMaxLifetime 设成 5 分钟,QPS 反而比无限制时高了 18%,因为避免了频繁建立连接的开销。这里有个反直觉的点:很多人以为连接池越大吞吐越高,但实际在 PostgreSQL 上,连接数超过 CPU 核数的 2 倍后,上下文切换和锁竞争会让吞吐下降。我用 sysbench 测过,4 核机器上,连接数从 10 涨到 200,TPS 从 12000 降到 9000。

图片

基于压测,我总结了一个 HikariCP 的配置模板(适用于 4 核 8G,PostgreSQL 14,SSD):

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 3000
      idle-timeout: 300000
      max-lifetime: 1200000
      validation-timeout: 3000
      keepalive-time: 300000

注意 connection-timeout 从 30000 降到 3000,让拿不到连接的请求快速失败,而不是排队等 30 秒。max-lifetime 设 20 分钟,小于云数据库的 30 分钟 wait_timeout。另外,keepalive-time 是 HikariCP 3.4.0 引入的,每 5 分钟发一次心跳,防止连接被防火墙切断。Druid 的话,我建议 maxActive=20maxWait=3000validationQuery=SELECT 1testWhileIdle=truetimeBetweenEvictionRunsMillis=60000。Go 的话,db.SetMaxOpenConns(20)db.SetMaxIdleConns(10)db.SetConnMaxLifetime(20*time.Minute)db.SetConnMaxIdleTime(5*time.Minute)。这些数字不是拍脑袋,是我在 2000 万请求压测下,P99 延迟从 1.2s 降到 180ms 的结果。

图片

说一个可能被喷的观点:大部分业务系统根本不需要连接池。如果你的 QPS 低于 500,直接用短连接,每个请求新建一个数据库连接,性能损失不到 5%,但省去了所有连接池的调优和故障排查。PostgreSQL 建立连接的开销大约是 1.5ms(本地),MySQL 是 0.8ms,而一个慢查询动辄 10ms 以上。连接池真正有价值的地方是长事务、高并发、或者数据库连接建立成本极高的场景(比如跨可用区)。我见过太多团队,为了“性能”把 HikariCP 的 maximumPoolSize 设成 200,结果数据库先挂了。所以,下次你调连接池参数时,先问自己:我的瓶颈真的是连接数吗?还是慢 SQL?还是锁?用 pg_stat_activity 或者 SHOW PROCESSLIST 看一眼,比盲目调参有用得多。

图片

🏷️ 标签: