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

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


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

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

  • 添加依赖

    org.springframework.boot
    spring-boot-starter-actuator


    io.micrometer
    micrometer-registry-prometheus
    1.2.1

如果在运行时会出现找不到方法之类的异常,请适当降低依赖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的bean,来进行一些自定义的操作:

@Bean
@ConditionalOnBean({MtkMeterRegistryAutoConfiguration.class})
@Autowired
public MeterRegistryCustomizer 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 f , Object target , String... tags){
        ArrayList 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 iff)/MeterFilter deny(final Predicate iff)根据Predicate语句的结果来是通过/拒绝(返回true),还是中立(返回false)
      • MeterFilter denyUnless(final Predicate iff) 就是MeterFilter deny(final Predicate iff)结果的反过来
      • MeterFilter commonTags(final Iterable 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官方文档


    测试项目地址:

    你可能感兴趣的:(Java中使用Micrometer来实现监控数据的输出)