问题
最近线上忽然频繁多个系统爆出同一个错误:
HikariPool-1 - Connection is not available, request timed out after 30008ms
经过测试同事和运维的反馈,这个问题重启后消失,但是在运行一段时间后会再次爆出,这段时间大概为一个月左右。
分析
很明显,上述问题是数据库连接问题,而且是Hikari的数据库连接池给出的”数据库连接不可用,请求超时“。
上面的实施描述中有几个关键线索:
- 最近: 意味着之前是没有的,最近才产生。最近改了什么?
- **多个系统:**意味着很可能是共用的底层依赖jar包,或者是公共服务,例如mysql引起的。
- 重启后有一个月左右稳定期: 没有立即暴雷,而是运行了一段时间,很可能是积累的。
经过确认,排除以下可能:
- 运维最近给很多系统改小了连接池配置,给程序穿了小鞋。
- 最近并发量呈现出阶段式稳步递增的现象,而且递增到突破了系统承受范围(Hikari配置的连接数上限,或者mysql配置的连接上线)。
那么结论很明显: 存在数据库连接池连接泄露!
我们知道,常规使用ORM框架,hibernate或mybatis都无需关注连接的关闭,即使使用spring 的JdbcTemplate,也无需处理连接关闭,因为其内部会自动关闭数据库连接,释放连接资源。
因此,最大可能是手动创建了数据库连接没有关闭,或者手动从JdbcTemplate里获取了数据库连接,执行了操作,但是没有关闭。
结合上面提到的几个关键线索,最后定位一个底层的基础包:log-spring-boot-starter
,里面有段代码是这样写的:
@Override
public void afterPropertiesSet() throws Exception {
checkTableNextMonth();
}
public void checkTableNextMonth(){
ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(3);
scheduledService.scheduleAtFixedRate(()->{
LocalDate now = LocalDate.now();
String querySql ="select count(*) from information_schema.tables where table_name = '%s%s' and table_schema = '%s' ";
String yearMonth =now.with(TemporalAdjusters.firstDayOfNextMonth()).format(DateTimeFormatter.ofPattern(YM));
String fullSql = String.format(querySql,initializer.TABLE_NAME,yearMonth,getSchemaName());
Long count = jdbcTemplate.queryForObject(fullSql,new HashMap<>(),Long.class);
if(count!=null && count==0){
tableExist= false;
createTable(initializer.TABLE_NAME,yearMonth);
return;
}
tableExist=true;
}, 1, 1, TimeUnit.DAYS);
}
/**
* 获取库名
* @return 库名
*/
private String getSchemaName(){
String schemaName=null;
DataSource dataSource = initializer.getDataSource();
try {
String uri= dataSource.getConnection().getMetaData().getURL();
schemaName= JdbcUrlUtils.findDatabaseName(uri);
} catch ( SQLException e) {
e.printStackTrace();
}
return schemaName;
}
注意到,checkTableNextMonth
方法里有一个定时调度,而定时调度的任务里,有一个getSchemaName
,这个方法里手动根据DataSource获取了数据库连接,但是,却没有手动关闭!
所以产生了这样的结果:每天泄露一个数据库连接,so happy!
验证&复现
因为这个是定时任务,而且是1天1次,所以本地将1天1次改为每秒一次——即每秒泄露一个数据库连接。然后压测下:
普罗米修斯很快Connection Timeout Count
数量就上来了。 程序也很快包错误日志如下:
解决
找到bug了,就好办了。只需要将Connection的获取
放到try后面的括号里就行, 将 getSchemaName
改为如下即可:
/**
* 获取库名
* @return 库名
*/
private String getSchemaName(){
String schemaName=null;
DataSource dataSource = initializer.getDataSource();
**// 在try后圆括号中的变量,在try语句执行完毕后,会自动关闭。前提是这些变量得实现接口java.lang.AutoCloseable**
try(Connection connection = dataSource.getConnection()) {
String uri = connection.getMetaData().getURL();
schemaName= JdbcUrlUtils.findDatabaseName(uri);
} catch ( SQLException e) {
e.printStackTrace();
}
return schemaName;
}
再次经过压测,发现过了更长时间,还是出现很多超时连接。咋回事?
注意普罗米修斯界面里那个205上面的10,不难发现,这次的确是并发量太高了,Hikari 配置的10个连接数完全不够用,因为压测时定的并发量是200。
于是将Hikari的连接池配置修改为100,如下:
spring:
datasource:
hikari:
maximum-pool-size: 100
再次经过压测,发现没问题,虽然数据库连接池最大数量100 仍然低于并发的200,但是连接默认的等待时间30000(30秒),是允许等待的。而每次数据库执行是很快的,所以,没有超时的连接。
结论
我们手动获取Connection时,一定要记得关闭连接!
我们手动获取Connection时,一定要记得关闭连接!
我们手动获取Connection时,一定要记得关闭连接!