Grails应用HikariCP连接超时(maxWait未生效)问题排查
2025-03-03 09:28:08
Grails 应用中 maxWait 配置未生效问题排查及解决
使用 JMeter 对 Grails 4.0.10 应用进行负载测试时, 大概5分钟后日志开始记录如下错误:
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
根据文档, Tomcat JDBC 连接池的默认 maxWait
是 30 秒 (30000 毫秒)。 尽管在 application.yml
或 DataSource.groovy
(如果是老版本 Grails) 中配置了 maxWait = 10000
, 但错误信息表明超时时间仍然是默认的 30000 毫秒,配置似乎没起作用。
这表示 maxWait
配置更改没有生效。我们希望增加 maxWait
,以便负载测试能够通过。
问题原因分析
这个问题通常由几个原因导致:
- 配置位置错误: Grails 有多个配置文件和配置方式, 配置可能放错了地方, 或者被其他地方的配置覆盖了。
- 配置属性名错误:
maxWait
属性名拼写错误、大小写错误, 或者使用了不同连接池的属性名。 - 连接池类型不匹配: 配置了 HikariCP 的属性, 但实际使用的不是 HikariCP, 或者反过来。
- 配置未被加载: 可能因为某些原因, 配置文件根本没有被正确加载。
- 旧的 Tomcat JDBC Pool 被使用 Grails 4 默认使用 HikariCP。 然而,老旧的 Tomcat JDBC pool 文档可能导致对属性使用的误解.
解决方案
下面是解决此问题的几种方法, 可以逐一尝试:
1. 确认 HikariCP 连接池被正确使用
首先,确认 Grails 4.0.10 应用确实在使用 HikariCP 连接池。从堆栈跟踪里已经看到 HikariPool-1
, 这表示 HikariCP 正在 被使用。 但保险起见, 仍可显式地确认:
- 检查依赖: 确认
build.gradle
中没有其他数据库连接池的依赖, 例如tomcat-jdbc
. 如果有, 移除它们。 - 不用在
build.gradle
中显式依赖 HikariCP。Grails 4 默认包含了它。
因为从错误信息 HikariPool-1 - Connection is not available, request timed out after 30000ms.
看, 当前使用的是 HikariCP, 可以跳过这一步.
2. 检查并统一配置位置 (推荐)
Grails 4 推荐使用 application.yml
进行配置。
- 使用
application.yml
: 将数据源配置移动到grails-app/conf/application.yml
文件中. 使用 YAML 格式:
dataSource:
pooled: true
dbCreate: none
url: "jdbc:mysql://localhost:3306/dev2?useUnicode=yes&characterEncoding=UTF-8"
driverClassName: "com.mysql.cj.jdbc.Driver"
dialect: org.hibernate.dialect.MySQL8Dialect
type: "com.zaxxer.hikari.HikariDataSource" #明确指定使用的连接池
properties:
jmxEnabled: true
initialSize: 5
maxActive: 50 #或者使用maximumPoolSize, 详见下方
minIdle: 5
maxIdle: 25 #maxIdle 在Hikari中已经不推荐
#maxWait: 10000 #这个也要改
connectionTimeout: 30000 #使用connectionTimeout
maxLifetime: 600000
idleTimeout: 60000 # 使用idleTimeout
validationQuery: "SELECT 1"
#validationQueryTimeout: 3 # HikariCP 没有这个属性
validationInterval: 15000 # 应该是minimumIdle的2倍以上
#testOnBorrow: true # HikariCP 性能考虑通常不用配置
#testWhileIdle: true # HikariCP 性能考虑通常不用配置
#testOnReturn: false # HikariCP 性能考虑通常不用配置
#jdbcInterceptors: "ConnectionState;StatementCache(max=200)" # 默认会有,如需更改可以配置
#defaultTransactionIsolation: java.sql.Connection.TRANSACTION_READ_COMMITTED # HikariCP默认值已经是READ_COMMITTED,不用配置.
- 移除
DataSource.groovy
: 如果你在grails-app/conf/DataSource.groovy
中也有数据源配置, 删掉它。 - 移除application.yml中一些对于Hikari无用的参数
3. 使用正确的 HikariCP 属性名
Tomcat JDBC Pool 和 HikariCP 的属性名不完全相同。
HikariCP 使用 connectionTimeout
代替 maxWait
来控制获取连接的超时时间. 将 maxWait
改为 connectionTimeout
:
dataSource:
# ... 其他配置 ...
properties:
# ... 其他配置 ...
connectionTimeout: 10000 # 单位: 毫秒
同样, HikariCP 不再建议使用maxIdle
, 建议使用 maximumPoolSize
同时控制最大连接数和最大空闲连接数。 如果想要配置, 尽量让它接近 maximumPoolSize
.
HikariCP 使用idleTimeout
代替minEvictableIdleTimeMillis
.
更推荐的配置示例:
dataSource:
pooled: true
dbCreate: "none"
url: "jdbc:mysql://localhost:3306/dev2?useUnicode=yes&characterEncoding=UTF-8"
driverClassName: "com.mysql.cj.jdbc.Driver"
# dialect: org.hibernate.dialect.MySQL8Dialect #通常无需配置. hibernate会自动选择
type: "com.zaxxer.hikari.HikariDataSource"
properties:
jmxEnabled: true
minimumIdle: 5 #最小空闲
maximumPoolSize: 50 #最大连接数
connectionTimeout: 10000 # 获取连接超时
idleTimeout: 30000 # 空闲连接超时 (超过此时间将被移除)
maxLifetime: 1800000 # 连接最大生命周期(建议小于数据库服务器的wait_timeout). 推荐为30分钟(1800000). 如果MySQL配置比较特殊可以继续保持原来的值.
connectionTestQuery: "SELECT 1" # 替代validationQuery. HikariCP用这个
HikariCP 的常用配置及最佳实践
minimumIdle
(对应旧版minIdle
): 最小空闲连接数。HikariCP 会尽量保持池中有这么多空闲连接。maximumPoolSize
(对应旧版maxActive
): 连接池允许的最大连接数(包括使用中的和空闲的)。这个值设置得太大,会给数据库带来过大的压力;设置得太小,又会限制应用的并发能力。connectionTimeout
: 等待从连接池获取连接的最大时间(毫秒)。如果超过这个时间还没获取到连接,就会抛出SQLException
。idleTimeout
: 连接在池中保持空闲状态的最长时间(毫秒). 仅当minimumIdle
小于maximumPoolSize
时生效。maxLifetime
: 连接在池中的最长生命周期(毫秒)。强烈建议设置这个值,并且应该比数据库服务器配置的任何连接时间限制短几秒钟。
如何确定 maximumPoolSize
:
根据经验公式,对于 OLTP 类型的应用(大部分 Web 应用都属于此类),maximumPoolSize
的值可以大致按照以下公式估算:
connections = ((core_count * 2) + effective_spindle_count)
例如,对于一个 4 核 CPU,1 个磁盘的服务器:
连接数 = ((4 * 2) + 1) = 9
当然, 这个公式不是绝对的,可以压测一下系统实际使用到的数据库并发, 并稍作调整。
4. 清理、重新编译和重新启动
有时, 缓存或其他原因可能导致配置没有立即生效。
-
清理项目:
./gradlew clean
-
重新编译:
./gradlew assemble
-
杀掉进程, 重新启动应用:
由于使用了nohup
, 首先确定你的 Grails 进程 ID:ps aux | grep RCRoadRaceWeb4
找到对应的进程ID后,杀掉它:
kill -9 <进程ID>
然后,使用原来指令重新启动:
bash nohup java -Dgrails.env=prod -Duser.timezone=US/Mountain -jar RCRoadRaceWeb4-0.1.jar &
5. 检查启动参数
检查启动应用的命令, 确保没有其他地方覆盖了数据源配置:
nohup java -Dgrails.env=prod -Duser.timezone=US/Mountain -jar RCRoadRaceWeb4-0.1.jar &
-D
参数可以用来设置 JVM 系统属性, 但这里看起来并没有覆盖数据源相关的配置。
6. 开启 HikariCP 日志 (调试用)
如果以上步骤都做了,问题依然存在, 可以打开 HikariCP 的日志记录,查看它在启动时的详细信息,这有助于判断是否正确加载了配置.
在 application.yml
中添加:
logging:
level:
com.zaxxer.hikari: DEBUG #开启Hikari的日志
com.zaxxer.hikari.HikariConfig: DEBUG # 可以看到HikariCP具体的加载的配置参数
org.hibernate.SQL: DEBUG # 可以看到执行的SQL语句
org.hibernate.type.descriptor.sql: TRACE #可以看到SQL参数绑定. 如果SQL很大,可能性能较差,仅调试时使用
重启应用, 查看日志输出. 特别关注 HikariCP 的初始化日志,看看它是否读取到了你设置的 connectionTimeout
等参数。
额外安全建议
- 避免使用 root 用户: 在生产环境中, 永远不要使用 root 用户连接数据库。应该为每个应用创建一个独立的 MySQL 用户, 并且只授予它必要的权限 (例如, 只能访问特定的数据库, 不能创建用户等)。
完成上述操作之后, 重启你的 Grails 应用程序并再次运行 JMeter 负载测试, 现在 connectionTimeout
配置应该生效了。