前提
最近线上的项目使用了 spring-actuator
做度量统计收集,使用 Prometheus
进行数据收集, Grafana
进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为"埋点"。 SpringBoot
中的依赖 spring-actuator
中集成的度量统计API使用的框架是 Micrometer
,官网是 micrometer.io
。在实践中发现了业务开发者滥用了 Micrometer
的度量类型 Counter
,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于 Micrometer
分析其他的度量类型API的作用和适用场景。全文接近3W字,内容比较干,希望能够耐心阅读,有所收获。
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
是一个抽象类,主要实现包括:
- 1、
SimpleMeterRegistry
:每个Meter
的最新数据可以收集到SimpleMeterRegistry
实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。 - 2、
CompositeMeterRegistry
:多个MeterRegistry
聚合,内部维护了一个MeterRegistry
的列表。 - 3、全局的
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");
对于不同的监控系统或者存储系统,命名会自动转换如下:
- 1、Prometheus - http_server_requests_duration_seconds。
- 2、Atlas - httpServerRequests。
- 3、Graphite - http.server.requests。
- 4、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.config().commonTags("stack", "prod", "region", "us-east-1"); // 和上面的意义是一样的 registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));
像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。
还有两点点需要注意:
- 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
就是一个增量为正数的单值计数器。这个举个很简单的使用例子:
MeterRegistry meterRegistry = new SimpleMeterRegistry(); Counter counter = meterRegistry.counter("http.request", "createOrder", "/