Springboot+prometheus监控数据库连接池

背景

之前有提到过使用Prometheus做Springboot的监控,这次以一个实例来说明,通过一种统一的方式,监控数据库连接池的运行情况。

原理

其实在Springboot内部监控都是结合了micrometer来做的,基于他的MeterRegistry,实现多种方式的监控,如PrometheusMeterRegistry。他也提供了对于很多的监控实现,如缓存,线程池,tomcat,JVM,OKhttp等。对应在io.micrometer.core.instrument.binder包中。

数据库相关datasource的监控默认在spring-boot-actuator(使用版本为2.1.4.RELEASE),具体在org.springframework.boot.actuate.metrics.jdbc.DataSourcePoolMetrics。这里会参考已有数据库连接池监控的实现,完成对项目中使用的druid的简单监控。

关于mecrometer的相关的依赖,可以引入如下:

     <dependency>
          <groupId>io.micrometer</groupId>
          <artifactId>micrometer-core</artifactId>
          <version>1.5.1</version>
      </dependency>
      <dependency>
          <groupId>io.micrometer</groupId>
          <artifactId>micrometer-registry-prometheus</artifactId>
          <version>1.5.1</version>
      </dependency>

步骤

分析已有实现

我们看org.springframework.boot.actuate.metrics.jdbc.DataSourcePoolMetrics中的实现,是使用的org.springframework.boot.jdbc.metadata.DataSourcePoolMetadata来获取数据库连接池运行数据的。再看这个接口,在spring-boot:2.1.4.RELEASE中包括tomcat,dbcp2,hikari是有默认实现,比如下面是hikari的实现,获取到了当前运行连接数,最高连接数,探活SQL等。

/**
 * {@link DataSourcePoolMetadata} for a Hikari {@link DataSource}.
 *
 * @author Stephane Nicoll
 * @since 2.0.0
 */
public class HikariDataSourcePoolMetadata
        extends AbstractDataSourcePoolMetadata<HikariDataSource> {

    public HikariDataSourcePoolMetadata(HikariDataSource dataSource) {
        super(dataSource);
    }

    @Override
    public Integer getActive() {
        try {
            return getHikariPool().getActiveConnections();
        }
        catch (Exception ex) {
            return null;
        }
    }

    private HikariPool getHikariPool() {
        return (HikariPool) new DirectFieldAccessor(getDataSource())
                .getPropertyValue("pool");
    }

    @Override
    public Integer getMax() {
        return getDataSource().getMaximumPoolSize();
    }

    @Override
    public Integer getMin() {
        return getDataSource().getMinimumIdle();
    }

    @Override
    public String getValidationQuery() {
        return getDataSource().getConnectionTestQuery();
    }

    @Override
    public Boolean getDefaultAutoCommit() {
        return getDataSource().isAutoCommit();
    }

}

DataSourcePoolMetadata的定义,可以参考注释理解各个方法的意义。

public interface DataSourcePoolMetadata {

    /**
     * Return the usage of the pool as value between 0 and 1 (or -1 if the pool is not
     * limited).
     * <ul>
     * <li>1 means that the maximum number of connections have been allocated</li>
     * <li>0 means that no connection is currently active</li>
     * <li>-1 means there is not limit to the number of connections that can be allocated
     * </li>
     * </ul>
     * This may also return {@code null} if the data source does not provide the necessary
     * information to compute the poll usage.
     * @return the usage value or {@code null}
     */
    Float getUsage();

    /**
     * Return the current number of active connections that have been allocated from the
     * data source or {@code null} if that information is not available.
     * @return the number of active connections or {@code null}
     */
    Integer getActive();

    /**
     * Return the maximum number of active connections that can be allocated at the same
     * time or {@code -1} if there is no limit. Can also return {@code null} if that
     * information is not available.
     * @return the maximum number of active connections or {@code null}
     */
    Integer getMax();

    /**
     * Return the minimum number of idle connections in the pool or {@code null} if that
     * information is not available.
     * @return the minimum number of active connections or {@code null}
     */
    Integer getMin();

    /**
     * Return the query to use to validate that a connection is valid or {@code null} if
     * that information is not available.
     * @return the validation query or {@code null}
     */
    String getValidationQuery();

    /**
     * The default auto-commit state of connections created by this pool. If not set
     * ({@code null}), default is JDBC driver default (If set to null then the
     * java.sql.Connection.setAutoCommit(boolean) method will not be called.)
     * @return the default auto-commit state or {@code null}
     */
    Boolean getDefaultAutoCommit();

}

具体看下org.springframework.boot.actuate.metrics.jdbc.DataSourcePoolMetrics的实现,这里将单个datasource导入,然后根据实现的DataSourcePoolMetadata,获取到了具体的运行数据,然后,本身实现了MeterBinder接口,所以最终通过在bindTo(MetricRegistry registry)调用bindPoolMetadata() -> bindDataSource()完成监控的记录。

public class DataSourcePoolMetrics implements MeterBinder {

    private final DataSource dataSource;

    private final CachingDataSourcePoolMetadataProvider metadataProvider;

    private final Iterable<Tag> tags;

    public DataSourcePoolMetrics(DataSource dataSource,
            Collection<DataSourcePoolMetadataProvider> metadataProviders,
            String dataSourceName, Iterable<Tag> tags) {
        this(dataSource, new CompositeDataSourcePoolMetadataProvider(metadataProviders),
                dataSourceName, tags);
    }

    public DataSourcePoolMetrics(DataSource dataSource,
            DataSourcePoolMetadataProvider metadataProvider, String name,
            Iterable<Tag> tags) {
        Assert.notNull(dataSource, "DataSource must not be null");
        Assert.notNull(metadataProvider, "MetadataProvider must not be null");
        this.dataSource = dataSource;
        this.metadataProvider = new CachingDataSourcePoolMetadataProvider(
                metadataProvider);
        this.tags = Tags.concat(tags, "name", name);
    }

    @Override
    public void bindTo(MeterRegistry registry) {
        if (this.metadataProvider.getDataSourcePoolMetadata(this.dataSource) != null) {
            bindPoolMetadata(registry, "active", DataSourcePoolMetadata::getActive);
            bindPoolMetadata(registry, "max", DataSourcePoolMetadata::getMax);
            bindPoolMetadata(registry, "min", DataSourcePoolMetadata::getMin);
        }
    }

    private <N extends Number> void bindPoolMetadata(MeterRegistry registry,
            String metricName, Function<DataSourcePoolMetadata, N> function) {
        bindDataSource(registry, metricName,
                this.metadataProvider.getValueFunction(function));
    }

    private <N extends Number> void bindDataSource(MeterRegistry registry,
            String metricName, Function<DataSource, N> function) {
        if (function.apply(this.dataSource) != null) {
            registry.gauge("jdbc.connections." + metricName, this.tags, this.dataSource,
                    (m) -> function.apply(m).doubleValue());
        }
    }

    private static class CachingDataSourcePoolMetadataProvider
            implements DataSourcePoolMetadataProvider {

        private static final Map<DataSource, DataSourcePoolMetadata> cache = new ConcurrentReferenceHashMap<>();

        private final DataSourcePoolMetadataProvider metadataProvider;

        CachingDataSourcePoolMetadataProvider(
                DataSourcePoolMetadataProvider metadataProvider) {
            this.metadataProvider = metadataProvider;
        }

        public <N extends Number> Function<DataSource, N> getValueFunction(
                Function<DataSourcePoolMetadata, N> function) {
            return (dataSource) -> function.apply(getDataSourcePoolMetadata(dataSource));
        }

        @Override
        public DataSourcePoolMetadata getDataSourcePoolMetadata(DataSource dataSource) {
            DataSourcePoolMetadata metadata = cache.get(dataSource);
            if (metadata == null) {
                metadata = this.metadataProvider.getDataSourcePoolMetadata(dataSource);
                cache.put(dataSource, metadata);
            }
            return metadata;
        }

    }

}

而关于什么时候使用了DataSourceMetric,可以看它的构造方法引用,找到org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration,如下,可以看到,是通过spring的依赖注入,完成对所有datasource构造DataSourceMetric并注册到MeterRegistry的。

        //注入所有Datasource ,结构为Map,名称和对应实例
        @Autowired
        public void bindDataSourcesToRegistry(Map<String, DataSource> dataSources) {
            dataSources.forEach(this::bindDataSourceToRegistry);
        }

        private void bindDataSourceToRegistry(String beanName, DataSource dataSource) {
            String dataSourceName = getDataSourceName(beanName);
            new DataSourcePoolMetrics(dataSource, this.metadataProviders, dataSourceName,
                    Collections.emptyList()).bindTo(this.registry);
        }

        /**
         * Get the name of a DataSource based on its {@code beanName}.
         * @param beanName the name of the data source bean
         * @return a name for the given data source
         */
        private String getDataSourceName(String beanName) {
            if (beanName.length() > DATASOURCE_SUFFIX.length()
                    && StringUtils.endsWithIgnoreCase(beanName, DATASOURCE_SUFFIX)) {
                return beanName.substring(0,
                        beanName.length() - DATASOURCE_SUFFIX.length());
            }
            return beanName;
        }

实现druid连接池监控

完成了原理的分析,现在我们再来了解怎么实现对druid的监控。参考上面,定义一个自定义的DruidDataSourcePoolMetadata实现自DataSourcePoolMetadata或者AbstractDataSourcePoolMetadata。但目前问题是,上面使用的是
CachingDataSourcePoolMetadataProvider一个内部类的provider,没法直接使用将DruidDataSourcePoolMetadata添加到里边。但回到DataSourcePoolMetricsDataSourcePoolMetricsAutoConfiguration,可以看到DataSourcePoolMetadataProvider可以注入多个。因此,再实现个针对DruidDataSourcePoolMetadataDataSourcePoolMetadataProvider,然后执行注入,就可以得到我们想要的功能。

    @Bean
    public DataSourcePoolMetadataProvider druidPoolDataSourceMetadataProvider() {
        return (dataSource) -> {
            DruidDataSource ds = DataSourceUnwrapper.unwrap(dataSource,
                    DruidDataSource.class);
            if (ds != null) {
                return new DruidDataSourcePoolMetadata(ds);
            }
            return null;
        };
    }
    /**
         * 参考 org.springframework.boot.jdbc.metadata.HikariDataSourcePoolMetadata
         */
        private class DruidDataSourcePoolMetadata extends AbstractDataSourcePoolMetadata<DruidDataSource> {
            /**
             * Create an instance with the data source to use.
             *
             * @param dataSource the data source
             */
            DruidDataSourcePoolMetadata(DruidDataSource dataSource) {
                super(dataSource);
            }
    
            @Override
            public Integer getActive() {
                return getDataSource().getActiveCount();
            }
    
            @Override
            public Integer getMax() {
                return getDataSource().getMaxActive();
            }
    
            @Override
            public Integer getMin() {
                return getDataSource().getMinIdle();
            }
    
            @Override
            public String getValidationQuery() {
                return getDataSource().getValidationQuery();
            }
    
            @Override
            public Boolean getDefaultAutoCommit() {
                return getDataSource().isDefaultAutoCommit();
            }
        }

结果

结果

总结

以上就是本期的所有内容,感谢阅读。可以看到spring-boot在模块化方面特别好,各种自定义实现可以在多个层次很好的组合起来,对于开发人员来说是十分方便的。这个例子中很多的思路可以作为以后开发的参考。同时,druid本身自带很多运行参数的监控,参考com.alibaba.druid.stat,如果能添加更多指标的数据并和DataSourceMetric结合,也是不错的选择,看起来实现也并不是很复杂。

参考资料

  1. micrometer
  2. custom_metrics_micrometer_prometheus_spring_boot_actuator
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容