记一次数据库连接不可用,连接超时问题:HikariPool-1 - Connection is not available, request timed out after 30008ms

Administrator
Administrator
发布于 2023-05-09 / 1371 阅读
0
0

记一次数据库连接不可用,连接超时问题:HikariPool-1 - Connection is not available, request timed out after 30008ms

问题

最近线上忽然频繁多个系统爆出同一个错误:

HikariPool-1 - Connection is not available, request timed out after 30008ms

经过测试同事和运维的反馈,这个问题重启后消失,但是在运行一段时间后会再次爆出,这段时间大概为一个月左右。

分析

很明显,上述问题是数据库连接问题,而且是Hikari的数据库连接池给出的”数据库连接不可用,请求超时“。

上面的实施描述中有几个关键线索:

  • 最近: 意味着之前是没有的,最近才产生。最近改了什么?
  • **多个系统:**意味着很可能是共用的底层依赖jar包,或者是公共服务,例如mysql引起的。
  • 重启后有一个月左右稳定期: 没有立即暴雷,而是运行了一段时间,很可能是积累的。

经过确认,排除以下可能:

  1. 运维最近给很多系统改小了连接池配置,给程序穿了小鞋。
  2. 最近并发量呈现出阶段式稳步递增的现象,而且递增到突破了系统承受范围(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次改为每秒一次——即每秒泄露一个数据库连接。然后压测下:

Untitled

普罗米修斯很快Connection Timeout Count数量就上来了。 程序也很快包错误日志如Untitled 1下:

解决

找到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时,一定要记得关闭连接!


评论