美文网首页mysql
Druid连接池的监控stat造成内存泄漏

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

作者: 小胖学编程 | 来源:发表于2021-01-18 17:03 被阅读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

    相关文章

      网友评论

        本文标题:Druid连接池的监控stat造成内存泄漏

        本文链接:https://www.haomeiwen.com/subject/qhdxzktx.html