micrometer埋点(Spring boot 2.X metrics)

micrometer埋点


UTOOLS1578896722004.png

本文代码和演示基于 spring boot 2.1.4.RELEASE

本文作者:gsh456,转载请标明作者或者原文链接。


[TOC]


1前言

1.1作用

运行良好的应用离不开对性能指标的收集。这些性能指标可以有效地对生产系统的各方面行为进行监控,帮助运维人员掌握系统运行状态和查找问题原因。性能指标监控通常由两个部分组成:第一个部分是性能指标数据的收集,需要在应用程序代码中添加相应的代码来完成;另一个部分是后台监控系统,负责对数据进行聚合计算和提供 API 接口。在应用中使用计数器、计量仪和计时器来记录关键的性能指标。在专用的监控系统中对性能指标进行汇总,并生成相应的图表来进行可视化分析。

Micrometer 为 Java 平台上的性能数据收集提供了一个通用的 API,应用程序只需要使用 Micrometer 的通用 API 来收集性能指标即可。Micrometer 会负责完成与不同监控系统的适配工作。这就使得切换监控系统变得很容易。Micrometer 还支持推送数据到多个不同的监控系统。

1.2 Actuator

你可以通过文末的参考文章获取它的详细概念和具体使用方法。这里只是简述下他的概念和使用方式,本文重点在micrometer。

Spring Boot Actuator是Spring Boot的一个组件,可以帮助你监控和管理Spring Boot应用,比如健康检查、审计、统计和HTTP追踪等。所有的这些特性可以通过JMX或者HTTP endpoints来获得。
你可以访问 http://ip:端口/actuator 查看系统中暴露的endpoint信息,也可以加上具体的 endpoint 查看他们的详细情况,例如 http://127.0.0.1:7001/actuator/health 查看健康信息。Spring Boot 2中的依赖actuator中集成的度量统计API使用的框架是Micrometer。官网

本文重点介绍/actuator/metrics,其他的就属于抛砖了。

1.3 metrics

打开 ip:端口/actuator/metrics 网址就可以看到当前微服务的所有metrics,每一个metric都相当于influx数据库的一个measurement,也就是传统数据库的数据表的概念。

UTOOLS1578886600199.png
{
    "names": [
        "jvm.memory.committed",
        "jvm.threads.states",
        "tomcat.sessions.rejected",
        "logback.events",
        "http.server.requests",
        "process.cpu.usage",
        "jvm.memory.used",
        "jvm.memory.max",
        "jvm.classes.loaded",
        "system.memory.free",
        "system.memory.used",
        "jvm.buffer.count",
        "tomcat.global.sent",
        "jvm.classes.unloaded",
        "jvm.buffer.total.capacity",
        "tomcat.sessions.active.current",
        "jvm.buffer.memory.used",
        "tomcat.sessions.alive.max",
        "jvm.gc.live.data.size",
        "tomcat.global.request.max",
        "tomcat.global.request",
        "tomcat.global.received",
        "tomcat.sessions.active.max",
        "jvm.gc.pause",
        "process.start.time",
        "tomcat.threads.config.max",
        "jvm.gc.memory.promoted",
        "timer2",
        "tomcat.global.error",
        "hystrix.rolling.max.active.threads",
        "jvm.gc.max.data.size",
        "tomcat.threads.current",
        "system.cpu.count",
        "hystrix.pool.size",
        "user.counter.total",
        "tomcat.sessions.created",
        "jvm.threads.daemon",
        "system.cpu.usage",
        "tomcat.threads.busy",
        "jvm.gc.memory.allocated",
        "system.memory.percent",
        "user.gauge.total",
        "hystrix.rolling.count.threads.rejected",
        "tomcat.sessions.expired",
        "hystrix.thread.pool",
        "jvm.threads.live",
        "jvm.threads.peak",
        "process.uptime"
    ]
}

把这些信息按照json格式化一下,这里记录着微服务的名字,例如 process.uptime 这个metric就是记录当前微服务的启动时长的metric,我们再进一步访问ip:端口/actuator/metrics/process.uptime 就会看到以下内容

{
    "name": "process.uptime",
    "description": "The uptime of the Java virtual machine",
    "baseUnit": "milliseconds",
    "measurements": [
        {
            "statistic": "VALUE",
            "value": 4949999
        }
    ],
    "availableTags": [
        {
            "tag": "port",
            "values": [
                "7006"
            ]
        },
        {
            "tag": "ip",
            "values": [
                "192.168.3.70"
            ]
        },
        {
            "tag": "uuid",
            "values": [
                "192.168.3.70:7006"
            ]
        }
    ]
}

从measurements中的value值就可以看出,当前微服务已经运行 1.4小时了(value实际为毫秒值这里手动换算为小时),tag为标签,标签是一个很重要的概念。这里有三个标签,分别代表当前服务运行的ip地址,端口号,以及他们两个的组合值 uuid。这三个标签是系统已经默认有的,不用管他们。你也可以自定义标签。

public class CustomMetrics implements MeterRegistryCustomizer {
@Override
    public void customize(MeterRegistry registry) {
         //唯一标识, IP:port
        registry.config()
                .commonTags("ip", host + "" )
                .commonTags("port",  port+"")
                .commonTags("uuid", host + ":" + port);
    }
}

2 概念

2.1 MeterRegistry

Meter是收集关于你的应用的一系列指标的接口。Meter是由MeterRegistry创建的。每个支持的监控系统都必须实现MeterRegistry。Micrometer中包含一个SimpleMeterRegistry,它在内存中维护每个meter的最新值,并且不支持将数据导出到任何地方,主要用来进行本地开发和测试。
Micrometer 支持多个不同的监控系统。通过计量器注册表实现类 CompositeMeterRegistry 可以把多个计量器注册表组合起来,从而允许同时发布数据到多个监控系统。对于由这个类创建的计量器,它们所产生的数据会对 CompositeMeterRegistry中包含的所有计量器注册表都产生影响。

2.2 常用类型

Micrometer中,Meter[1]的常用类型包括:CounterGaugeTimerDistributionSummary ,其中前两种用的最多。

2.3 Meter命名

Micrometer中,Meter的命名约定使用英文逗号(dot,也就是".")分隔单词。但是不同的监控系统,对命名的规约可能并不相同,如果命名规约不一致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。Micrometer中使用英文逗号分隔单词的命名规则,再通过底层的命名转换接口NamingConvention进行转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖NamingConvention实现自定义的命名转换规则:registry.config().namingConvention(myCustomNamingConvention);。在Micrometer中,对一些主流的监控系统或者存储系统的命名规则提供了默认的转换方式。例如 在代码中 order.request.times这些写,那么在influxDb对应的Measurement 就是 order_request_times,Micrometer对接不同的监控系统命名规则是不一样的。

MeterRegistry registry =  new SimpleMeterRegistry();
registry.timer("order.request.times");

2.4 Tag

Tag(标签)是Micrometer的一个重要的功能,严格来说,一个度量框架只有实现了标签的功能,才能真正地多维度进行度量数据收集。Tag的命名一般需要是有意义的,所谓有意义就是可以根据Tag的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。假设我们需要监控数据库的调用和Http请求调用统计,一般推荐的做法是:

MeterRegistry registry = new SimpleMeterRegistry();
//country相当于 tag key,China相当于tag value,tag value不能为空
registry.counter("request.times.order", "country", "China");
registry.counter("request.times.pay", "type", "alipay");

另外Micrometer中的tag和InfluxDb的tag一致,而且如果这个 meter同步到influxDb,那么meter中的tag也会同步到InfluxDb 表的tag。所以tag命名要规范,尽量避免 ,例如刚刚的代码就尽量不要这些写

registry.counter("request“,”times","order", "country", "China");
registry.counter("request","times","pay", "type", "alipay");

3. METER

3.1 Counter

Counter是一种比较简单的Meter,它是一种单值的度量类型,或者说是一个单值计数器。Counter的作用是记录总量或者计数值,适用于一些单向增长类型的统计,例如下单支付次数接口请求总量记录等等,通过Tag可以区分不同的场景,对于下单,可以使用不同的Tag标记不同的业务来源或者是按日期划分,对于Http请求总量记录,可以使用Tag区分不同的URL。

//标签值为 china的请求次数
MeterRegistry meterRegistry = new SimpleMeterRegistry();
//  写法一
Counter counter = meterRegistry.counter("request.times.order", "country", "China");
 //  写法二
 Counter counter2 = Counter
     .builder("request.times.order")
     .baseUnit("short") // optional
     .description("a description of what this counter does") // optional
     .tags("country", "China") // optional
     .register(registry);
//请求次数+1
counter.increment();
counter2.increment();
System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=1.0}]

SimpleMeterRegistry无法到数据导出。如果想将数据进行导出,

  1. 使用全局性质的 MeterRegistry 。
  2. 使用Metrics的静态方法(例子中使用)。
 private void count(String c){
        Metrics.counter("request.times.order", "country", c).increment();
    }

或者

    static final Counter userCounter = Metrics.counter("user.counter.total", "services", "demo");
    public void processCollectResult1() {
        userCounter.increment();
        System.out.println(userCounter.count());
    }
    public void processCollectResult2()  {
        userCounter.increment();
        System.out.println(userCounter.count());
    }

看情况使用,第二种在启动微服务后,就会metrics中存在,第一种只会在调用之后在metrics中存在。这两种方式都是全局的,如果 名字和标签都是一样的,就会在上一次的基础上+1;另外你也可以 在 increment() 方法中填入增加的数量。

3.2 Gauge

Gauge(仪表)是获取当前度量记录值的句柄,也就是它表示一个可以任意上下浮动的单数值度量Meter。Gauge通常用于变动的测量值,测量值用ToDoubleFunction参数的返回值设置,如当前的内存使用情况,同时也可以测量上下移动的”计数”,比如队列中的消息数量。官网文档中提到Gauge的典型使用场景是用于测量集合或映射的大小或运行状态中的线程数。Gauge一般用于监测有自然上界的事件或者任务,而Counter一般使用于无自然上界的事件或者任务的监测,所以像Http请求总量计数应该使用Counter而非Gauge。

//必须在方法的外面, 我的实验结果是这样的,如果又更好的方法或者不同意此观点,请联系我,学习是不断进步的。
AtomicDouble atomicDouble = new AtomicDouble(0);
AtomicDouble ad = Metrics.gauge("user.gauge.total", init(), atomicDouble);
AtomicDouble ad2 = Metrics.gauge("user.gauge.total", init(), atomicDouble);
    public void gauge1(Double d)  {
        //这里使用  atomicDouble 或者 ad 这两个变量都可以
        ad.set(d);
        System.out.println(atomicDouble);
    }
    public void gauge2(Double d)  {
        ad2.set(d);
        System.out.println(atomicDouble);
    }

3.3 Timer

Timer(计时器)适用于记录耗时比较短的事件的执行时间,通过时间分布展示事件的序列和发生频率。所有的Timer的实现至少记录了发生的事件的数量和这些事件的总耗时,从而生成一个时间序列。Timer的基本单位基于服务端的指标而定,但是实际上我们不需要过于关注Timer的基本单位,因为Micrometer在存储生成的时间序列的时候会自动选择适当的基本单位。Timer接口提供的常用方法如下:

//内存中 
public void timer1(){
        Timer timer = Timer.builder("timer")
                .tag("timer", "timersample")
                .description("timer sample test.")
                .register(new SimpleMeterRegistry());
        for(int i=0; i<2; i++) {
            timer.record(() -> {
                createOrder();
            });
        }
        System.out.println(timer.count());
        System.out.println(timer.measure());
        System.out.println(timer.totalTime(TimeUnit.SECONDS));
        System.out.println(timer.mean(TimeUnit.SECONDS));
        System.out.println(timer.max(TimeUnit.SECONDS));
    }
//全局
    public void timer2(){
        Timer timer = Metrics.timer("timer2", "timer", "timersample");
        for(int i=0; i<2; i++) {
            timer.record(() -> {
                createOrder();
            });
        }
        System.out.println(timer.count());
        System.out.println(timer.measure());
        System.out.println(timer.totalTime(TimeUnit.SECONDS));
        System.out.println(timer.mean(TimeUnit.SECONDS));
        System.out.println(timer.max(TimeUnit.SECONDS));
    }
    private static void createOrder() {
        try {
            TimeUnit.SECONDS.sleep(5); //模拟方法耗时
        } catch (InterruptedException e) {
            //no-operation
        }
    }

3.4 DistributionSummary

Summary(摘要)主要用于跟踪事件的分布,在Micrometer中,对应的类是DistributionSummary(分发摘要)。它的使用方式和Timer十分相似,但是它的记录值并不依赖于时间单位。常见的使用场景:使用DistributionSummary测量命中服务器的请求的有效负载大小。

 /**
     * 只有内存存在(actuator无法查到,influx不会自动记录),用于系统会自动统计,一些最大值和平均值一些信息
     * @since JDK1.8
     * @author gsh456
     * @date 2020/1/13 10:52
     */
    public void distributionSummary1(){

        DistributionSummary summary = DistributionSummary.builder("summary")
                .tag("summary", "summarySample")
                .description("summary sample test")
                .register(new SimpleMeterRegistry());
        summary.record(2D);
        summary.record(3D);
        summary.record(4D);
        System.out.println(summary.count());
        System.out.println(summary.measure());
        System.out.println(summary.max());
        System.out.println(summary.mean());
        System.out.println(summary.totalAmount());
    }
    /**
     * 全局使用,自动记录到influxDb,同时actuator也可以查到
     * @since JDK1.8
     * @author gsh456
     * @date 2020/1/13 10:52
     */
    public void distributionSummary2(){
        DistributionSummary summary = Metrics.summary("summary2", "summary", "summarySample");
        summary.record(2D);
        summary.record(3D);
        summary.record(4D);
        System.out.println(summary.count());
        System.out.println(summary.measure());
        System.out.println(summary.max());
        System.out.println(summary.mean());
        System.out.println(summary.totalAmount());
    }

前面提到Meter主要包括:TimerCounterGaugeDistributionSummary,除了这四个还有他们的一些细节的分支,例如LongTaskTimerFunctionCounterFunctionTimerTimeGauge这些,由于这些并不是太常用下面就简单的说一下,有感兴趣的可以看一下,不感兴趣的略过。下面逐一分析它们的作用和个人理解的实际使用场景。

3.5 FunctionCounter

FunctionCounterCounter的特化类型,它把计数器数值增加的动作抽象成接口类型ToDoubleFunction,这个接口JDK1.8中对于Function的特化类型接口。FunctionCounter的使用场景和Counter是一致的,这里介绍一下它的用法:

 public static void main(String[] args) throws Exception {
            MeterRegistry registry = new SimpleMeterRegistry();
            AtomicInteger n = new AtomicInteger(0);
            //这里ToDoubleFunction匿名实现其实可以使用Lambda表达式简化为AtomicInteger::get
            FunctionCounter.builder("functionCounter", n, new ToDoubleFunction() {
                @Override
                public double applyAsDouble(AtomicInteger value) {
                    return value.get();
                }
            }).baseUnit("function")
                    .description("functionCounter")
                    .tag("createOrder", "CHANNEL-A")
                    .register(registry);
            //下面模拟三次计数      
            n.incrementAndGet();
            n.incrementAndGet();
            n.incrementAndGet();
        }

3.6 FunctionTimer

FunctionTimer是Timer的特化类型,它主要提供两个单调递增的函数(其实并不是单调递增,只是在使用中一般需要随着时间最少保持不变或者说不减少):一个用于计数的函数和一个用于记录总调用耗时的函数。

3.7 LongTaskTimer

LongTaskTimer也是一种Timer的特化类型,主要用于记录长时间执行的任务的持续时间,在任务完成之前,被监测的事件或者任务仍然处于运行状态,任务完成的时候,任务执行的总耗时才会被记录下来。LongTaskTimer适合用于长时间持续运行的事件耗时的记录,例如相对耗时的定时任务。在Spring应用中,可以简单地使用@Scheduled和@Timed注解,基于spring-aop完成定时调度任务的总耗时记录:

@Timed(value = "aws.scrape", longTask = true)
@Scheduled(fixedDelay = 360000)
void scrapeResources() {
    //这里做相对耗时的业务逻辑
}

3.8 TimeGauge

TimeGauge是Gauge的特化类型,相比Gauge,它的构建器中多了一个TimeUnit类型的参数,用于指定ToDoubleFunction入参的基础时间单位。

4. 自定义metrics

上面那些都是已经提供好的

4.1 DemoMetrics

public class DemoMetrics implements MeterBinder {
    private AtomicLong systemMemoryUsed = new AtomicLong(0);
    //这里实现了MeterBinder接口的bindTo方法,将要采集的指标注册到MeterRegistry
    @Override
    public void bindTo(MeterRegistry meterRegistry) {
    //这里的MeterRegistry 是全局的
Gauge.builder("system.memory.used",systemMemoryUsed, AtomicLong::get)
//              .tag("groupName", this.groupName)
                .description("系统已用内存(byte)")
                .register(registry);
}
    //定时器,定时改变内存数值
    @Scheduled(fixedRate = 1000)
    public void recordMemory(){
         //获取内存信息,省略
        //更改内存
        systemMemoryUsed.set(physicalUse);
     }

4.2 注册

@Bean
public DemoMetrics demoMetrics(){
    return new DemoMetrics();
}

4.4 验证

​ 验证很简单,打开 [1.3metrics网址](#1.3 metrics),看新的metric是否存在即可。

5. 与InfluxDb集成

micrometer已经默认集成了多种监控系统,所以只需要简单的配置即可。

5.1 maven引包

        
        
            org.springframework.boot
            spring-boot-starter-actuator
         
        
        
            io.micrometer
            micrometer-registry-influx
        

5.2 yml配置

# 开放健康检查接口
management:
   endpoints:
      web:
         exposure:
            include: "*"
   endpoint:
      health:
         show-details: ALWAYS
   metrics:
      export:
         influx:
            enabled: true
         #配置类InfluxProperties
            #数据库名称,如果不填默认为mydb
            db: mydb
            #数据库地址
            uri: http://ip:port
            #数据库用户名
            userName: userName
            #密码
            password: password
            #是否自动建表,默认值:true
            auto-create-db: true
            #ANY,  ONE,  QUORUM, ALL; 集群使用
            # ANY 写入任何一个节点,就返回成功(即使数据的所属文件和节点不一致,也算保存成功)
            # ONE 可以写入任何一个节点,就返回成功(必须数据所属文件和节点一致才算保存成功)
            # QUORUM 大多数节点返回成功即成功(例如 三个节点,两个节点保存成功,即可按成功返回)
            # ALL 所有节点返回成功才算成功 (例如有三个节点,所有节点保存成功,才算是成功)
            consistency: one
            #是否启用压缩
            compressed: true
            #数据保留策略
            retentionPolicy: autogen
            #Time period for which Influx should retain data in the current database. For instance 7d
            # 数据保留的时间,例如保留七天
            #retentionDuration:
            #How many copies of the data are stored in the cluster. Must be 1 for a single node instance.
            #节点的备份数据,单节点的必须为1
            #retentionReplicationFactor:
            #每一组文件,保存的时间范围
            #retentionShardDuration:
         # 配置类InfluxProperties的父类StepRegistryProperties属性
            #多长时间保存一次 默认1分钟
            step: 1m
            #连接超时时间(如果不填默认为1S)
            connect-timeout: 1s
            #读取超时时间(如果不填默认10S)
            read-timeout: 10s
            #线程数量(如果不填默认为2)
            numThreads: 2
            #The number of parameters or columns that can be queued within each call to Exec. Defaults to 10000
            #多少条数据存到数据库
            batchSize: 10000

只需要这两步即可完成influxDb与metrics的集成,全局的metrics数据会根据配置文件的规则(例如一分钟同步一次),自动创建influxDb表格和数据列,自动同步到InfluxDb中。另外在1.3中,我们也看到,虽然系统中我们 没有配置meter,但是也会有很多的metrics,这是micrometer自动创建的metric用于观察微服务运行的基本信息,像jvm使用内存这些,这些metrics也会自动同步到InfluxDb中去。

6. Metrics清理

​ 天下文章一大抄,这个问题困扰了我好久,终于在谷歌找到了答案,国内文章搜索相关东西根本找不到。查询官方文档,里面有metric.close()。根本达不到想要的效果。

​ micrometer清理,metrics删除,metric 失效 metric 有效期。

​ 为何会产生这个问题的主要原因是--metrics不会自动清理,存与内存之中,同步于influxDb之外,直至下次服务重启。例如 我创建了 一个 order.count 进行测试,metric 一分钟同步一次,每次同步之后自动清零,由于metric不会消失,所以同步好多为0的无效数据。

UTOOLS1588852383546.png

​ 所以要想使用埋点监控系统,这个问题必须解决。

​ 解决思路,获取所有metric,遍历他们,然后过滤出value为0的数据,然后让他从MeterRegistry清理掉。

@Component
@Configuration
@EnableScheduling
public class MetricCleanTimer {
    @Autowired
    MeterRegistry registry;
    //每60分钟执行一次
    @Scheduled(fixedDelay = 600000)
    public void clean() {
        List meters = registry.getMeters();
        List delete = new ArrayList<>();
        for (Meter meter : meters) {
            if (meter instanceof Counter) {
                //暂时只过滤counter的,因为counter太多
                Counter counter = (Counter) meter;
                double count = counter.count();
                if (count == 0.0) {
                    //如果为0 就加入清理列表
                    delete.add(meter);
                }
            }
        }
        if (delete.size() > 0) {
            delete.forEach(e -> registry.remove(e));
        }
    }
}

7参考文章

  1. Spring Boot Actuator:健康检查、审计、统计和监控
  2. micrometer自定义metrics
  3. 使用 Micrometer 记录 Java 应用性能指标
  4. 给你的SpringBoot做埋点监控--JVM应用度量框架Micrometer
  5. springboot2输出metrics到influxdb
  6. MICROMETER application monito
  7. Best Java code snippets using io.micrometer.core.instrument.MeterRegistry.remove

  1. meter的类型一共有8种,本文重点介绍4种。 ↩

你可能感兴趣的:(micrometer埋点(Spring boot 2.X metrics))