Micrometer:别再吭哧吭哧地写代码实现接口耗时90,95,99线了!老王,快给你的 Spring Boot 做个埋点监控吧!

点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

每天 14:00 更新文章,每天掉亿点点头发...

源码精品专栏

 

原创 | Java 2021 超神之路,很肝~

中文详细注释的开源项目

RPC 框架 Dubbo 源码解析

背景

领导:小白,把咱们关键接口的耗时都记录下来,展示成一张图,让我们自己或者别人了解我们的接口耗时情况,有耗时的90,95,99线!

网络应用框架 Netty 源码解析

消息中间件 RocketMQ 源码解析

数据库中间件 Sharding-JDBC 和 MyCAT 源码解析

小白:好的,保证完成任务!预估两周才能开发完成上线。

领导:这个小功能要那么久?我刚问了大牛,他说一天就可以开发完成了。

小白:领导,这个需求既要设计数据表,也要埋点,还要前端展示,和前端开发联调。没有两周正拿不下来!

作业调度中间件 Elastic-Job 源码解析

领导:那这样,你跟着大牛学学,看他怎么那么快搞定的!

于是,小白找到大牛,将领导的需求告诉大牛,说明了自己的开发设计,并说明完成这些开发加班加点也要两周才能上线出效果。大牛笑笑说,其实不用这么麻烦,可以利用已有的资源剩下很多工作:比如不用开发前端,而是直接利用我们现有的grafana画图展示;不用存储到数据库,而是利用prometheus采集这些信息;甚至不用写代码实现计时器,只需要利用Micrometer的@Timed注解来完成计时和90,95,99线就可以了。

分布式事务中间件 TCC-Transaction 源码解析

Eureka 和 Hystrix 源码解析

Java 并发源码

Micrometer实现接口耗时实例

1.添加依赖pom.xml

来源:cnblogs.com/rolandlee/p/11343848.html

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Contains aspectj - required by TimedAspect -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>

其中actuator是spring-boot提供的metric信息采集基础设施,aop是切面依赖,用来使用micrometer的@Timed注解,micrometer-registry-prometheus是micrometer注册到prometheus的插件。

JVM应用度量框架Micrometer实战前提Micrometer提供的度量类库MeterRegistryTag与Meter的命名Meters基于SpirngBoot、Prometheus、Grafana集成SpirngBoot中使用MicrometerPrometheus的安装和配置Grafana的安装和使用

Micrometer:别再吭哧吭哧地写代码实现接口耗时90,95,99线了!老王,快给你的 Spring Boot 做个埋点监控吧!(图1)

2.在启动类配置切面,以便利用@Timed注解


JVM应用度量框架Micrometer实战 前提

spring-actuator做度量统计收集,使用Prometheus(普罗米修斯)进行数据收集,Grafana(增强ui)进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为”埋点”。SpringBoot中的依赖spring-actuator中集成的度量统计API使用的框架是Micrometer,官网是Micrometer.io。

在实践中发现了业务开发者滥用了Micrometer的度量类型Counter,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于Micrometer分析其他的度量类型API的作用和适用场景。

@SpringBootApplication
public class SpringBootWithMetricsApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootWithMetricsApplication.class, args);
}

/**
* This is required so that we can use the @Timed annotation
* on methods that we want to time.
* See: https://micrometer.io/docs/concepts#_the_timed_annotation
*/
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}

}

3.在要计时的类内使用注解@Timed完成接口耗时信息采集

Micrometer提供的度量类库

Meter是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为”米”或者”千分尺”,但是显然听起来都不是很合理,因此下文直接叫Meter,理解它为度量接口即可。Meter是由MeterRegistry创建和保存的,可以理解MeterRegistry是Meter的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个MeterRegistry的具体实现。

Micrometer中,Meter的具体类型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,FunctionCounter,FunctionTimer和TimeGauge。

@RestController
public class GreetingController {

private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();

/*
* private final MeterRegistry registry;
*
*//**
* We inject the MeterRegistry into this class
*//*
* public GreetingController(MeterRegistry registry) { this.registry = registry;
* }
*/

/**
* The @Timed annotation adds timing support, so we can see how long it takes to
* execute in Prometheus percentiles
*/
@GetMapping("/greeting")
@Timed(description = "Time taken to return greeting", percentiles = { 0.5, 0.90 })
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
Random rand =new Random(20);
int i=rand.nextInt(1000);
Thread.sleep(Long.parseLong(""+i));
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}

}

启动应用后,可以在浏览器端查看采集的接口信息

下面分节详细介绍这些类型的使用方法和实战使用场景。而一个Meter具体类型需要通过名字和Tag(这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的Tag去区分多种维度进行数据统计。

http://localhost:9080/actuator/prometheus

MeterRegistry

MeterRegistry在Micrometer是一个抽象类,主要实现包括:

SimpleMeterRegistry:每个Meter的最新数据可以收集到SimpleMeterRegistry实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。CompositeMeterRegistry:多个MeterRegistry聚合,内部维护了一个MeterRegistry的列表。全局的MeterRegistry:工厂类io.micrometer.core.instrument.Metrics中持有一个静态final的CompositeMeterRegistry实例globalRegistry。

当然,使用者也可以自行继承MeterRegistry去实现自定义的MeterRegistry。SimpleMeterRegistry适合做调试的时候使用,它的简单使用方式如下:

# HELP method_timed_seconds Time taken to return greeting
# TYPE method_timed_seconds summary
method_timed_seconds{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",quantile="0.5",} 8.6016E-5
method_timed_seconds{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",quantile="0.9",} 1.59744E-4
method_timed_seconds{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",quantile="0.95",} 1.67936E-4
method_timed_seconds{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",quantile="0.99",} 0.018870272
method_timed_seconds_count{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",} 20.0
method_timed_seconds_sum{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",} 0.0203684
# HELP method_timed_seconds_max Time taken to return greeting
# TYPE method_timed_seconds_max gauge
method_timed_seconds_max{class="com.tutorialworks.demos.springbootwithmetrics.GreetingController",exception="none",method="greeting",} 0.0184612Prometheus采集接口耗时信息

Prometheus是一个内存中的维度时间序列数据库,具有简单的内置UI、定制查询语言和数学操作。Prometheus的设计是基于pull模型进行操作,根据服务发现定期从应用程序实例中抓取指标。

安装Prometheus

MeterRegistry registry = new SimpleMeterRegistry();
Counter counter = registry.counter("counter");
counter.increment();

CompositeMeterRegistry实例初始化的时候,内部持有的MeterRegistry列表是空的,如果此时用它新增一个Meter实例,Meter实例的操作是无效的

windows下,下载地址:https://prometheus.io/download/,版本无要求,选择最新即可。

prometheus-2.28.1.windows-amd64.zip

解压,修改配置文件

CompositeMeterRegistry composite = new CompositeMeterRegistry();

Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // <- 实际上这一步操作是无效的,但是不会报错

SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple);  // <- 向CompositeMeterRegistry实例中添加SimpleMeterRegistry实例

compositeCounter.increment();  // <-计数成功

全局的MeterRegistry的使用方式更加简单便捷,因为一切只需要操作工厂类Metrics的静态方法:

Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = Metrics.counter("counter", "tag-1", "tag-2");
counter.increment();
Tag与Meter的命名

Micrometer中,Meter的命名约定使用英文逗号(dot,也就是”.”)分隔单词。但是对于不同的监控系统,对命名的规约可能并不相同,如果命名规约不一致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。

Micrometer中使用英文逗号分隔单词的命名规则,再通过底层的命名转换接口NamingConvention进行转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖NamingConvention实现自定义的命名转换规则:registry.config().namingConvention(myCustomNamingConvention);。

配置信息如下:

- job_name: 'spring-actuator'
metrics_path: '/actuator/prometheus'
scrape_interval: 5s
static_configs:
- targets: ['localhost:9080']

此时,可以在浏览器端使用PQL查询接口耗时信息:

在Micrometer中,对一些主流的监控系统或者存储系统的命名规则提供了默认的转换方式,例如当我们使用下面的命名时候:

MeterRegistry registry = ...
registry.timer("http.server.requests");

对于不同的监控系统或者存储系统,命名会自动转换如下:

Prometheus - http_server_requests_duration_seconds。Atlas - httpServerRequests。Graphite - http.server.requests。InfluxDB - http_server_requests。

其实NamingConvention已经提供了5种默认的转换规则:dot、snakeCase、camelCase、upperCamelCase和slashes。

另外,Tag(标签)是Micrometer的一个重要的功能,严格来说,一个度量框架只有实现了标签的功能,才能真正地多维度进行度量数据收集。Tag的命名一般需要是有意义的,所谓有意义就是可以根据Tag的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。

假设我们需要监控数据库的调用和Http请求调用统计,一般推荐的做法是:

http://localhost:9090/

Micrometer:别再吭哧吭哧地写代码实现接口耗时90,95,99线了!老王,快给你的 Spring Boot 做个埋点监控吧!(图2)

Grafana展示接口耗时信息

Grafana支持多种不同的时序数据库数据源,Grafana对每种数据源提供不同的查询方法,而且能很好的支持每种数据源的特性。

method_timed_seconds{quantile="0.5",method="greeting"}

MeterRegistry registry = ...
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")

这样,当我们选择命名为”database.calls”的计数器,我们可以进一步选择分组”db”或者”users”分别统计不同分组对总调用数的贡献或者组成。一个反例如下:

MeterRegistry registry = ...
registry.counter("calls",
    "class", "database",
    "db", "users");

registry.counter("calls",
    "class", "http",
    "uri", "/api/users");

通过命名”calls”得到的计数器,由于标签混乱,数据是基本无法分组统计分析,这个时候可以认为得到的时间序列的统计数据是没有意义的。可以定义全局的Tag,也就是全局的Tag定义之后,会附加到所有的使用到的Meter上(只要是使用同一MeterRegistry),全局的Tag可以这样定义:

MeterRegistry registry = ...
registry.counter("calls",
    "class", "database",
    "db", "users");

registry.counter("calls",
    "class", "http",
    "uri", "/api/users");


MeterRegistry registry = ...
registry.config().commonTags("stack", "prod", "region", "us-east-1");
// 和上面的意义是一样的
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));

像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。

method_timed_seconds{quantile="0.95",method="greeting"}

method_timed_seconds{quantile="0.99",method="greeting"}

还有两点点需要注意:

Micrometer:别再吭哧吭哧地写代码实现接口耗时90,95,99线了!老王,快给你的 Spring Boot 做个埋点监控吧!(图3)

总结

   在日常开发中,常常需要监控某些关键接口响应情况,比较容易想到的是记录日志或者写到数据库中,但因日志和数据库查看不直观,也不利于监控报警。Micrometer 为 Spring boot应用上的性能数据收集提供了一个通用的 API,它提供了多种度量指标类型(Timers、Guauges、Counters等),同时支持接入不同的监控系统,例如 Influxdb、Graphite、Prometheus 等。可以通过 Micrometer 收集 Java 性能数据,配合 Prometheus 监控系统实时获取数据,并最终由 Grafana 展示出来,从而很容易实现应用的监控。

1、Tag的值必须不为null。

2、Micrometer中,Tag必须成对出现,也就是Tag必须设置为偶数个,实际上它们以Key=Value的形式存在,具体可以看io.micrometer.core.instrument.Tag接口:

public interface Tag extends Comparable<Tag> {
    String getKey();

    String getValue();

    static Tag of(String key, String value) {
        return new ImmutableTag(key, value);
    }

    default int compareTo(Tag o) {
        return this.getKey().compareTo(o.getKey());
    }
}

当然,有些时候,我们需要过滤一些必要的标签或者名称进行统计,或者为Meter的名称添加白名单,这个时候可以使用MeterFilter。MeterFilter本身提供一些列的静态方法,多个MeterFilter可以叠加或者组成链实现用户最终的过滤策略。例如:

MeterRegistry registry = ...
registry.config()
    .meterFilter(MeterFilter.ignoreTags("http"))
    .meterFilter(MeterFilter.denyNameStartsWith("jvm"));

表示忽略”http”标签,拒绝名称以”jvm”字符串开头的Meter。更多用法可以参详一下MeterFilter这个类。

Meter的命名和Meter的Tag相互结合,以命名为轴心,以Tag为多维度要素,可以使度量数据的维度更加丰富,便于统计和分析。

Meters

前面提到Meter主要包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,FunctionCounter,FunctionTimer和TimeGauge。下面逐一分析它们的作用和个人理解的实际使用场景(应该说是生产环境)。

Counter

Counter是一种比较简单的Meter,它是一种单值的度量类型,或者说是一个单值计数器。Counter接口允许使用者使用一个固定值(必须为正数)进行计数。准确来说:Counter就是一个增量为正数的单值计数器。这个举个很简单的使用例子:

使用场景:

Counter的作用是记录XXX的总量或者计数值,适用于一些增长类型的统计,例如下单、支付次数、Http请求总量记录等等,通过Tag可以区分不同的场景,对于下单,可以使用不同的Tag标记不同的业务来源或者是按日期划分,对于Http请求总量记录,可以使用Tag区分不同的URL。用下单业务举个例子:

//实体
@Data
public class Order {

    private String orderId;
    private Integer amount;
    private String channel;
    private LocalDateTime createTime;
}


public class CounterMain {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    static {
            Metrics.addRegistry(new SimpleMeterRegistry());
        }

        public static void main(String[] args) throws Exception {
            Order order1 = new Order();
            order1.setOrderId("ORDER_ID_1");
            order1.setAmount(100);
            order1.setChannel("CHANNEL_A");
            order1.setCreateTime(LocalDateTime.now());
            createOrder(order1);
            Order order2 = new Order();
            order2.setOrderId("ORDER_ID_2");
            order2.setAmount(200);
            order2.setChannel("CHANNEL_B");
            order2.setCreateTime(LocalDateTime.now());
            createOrder(order2);
            Search.in(Metrics.globalRegistry).meters().forEach(each -> {
                StringBuilder builder = new StringBuilder();
                builder.append("name:")
               &nb

标签: 时序计时器

随便看看