mysql

Druid连接池的监控stat造成内存泄漏

2021-01-18  本文已影响0人  小胖学编程

阿里的Druid连接池可以对sql进行监控。但是监控信息会存储在内存中,某些场景下会造成内存泄漏。

1. 起因

线上某台机器报警(堆内存使用率高),登录服务器将堆dump下来,进行分析:

image.png

发现:JdbcDataSourceStat中的sqlStatMap比较消耗内存。

因为就是Druid开启stat监控,所以sql信息就会存储到该Map中,占用内存,造成内存泄漏。

stat监控sql信息页面:可以看到会持有sql信息。

image.png

当然也有人在github的Druid的Issues上提出了这个问题。每个sql语句都会长期持有引用,加快FullGC频率

2. 实际分析

sql信息存储到sqlStatMap的源码如下所示:

public class JdbcDataSourceStat implements JdbcDataSourceStatMBean {
    private final LinkedHashMap<String, JdbcSqlStat>            sqlStatMap;

    public JdbcSqlStat createSqlStat(String sql) {
        lock.writeLock().lock();
        try {
            JdbcSqlStat sqlStat = sqlStatMap.get(sql);
            if (sqlStat == null) {
                sqlStat = new JdbcSqlStat(sql);
                sqlStat.setDbType(this.dbType);
                sqlStat.setName(this.name);
                sqlStatMap.put(sql, sqlStat);
            }

            return sqlStat;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

我们发现若是sql(key)相同,那么不会put到Map中,那么key是什么样子呢?

经过本地debug分析:

image.png

可以知道,sql并没有携带参数,是原始的sql信息。

image.png

但在进行分析时,发现sqlStatMap中存储的key好像都是一个sql???

进行分析后发现:此sql是一个批量语句!

案例复现:当批量操作参数个数不同时,对于sqlStatMap是不同的key。

image.png

分析结论:批量操作,由于参数个数不同,导致sqlStatMap存储的数据量大。

3. SpringBoot2.x会自动开启Druid的stat

有同学发现,自己的SpringBoot项目的配置文件中并没有开启stat配置,但是还是出现上面现象。

需要注意的是:SpringBoot2.x可以自动装配Druid。且会自动开启stat监控。

public class DruidFilterConfiguration {

    @Bean
    @ConfigurationProperties(FILTER_STAT_PREFIX)
    @ConditionalOnProperty(prefix ="spring.datasource.druid.filter.stat", name = "enabled", matchIfMissing = true)
    @ConditionalOnMissingBean
    public StatFilter statFilter() {
        return new StatFilter();
    }
}

matchIfMissing = true意思是没有配置spring.datasource.druid.filter.stat=true,那么会加载该Bean。

解决方案:是在配置类中使用spring.datasource.druid.filter.stat=false,或者在自己的Configuration配置StatFilter这个bean。

当然也会自动开启监控台:

@ConditionalOnWebApplication
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true", matchIfMissing = true)
public class DruidStatViewServletConfiguration {
    @Bean
    public ServletRegistrationBean statViewServletRegistrationBean(DruidStatProperties properties) {
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean();
        registrationBean.setServlet(new StatViewServlet());
        registrationBean.addUrlMappings(config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*");
        if (config.getAllow() != null) {
            registrationBean.addInitParameter("allow", config.getAllow());
        }
        if (config.getDeny() != null) {
            registrationBean.addInitParameter("deny", config.getDeny());
        }
        if (config.getLoginUsername() != null) {
            registrationBean.addInitParameter("loginUsername", config.getLoginUsername());
        }
        if (config.getLoginPassword() != null) {
            registrationBean.addInitParameter("loginPassword", config.getLoginPassword());
        }
        if (config.getResetEnable() != null) {
            registrationBean.addInitParameter("resetEnable", config.getResetEnable());
        }
        return registrationBean;
    }
}

可访问http://ip:端口/druid/sql.html查看控制台,默认密码没有配置。

4. 解决方案

Druid的监控统计功能是通过filter-chain扩展实现,如果你要打开监控统计功能,配置StatFilter,具体看这里:https://github.com/alibaba/druid/wiki/配置_StatFilter

4.1 方案一:直接关闭Druid的stat

显式的在配置文件使用spring.datasource.druid.filter.stat=false

4.2 方案二:开启sql合并

结构重复的sql语句的sql比较多,可以开启sql合并。例如:批量操作导致sqlStatMap过大可以采用这种方案。

SQL监控的LinkedHashMap<String, JdbcSqlStat> sqlStatMap是以SQL语句作为键的。针对上面批量处理导致大量的sql存储到sqlStatMap的问题,可以开启sql合并。

image.png image.png

可以看到,只保留sql的结构,忽略sql的参数。

image.png

解决方案:
或者通过增加JVM的参数配置:

-Ddruid.stat.mergeSql=true
或者

spring:
    druid:
      connectionProperties: druid.stat.mergeSql=true

或者

@Configuration
public class DruidConfig {

    @Bean
    public StatFilter statFilter() {
        StatFilter statFilter = new StatFilter();
        statFilter.setMergeSql(true);
        return statFilter;
    }
}

4.3 方案三:控制sqlStatMap大小

有业务需求不能合并sql或者合并了sql也没有太大效果(结构重复的sql语句不多),也可以不设置sql合并而是设置druid.stat.sql.MaxSize(默认1000个)。

源码:

public class JdbcDataSourceStat implements JdbcDataSourceStatMBean {

        sqlStatMap = new LinkedHashMap<String, JdbcSqlStat>(16, 0.75f, false) {

            protected boolean removeEldestEntry(Map.Entry<String, JdbcSqlStat> eldest) {
                boolean remove = (size() > maxSqlSize);

                if (remove) {
                    JdbcSqlStat sqlStat = eldest.getValue();
                    if (sqlStat.getRunningCount() > 0 || sqlStat.getExecuteCount() > 0) {
                        skipSqlCount.incrementAndGet();
                    }
                }

                return remove;
            }
        };
}

LinkedHashMap有一个 removeEldestEntry(Map.Entry eldest)方法,通过覆盖这个方法,加入一定的条件,满足条件返回true。当put进新的值方法返回true时,便移除该map中最老的键和值。

sqlStatMap重写了removeEldestEntry方法,来控制最大数量。

解决方案:
或者通过增加JVM的参数配置:
-Ddruid.stat.sql.MaxSize=100

或者

spring:
    druid:
      connectionProperties: druid.stat.sql.MaxSize=100

推荐阅读

惨遭DruidDataSource和Mybatis暗算,导致OOM

上一篇下一篇

猜你喜欢

热点阅读