Java中使用Micrometer来实现监控数据的输出

声明:
1.本节以Prometheus作为监控平台来监控,所以Micrometer的实现模块选择的也是Prometheus的
2.本节主要展示效果,具体写法请阅读参考文档列出的文章


主要内容

一、如何在Springboot项目中使用Micrometer

spring boot actuator中配置了与Micrometer相关的自动配置,只要添加Mircrometer的具体实现模块的依赖即可,配置完成后,运行程序,即可在/actuator/prometheus路径下看到输出的metric信息

  • 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.2.1</version>
</dependency>

如果在运行时会出现找不到方法之类的异常,请适当降低依赖micrometer-registry-prometheus的版本

  • 配置spring boot actuator
management:
  endpoints:
    web:
      exposure:
        include: "health,info,prometheus"

二、注册表

Micrometer中核心两个内容便是meter和registry,注册表用于将各种meter注册到自身中进行管理,而meter就是各种监控的工具,比如计数器,计量器,计时器等等。不同的监控系统会有不同的注册表实现,其基类都是MeterRegistry,对应于Prometheus的注册表是PrometheusMeterRegistry。
Spring boot中只要你配置好了上节所述的内容后,就会自动生成一个MeterRegistry类型的bean,你只需要在需要使用它的地方注入即可,如果你需要自定义MeterRegistry的各种配置,比如公共标签(所有该项目的监控信息都会加上一个相同的标签)等等,你可以配置一个类型为MeterRegistryCustomizer<MeterRegistry>的bean,来进行一些自定义的操作:

@Bean
@ConditionalOnBean({MtkMeterRegistryAutoConfiguration.class})
@Autowired
public MeterRegistryCustomizer<MeterRegistry> meterRegistryCustomizer(MtkMeterRegistryConfiguration mtkMeterRegistryConfiguration){
    return mtkMeterRegistryConfiguration::apply;
}

三、计数器

计数器用来表示计数类监控项目,比如“控制器的访问次数”,“方法的调用次数”等等,这类监控信息都是只增不减的,且和次数有关。

  • 创建一个计数器,对访问页面"/meter/greet"的次数进行计数
    • 控制器
    /**
    * 每访问一次该页面,green_count计数器+1
    */
    @RequestMapping("/greet")
    public String greet(){
        return "greet count: " + metricTemplate.counterAdd("greet.count");
    }
    
    • MetricTemplate#counterAdd
    public double counterAdd(String counterName , String... tags){
        return counterAdd(counterName , 1 , tags);
    }
    
    public double counterAdd(String counterName , double increment ,String... tags){
        Counter counter = meterRegistry.counter(counterName, tags);
        counter.increment(increment);
        return counter.count();
    }
    
    • 访问localhost:8080/meter/greet5次
    • 访问localhost:8080/actuator/prometheus搜索"greet_count"
    # HELP greet_count_total  
    # TYPE greet_count_total counter
    greet_count_total{project="mtk-micrometer",} 5.0
    

四、计量器

计量器用于持续计量类的任务,比如“集合长度”、“加载了类的个数”、“最大访问时间”等等。

  • 创建一个计量器,对一个集合的长度进行计量,每访问/meter/random一次,这个集合长度就会加一
    • 控制器
    @Autowired
    public MeterTest(MetricTemplate metricTemplate){
        this.metricTemplate = metricTemplate;
        //计量器初始化
        this.metricTemplate.gaugeSet("random.list.gauge" , target -> ((List)target).size() , randomIntList);
    }
    /**
     *每访问一次,生成一个随机值,并存入randomIntList
     * @return 返回随机产生的值
     */
    @RequestMapping("/random")
    public int randomNumber(){
        int randomNumber = (int) (Math.random() * 100);
        randomIntList.add(randomNumber);
        return randomNumber;
    }
    
    • MetricTemplate#gaugeSet
    public void gaugeSet(String gaugeName , ToDoubleFunction<Object> f , Object target , String... tags){
        ArrayList<Tag> tagList = new ArrayList<>();
        if(tags.length %2 == 1) throw new IllegalArgumentException("Tags length is not correctly, length: " + tags.length);
        for(int i = 0 ; i < tags.length ; i+=2){
            tagList.add(Tag.of(tags[i] , tags[i+1]));
        }
        meterRegistry.gauge(gaugeName , tagList , target , f);
    }
    
    • 访问localhost:8080/meter/random
    • 访问localhost:8080/actuator/prometheus搜索"random_list_gauge"
    # HELP random_list_gauge  
    # TYPE random_list_gauge gauge
    random_list_gauge{project="mtk-micrometer",} 5.0
    
  • Tips
    • 不要用计量器去测量没有测量上下限的玩意儿
    • 不要用计量器做计数器做的事
    • 计量器中对测量对象的引用使用的都是弱引用(WeakReference),所以不会影响到垃圾收集,但因此若是目标对象被垃圾收集了,则计量器的显示结果将会是NaN
    • 不要用计量器去计量装箱类型的数值,因为它们是不可变的,尝试去重新为新值注册同名计量器也是不允许的,因为注册表只可存在一份同(名、标签)的Meter

五、计时器

计时器用于计时类监控,比如“某个线程的执行时间”,“某个操作的执行时间”。计时器有两种,一种是普通计时器,一种是长任务计时器,前者会在计时的任务结束后将计时器注册到注册表中,后者则可以实时显示任务执行了多久,即使任务还没执行完。需要注意的是,在使用record(Runnable runnable)方法对Runnable进行计时时,并不会去启动一个线程,而是执行Runnable中的run方法,并对该方法的执行时间进行计时。

  • 创建一个计时器,该计时器会对一个新启动的线程进行计时,该线程一共运行5s,访问/meter/timer就会触发
    • 控制器
    /**
     * 访问该页面后会启动一个执行5秒的线程,并用timer计算执行的时间
     */
    @RequestMapping("/timer")
    public void timerTest(){
        Meter.Id meterIdentity = metricTemplate.startTimer("timer.test");
        new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            metricTemplate.stopTimer(meterIdentity);
        }).start();
    }
    
    • MetricTemplate#startTimer和MetricTemplate#stopTimer
    public Meter.Id startTimer(String timerName , String... tags){
        Tags meterTags = Tags.of(tags);
        Meter.Id meterIdentity = new Meter.Id(timerName , meterTags , null , null , null);
        Timer.Sample sample = Timer.start();
        sampleMap.put(meterIdentity , sample);
        return meterIdentity;
    }
    
    public void stopTimer(Meter.Id meterIdentity){
        Timer.Sample sample = sampleMap.remove(meterIdentity);
        sample.stop(meterRegistry.timer(meterIdentity.getName() ,   meterIdentity.getTags()));
    }
    
    • 访问localhost:8080/meter/timer触发线程
    • 5秒后,访问localhost:8080/meter/prometheus搜索"timer_test"
    # HELP timer_test_seconds  
    # TYPE timer_test_seconds summary
    timer_test_seconds_count{project="mtk-micrometer",} 1.0
    timer_test_seconds_sum{project="mtk-micrometer",} 5.017332
    # HELP timer_test_seconds_max  
    # TYPE timer_test_seconds_max gauge
    timer_test_seconds_max{project="mtk-micrometer",} 5.017332
    
  • 使用@Timed注解快速为一个方法设定时间监控
    • 由于@Timed是基于Aop的,故你需要有AspectJ依赖,而且@Timed注解默认是无效的,你需要添加一个timedAspect来开启这个功能
    @Bean
    @Autowired
    @ConditionalOnMissingBean
    public TimedAspect timedAspect(MeterRegistry meterRegistry){
        return new TimedAspect();
    }
    
    • 然后你就可以在任意方法上使用@Timed注解来开启时间监控功能了
    @Timed
    public void sleep5SecondsFunc(){
        try {
            Thread.currentThread().sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    • 结果:
    # HELP method_timed_seconds  
    # TYPE method_timed_seconds summary
    method_timed_seconds_count{class="cn.mutsuki.micrometer.service.ExampleService",exception="none",method="sleep5SecondsFunc",project="mtk-micrometer",} 1.0
    method_timed_seconds_sum{class="cn.mutsuki.micrometer.service.ExampleService",exception="none",method="sleep5SecondsFunc",project="mtk-micrometer",} 5.0255475
    # HELP method_timed_seconds_max  
    # TYPE method_timed_seconds_max gauge
    method_timed_seconds_max{class="cn.mutsuki.micrometer.service.ExampleService",exception="none",method="sleep5SecondsFunc",project="mtk-micrometer",} 5.0255475
    

额外内容

六、Meter的命名

监控系统会根据得到的监控信息的名称来进行一定的分析,所以如果你的命名毫无章法,那么可能会削弱一些监控的性能,Micrometer通过一个名为NamingConvention的接口来规定命名的规则。比如你在程序中使用了java.jvm.memory来命名一个Meter,而你使用的NamingConvention的实现类是针对于Prometheus的,那么监控信息的输出结果上,该Meter对应的信息的名字会是java_jvm_memory。Micrometer默认的命名规则,是以句点进行分割。下面给出同一个名字在不同监控系统的本地化命名:

原命名:http.server.requests
Prometheus : http_server_requests_duration_seconds
Atlas : httpServerRequests
Graphite : http.server.requests
InfluxDB : http_server_requests


七、Meter过滤器

Meter过滤器会根据一定的规则来判断一个Meter是否应该被注入到注册表中,以及何时注入到注册表中,也可以用来对匹配的Meter进行一些额外的配置,比如添加前缀等等。除此之外,TimerDistributionSummary这两种Meter还包含了额外的统计数据的相关配置,这也可以使用过滤器来实现一次性的配置。

  • 如何创建一个过滤器?
    创建一个过滤器有两种方式,第一种是创建实现MeterFilter接口的类,第二种是使用MeterFilter自带的多种静态方法。
  • MeterFilter中核心的几个方法如下:
    • MeterFilterReply accept(Id id) : 根据输入的Meter.Id来判断该Meter注入到注册表的策略
    • Id map(Id id) : 根据输入的Meter.Id来返回一个新的Meter.Id代替旧的
    • DistributionStatisticConfig configure(Id id, DistributionStatisticConfig config) : 统计数据的相关配置(TimerDistributionSummary适用)
  • MeterFilterReply是一个枚举,用来表示注册的策略,它一共有三种值:
    • DENY 不允许该Meter注册
    • NEUTRAL 如果没有其他过滤器返回DENY则注册
    • ACCEPT 立即注册,不用管其后是否有其他的过滤器
  • MeterFilter提供的一些易用的方法
    • MeterFilter accept() 返回具有通过规则的过滤器
    • MeterFilter deny() 返回具有拒绝规则的过滤器
    • MeterFilter accept(final Predicate<Id> iff)/MeterFilter deny(final Predicate<Id> iff)根据Predicate语句的结果来是通过/拒绝(返回true),还是中立(返回false)
    • MeterFilter denyUnless(final Predicate<Id> iff) 就是MeterFilter deny(final Predicate<Id> iff)结果的反过来
    • MeterFilter commonTags(final Iterable<Tag> tags) 给所有Meter添加公共的标签
    • acceptNameStartsWith(String prefix)/denyNameStartsWith(String prefix)根据Meter的名字前缀来判断是通过还是拒绝
    • MeterFilter maximumAllowableMetrics(final int maximumTimeSeries) 如果注册的Meter达到参数指定的上限,则无法注册
    • MeterFilter renameTag(final String meterNamePrefix, final String fromTagKey, final String toTagKey)如果Meter的名字是以meterNamePrefix为前缀,且标签中有fromTagKey的标签,则将该标签改为toTagKey,标签对应的值保持不变
    • MeterFilter ignoreTags(final String... tagKeys)将所有Meter的与tagKeys匹配的标签去除掉
  • MeterFilter可以串起来使用,如下例是个白名单的应用示例:
registry.config()
    .meterFilter(MeterFilter.acceptNameStartsWith("http"))
    .meterFilter(MeterFilter.deny()); (1)

当Meter的名字为“http”开头时才允许该Meter被使用


参考文档:
[1] 使用 Micrometer 记录 Java 应用性能指标
[2] Micrometer官方文档


测试项目地址:

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

推荐阅读更多精彩内容