【分析】Spring Actuator + Druid数据库连接池耗尽问题
参考来源: https://github.com/alibaba/druid/issues/3059
问题环境
- spring boot actuator : 2.6.2系列(可能支持actuator的2.x都会引发该问题)
- druid:1.1.20(应该也不限制版本)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>runtime</scope>
</dependency>
</dependencies>
- 自定义DataSource数据源
spring:
datasource:
name: druidDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: Dongle@123
url: jdbc:mysql://db.dongle.com:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
management:
endpoints:
web:
exposure:
include: "*"
server:
ssl:
enabled: false
base-path: /
@Configuration
public class DruidDataSouceConfig {
@Bean("druidDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource setDataSource(){
return new DruidDataSource();
//return
}
@Bean(name = "resourceProcessTransactionManager")
public DataSourceTransactionManager setTransactionManager(@Qualifier("druidDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
问题一:访问actuator/configprops陷入死循环
Spring Actuator检测服务健康状态,当使用使用Druid管理数据库链接池时,若自定义DataSource源,访问/actuator/configprops接口时会导致数据库链接占满且服务死循环问题。
问题原因
主要问题是Spring Actuator输出配置信息时,会对DataSource做序列化处理,DruidDataSource有一个getConnection方法属性,会获得DruidPooledConnection,然后新获得的JDBCConnection继续获取DataSource,循环往复,导致数据库连接池被沾满,
问题分析
断点运行(完整调用逻辑请参考下方扩展:actuator/configprops调用流程)
从上图看结果就是DruidDataSource -> DruidPooledConnection -> DruidConnectionHolder -> DruidDataSource循环,属性嵌套属性,导致一直循环读取属性方法。
属性嵌套预览
1.DruidDataSource获取DruidPooledConnection
2.DruidPooledConnection存在DruidConnectionHolder属性
3.DruidConnectionHolder存在DruidAbstractDataSource属性
4.DruidDataSource 继承 DruidAbstractDataSource
问题二:数据连接池资源耗尽
问题分析
上面得到结果会永远死循环,DruidDataSource也会一直调用getConnection方法获取DruidPooledConnection ,由于死循环存在,数据库链接池肯定会被占满,并且死循环跳不出去,链接就不会被释放,导致当链接满了之后,下一次循环就会等待,
若自定义DataSource未定义maxWait最大等待时长,就会调用takeLast方法一直等待新连接释放
解决方案
一、禁用actuator/configprops接口
management:
endpoints:
web:
exposure:
exclude: "configprops"
二、使用DruidDataSourceBuilder创建DataSource源
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
@Bean("druidDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource setDataSource(){
//return new DruidDataSource();
// com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
return DruidDataSourceBuilder.create().build();
// 建议配置maxWait最大等待时长,否则会服务卡住
// DruidDataSourceWrapper dataSource = (DruidDataSourceWrapper) DruidDataSourceBuilder.create().build();
// dataSource.setMaxWait(3000);
}
三、使用Spring原生配置创建DataSource源
当系统没有多数据源时,可以保留Spring原生DataSource创建方式,不需要自定义数据源
spring:
datasource:
name: druidDataSource
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
username: root
password: Dongle@123
url: jdbc:mysql://db.dongle.com:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false
扩展:actuator/configprops调用流程
提示:后续逻辑请参照上面问题分析
总结
Druid本身个人认为应该是没有问题,毕竟也是一个成熟产品,只是原生Druid和Spring Actuator组合不友好,所有Druid也专门开发了druid-spring用于支持Spring框架。
技术只有在适宜场景使用才能达到最佳效果,不是说一定适用任何场景。保持学习,不要盲目使用是作为一个技术人员应该掌握的技术。