美文网首页
Java中使用Micrometer来实现监控数据的输出

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

作者: 睦月MTK | 来源:发表于2020-04-15 17:35 被阅读0次

    声明:
    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官方文档


    测试项目地址:

    相关文章

      网友评论

          本文标题:Java中使用Micrometer来实现监控数据的输出

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