返回

Grails应用HikariCP连接超时(maxWait未生效)问题排查

mysql

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.ymlDataSource.groovy (如果是老版本 Grails) 中配置了 maxWait = 10000, 但错误信息表明超时时间仍然是默认的 30000 毫秒,配置似乎没起作用。

这表示 maxWait 配置更改没有生效。我们希望增加 maxWait,以便负载测试能够通过。

问题原因分析

这个问题通常由几个原因导致:

  1. 配置位置错误: Grails 有多个配置文件和配置方式, 配置可能放错了地方, 或者被其他地方的配置覆盖了。
  2. 配置属性名错误: maxWait 属性名拼写错误、大小写错误, 或者使用了不同连接池的属性名。
  3. 连接池类型不匹配: 配置了 HikariCP 的属性, 但实际使用的不是 HikariCP, 或者反过来。
  4. 配置未被加载: 可能因为某些原因, 配置文件根本没有被正确加载。
  5. 旧的 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. 清理、重新编译和重新启动

有时, 缓存或其他原因可能导致配置没有立即生效。

  1. 清理项目:

    ./gradlew clean
    
  2. 重新编译:

    ./gradlew assemble
    
  3. 杀掉进程, 重新启动应用:
    由于使用了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 配置应该生效了。