JVM应用度量框架MICROMETER实战
前提
spring-actuator做度量统计收集,使用Prometheus(普罗米修斯)进行数据收集,Grafana(增强ui)进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为”埋点”。SpringBoot中的依赖spring-actuator中集成的度量统计API使用的框架是Micrometer,官网是Micrometer.io。
在实践中发现了业务开发者滥用了Micrometer的度量类型Counter,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于Micrometer分析其他的度量类型API的作用和适用场景。
MICROMETER提供的度量类库
Meter是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为”米”或者”千分尺”,但是显然听起来都不是很合理,因此下文直接叫Meter,理解它为度量接口即可。Meter是由MeterRegistry创建和保存的,可以理解MeterRegistry是Meter的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个MeterRegistry的具体实现。
Micrometer中,Meter的具体类型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,FunctionCounter,FunctionTimer和TimeGauge。
下面分节详细介绍这些类型的使用方法和实战使用场景。而一个Meter具体类型需要通过名字和Tag(这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的Tag去区分多种维度进行数据统计。
METERREGISTRY
MeterRegistry在Micrometer是一个抽象类,主要实现包括:
- SimpleMeterRegistry:每个Meter的最新数据可以收集到SimpleMeterRegistry实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。
- CompositeMeterRegistry:多个MeterRegistry聚合,内部维护了一个MeterRegistry的列表。
- 全局的MeterRegistry:工厂类io.micrometer.core.instrument.Metrics中持有一个静态final的CompositeMeterRegistry实例globalRegistry。
当然,使用者也可以自行继承MeterRegistry去实现自定义的MeterRegistry。SimpleMeterRegistry适合做调试的时候使用,它的简单使用方式如下:
MeterRegistry registry = new SimpleMeterRegistry();
Counter counter = registry.counter(counter);
counter.increment();
CompositeMeterRegistry实例初始化的时候,内部持有的MeterRegistry列表是空的,如果此时用它新增一个Meter实例,Meter实例的操作是无效的
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);
。
在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请求调用统计,一般推荐的做法是:
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)));
像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。更多springboot实战文章:SpringBoot内容聚合
还有两点点需要注意:
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:)
.append(each.getId().getName())
.append(,tags:)
.append(each.getId().getTags())
.append(,type:).append(each.getId().getType())
.append(,value:).append(each.measure());
System.out.println(builder.toString());
});
}
private static void createOrder(Order order)