前言

对于持久运行的服务来说,进行有效地监控是很有必要的,采集监控数据是整个监控流程中相对麻烦的一点,服务编写者往往需要添加额外的代码来进行埋点,同时要适配监控数据存储与展示。本次介绍的Micrometer是一款监控数据采集、输出框架,其将自己称为监控界的SLF4J,提供了监控服务商(如适配监控数据存储的时序数据库或者提供完整解决方案的监控框架)无关的API门面。了解到它是因为在项目实践过程中采用了Spring-actuator这款监控工具,而其中就使用了Micrometer。Micrometer也将与Spring良好集成作为宣传点,SpringBoot相关的监控解决方案从SpringBoot 2.0开始全面更改为Micrometer,不过要知道Micrometer与Spring属于同门,都是Pivotal旗下的产品。

核心概念

Micrometer提供一组核心抽象API,对不同的监控系统有不同的实现,以此做到提供商无关,目前开箱支持AppOptics,Azure Monitor ,Netflix Atlas,CloudWatch ,Datadog,Dynatrace,Elastic,Ganglia,Graphite,Humio,Influx/Telegraf,JMX,KairosDB,New Relic,Prometheus,SignalFx,Google Stackdriver,StatsD,Wavefront。当然开发者也可以编写自定义实现来支持其他监控系统或者对上述系统进行定制。核心内容包含在micrometer-core包中,每种开箱提供都有对应的Jar包,直接引入即可使用。

Meter

Meter是度量的基接口,也是所有度量的抽象,Micrometer提供了一组原生Meter类型,包括Timer、Counter、Gauge、DistributionSummary、LongTaskTimer、FunctionCounter、FunctionTimer、TimeGauge,这些类型将在下一节详细描述。Meter由name和dimension来唯一标识,在Micrometer中dimension又等价于tag,Micrometer提供了Tag接口(因为Tag比较简短)。具体举例来说比如监控一个Http Server的API请求数量,对于接口A的Meter可以定义为name=http.requests, tag=uri:/api/a,desc=api-a,对于接口B的Meter可以定义为name=http.requests, tag=uri:/api/b,desc=api-b,也就是说名称应该是对类别的描述而tag是针对某项具体统计的描述,这样便于数据分组,既可以统计某类的总体情况也可以对单个数据进行分析。

Registry

Registry是Meter的注册中心,由Registry创建Meter并统一管理Meter,每个监控系统都有对应的MeterRegistry实现。Micrometer默认提供了基于内存、便于测试的SimpleRegistry以及一个静态全局注册表(Metrics.globalRegistry)。全局注册表是一个组合注册表(CompositeMeterRegistry ),当需要对接多个监控系统时,可以使用组合注册表并向其中添加多个Registry。

在Micrometer中Meter的名称规定为点分格式(domain.sub.one),而不同的监控系统可能有不同的命名格式,为此Registry可以配置命名转换规则(接口NamingConvention),自动将名称转换为不同的格式(如驼峰、下划线等)。

Meter Filter

每个注册表都可以配置Meter过滤器,在工作项目中引入不同的依赖或者配合不同的工具往往需要传递MeterRegistry或者直接使用全局静态Registry,为了方便开发者收集指定的数据,或者对注册的Meter进行转换修改,Micrometer提供了Meter过滤器用于编写规则拒绝、转换或配置注册的Meter。

Meter类型

下面逐一分析Micrometer提供的meter类型

Counter

正如字面意思:计数器,它允许你增加固定的数量,且数量必须为正数,也就是说它描述一个递增的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构造器创建
Counter counter = Counter
.builder("http.request")
.baseUnit("num") // optional
.description("a description of what this counter does") // optional
.tags("uri", "/order/create") // optional
.register(registry);
counter.increment();

// 直接从Registry创建
MeterRegistry meterRegistry = new SimpleMeterRegistry();
Counter counter = meterRegistry.counter("http.request", "uri", "/order/create");
counter.increment();

Function Counter

特化类型的计数器,Counter的值由某个对象执行某个方法提供,而无需手动调用counter.increment。通常来说主要是用于包装已经存在的计数器或者统计对象,方法需要是单调递增的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构造器创建
MeterRegistry registry = new SimpleMeterRegistry();
Cache cache = ...;
FunctionCounter.builder("evictions", cache,
c -> c.stats().evictionCount())
.baseUnit("num")
.description("functionCounter")
.tag("usage", "service")
.register(registry);

// 直接从Registry创建
Cache cache = ...; // suppose we have a Guava cache with stats recording on
registry.more().counter("evictions", tags, cache, c -> c.stats().evictionCount());

Timer

Timer稍微有点绕,Timer记录的是单个事件执行消耗的时间,用于测量事件发生的频率和效率,例如统计一个小时内某个接口有多少次请求以及每次请求响应时间的平均值时,就可以用到Timer。注意Timer在事件结束时记录数据,每条记录代表一个时间段,在统计时需要注意区间的取值,同时这也是和Long Task Timer的重要区别。Timer提供了很多记录数据的方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 构造器创建
Timer timer = Timer
.builder("my.timer")
.description("a description of what this timer does") // optional
.tags("region", "test") // optional
.register(registry);

// 直接从Registry创建
registry.timer("my.timer", "region", "test");

// record directly
timer.record(Duration.of(60L, ChronoUnit.SECONDS));
// record function
timer.record(() -> {
// some operation ...
});
// wrap function
timer.wrap(() -> yourFunction());
// use sample
Timer.Sample sample = Timer.start();
// do something
sample.stop(timer);

Function Timer

Timer的特化类型,与Function Counter类似,但是Function Timer由两个单调递增的函数组成,一个用于计数,一个用于统计总耗时。同样常用于包装已经存在的监控对象。Function Timer从侧面表现了Timer到底是在做什么操作,可以很明显地发现Timer包含了计数功能,在此基础之上增加了每个记录的耗时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 构造器创建
FunctionTimer.builder("cache.gets.latency", cache,
c -> c.getLocalMapStats().getGetOperationCount(),
c -> c.getLocalMapStats().getTotalGetLatency(),
TimeUnit.NANOSECONDS)
.tags("name", cache.getName())
.description("Cache gets")
.register(registry);

// 直接从Registry创建
registry.more().timer("cache.gets.latency", cache,
c -> c.getLocalMapStats().getGetOperationCount(),
c -> c.getLocalMapStats().getTotalGetLatency(),
TimeUnit.NANOSECONDS);

Long Task Timer

Long Task Timer同样也是Timer的特殊类型,但是其功能与Timer大相径庭。Long Task Timer统计的是当前有多少正在执行的任务,以及这些这任务已经耗费了多少时间,适用于监控长时间执行的方法,统计类似当前负载量的相关指标。Long Task Timer在事件开始时记录,在事件结束后将事件移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 构造器创建
LongTaskTimer longTaskTimer = LongTaskTimer
.builder("long.task.timer")
.description("a description of what this timer does") // optional
.tags("region", "test") // optional
.register(registry);

// 直接从Registry创建
LongTaskTimer scrapeTimer = registry.more().longTaskTimer("scrape");

// record function
scrapeTimer.record(() -> {
// some operation ...
});

// record by sample
LongTaskTimer.Sample start = scrapeTimer.start();
// do something
start.stop();

Gauge

Gauge可以理解为直接的数值指标,典型的例子是线程池的活跃线程数量、集合的大小等,当指标不是递增的而是一个上下浮动的值时,你应该采用Gauge,同时Gauge也翻译为仪表盘,典型如汽车的速度仪表,这样就非常好理解了。

1
2
3
4
5
6
7
8
9
10
// 构造器创建
Gauge gauge = Gauge
.builder("gauge", myObj, myObj::gaugeValue)
.description("a description of what this gauge does") // optional
.tags("region", "test") // optional
.register(registry);

// 直接从registry创建
registry.gauge("listGauge", Collections.emptyList(),
new ArrayList<>(), List::size);

Time Gauge

Time Gauge是一种特殊的Gauge,与Gauge功能相似,但是记录的内容是时间,本质上就是比普通Gauge多了一个时间单位属性。

1
2
3
4
5
6
7
8
9
// 构造器创建
TimeGauge timeGauge = TimeGauge.builder("timeGauge", count,
TimeUnit.SECONDS, AtomicInteger::get)
.tag("tagkey", "tagVal")
.register(registry);

// 直接从registry创建
registry.more().timeGauge("timeGauge", count,
TimeUnit.SECONDS, AtomicInteger::get);

Distribution Summary

Distribution Summary翻译为分布摘要,主要用于跟踪事件的分布,它的记录形式与Timer十分相似,但是记录的内容不依赖于时间单位,可以是任意数值,比如在监测范围内各个Http请求的响应内容大小时就可以使用Distribution Summary。为了更加明确地表明记录的内容,通常创建Distribution Summary时应该设置baseUnit属性。

1
2
3
4
5
6
7
8
9
10
11
// 构造器创建
DistributionSummary summary = DistributionSummary
.builder("response.size")
.description("a description of what this summary does") // optional
.baseUnit("bytes") // optional (1)
.tags("region", "test") // optional
.scale(100) // optional (2)
.register(registry);

// 直接从registry创建
DistributionSummary summary = registry.summary("response.size");

Cumulate与Step

对于一个完整的监控体系来说,通常至少会有三个部分:应用程序、监控数据存储、监控数据表现,而某些框架或者工具会同时包含其中的多个或者多个工具共同组成一个部分,从而产生各种各样的组合。对于速率、平均值、事件分布、延迟等与时间窗口相关的监控指标(Rate aggregation)可以在不同的部分进行处理,例如对于某个接口请求速度的监控,可以在应用层计算好直接发送速度值;也可以直接发送请求数量到存储层然后由表现层来计算速度;又或者是由应用层存储累加值,由其他工具主动来抓取每个时刻的状态。

所以在应用层,有的Meter会有两种类型:累加(Accumulate)与滚动(Step)。以Counter为例,该基接口在core包提供的默认实现中包括:CumulativeCounterStepCounter,源码并不复杂,直接列出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// CumulativeCounter底层就是一个DoubleAdder
public class CumulativeCounter extends AbstractMeter implements Counter {
private final DoubleAdder value;

public CumulativeCounter(Id id) {
super(id);
this.value = new DoubleAdder();
}

@Override
public void increment(double amount) {
value.add(amount);
}

@Override
public double count() {
return value.sum();
}
}

// Javadoc翻译:向监视系统报告间隔速率的计数器,count()方法将报告上一个完整周期的数值
// 而非整个生命周期的总数。可以查看StepDouble(StepValue)源码来查看滚动的具体实现
public class StepCounter extends AbstractMeter implements Counter {
private final StepDouble value;

public StepCounter(Id id, Clock clock, long stepMillis) {
super(id);
this.value = new StepDouble(clock, stepMillis);
}

@Override
public void increment(double amount) {
value.getCurrent().add(amount);
}

@Override
public double count() {
return value.poll();
}
}

此外还有Timer与Distribution Summary(它们两个行为基本一致)。对于它们来说,单纯地使用Cumulative模式基本上没什么意义,因为通常来说事件是频繁的而有价值的是每个时间段范围内的统计,这也导致在初步了解学习过程中可能会对Timer的功能产生疑惑,尤其是和其他没有Step性质的Meter放在一起理解的时候。

这两个概念在官方文档中描述地比较“玄乎”,但这对理解和使用Counter、Timer等是非常重要的,额外注意Counter的使用,因为确实有不少情况是要统计整个生命周期的计数值,这个时候如果你使用的是以Step为实现的Registry,就需要额外处理,避免从StepCounter抓取出局部范围内的累加值。

整合InfluxDB与Grafana

最后简单整合一下InfluxDB和Grafana,构建出一个完整的流程。当然,这个整合只是一个极其简单的示例,不过对于Meter的理解很有帮助。

InfluxDB

InfluxDB是一个开源的TSDB(时序数据库)使用Go语言编写。和传统数据库相比,influxDB在相关概念上有一定不同,具体对应如下:

influxDB 传统数据库中的概念
database 数据库
measurement 数据库中的表
point 表中的一行数据

每个point的数据结构由时间戳(time,固定)、标签(tags)、数据(fields)三部分组成,具体含义如下:

point 属性 含义
time 数据记录的时间,是主索引(自动生成)
tags 各种有索引的属性,可以认为是一种分类标识
fields 各种value值(没有索引的属性),表示具体数值

InfluxDB提供了类SQL的操作语法,熟悉SQL的情况下很容上手,具体使用方法请查看相关文档。

为了快速搭建一个InfluxDB,直接使用官方Docker镜像是最为便捷的,当然其他安装方式也很方便,照着官方文档来就可以。访问DockerHub搜索InfluxDB,选择一个版本(以1.8.1为例),执行如下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ docker pull influxdb:1.8.1
# 进入存放Influxdb数据的目录
$ cd ...
# 初始化数据库db0和管理员账号(仅供测试使用)
$ docker run --rm \
-e INFLUXDB_DB=db0 \
-e INFLUXDB_ADMIN_USER=admin -e INFLUXDB_ADMIN_PASSWORD=yourpassword \
-e INFLUXDB_USER=telegraf -e INFLUXDB_USER_PASSWORD=yourpassword \
-v $PWD:/var/lib/influxdb \
influxdb:1.8.1 /init-influxdb.sh

# 运行容器
$ docker run -p 8086:8086 \
-v $PWD:/var/lib/influxdb \
--name influxdb \
influxdb:1.8.1

# 进入influxdb的CLI/SHELL
$ docker exec -it influxdb influx

这样就完成了一个InfluxDB的安装(没有开启用户认证),可以先随便自由使用一番熟悉熟悉。

Grafana

Grafana是一个跨平台的开源的度量分析和可视化工具,可以通过查询采集的数据然后可视化的展示,并且能够设置报警通知。Grafana原生支持各种主流的数据源,拥有强大、美观的图表以及丰富的定制功能,是主流的监控数据可视化平台。可以直接访问 https://play.grafana.org/ 来进行体验。

同样Grafana的安装也很方便,这里还是选择使用Docker来快速构建服务,以7.1.1版本为例,执行如下命令:

1
2
$ docker pull grafana/grafana:7.1.1
$ docker run -d -p 3000:3000 --name grafana grafana/grafana:7.1.1

打开浏览器访问Grafana所在服务器的3000端口就可以看到Grafana界面,默认的用户名和密码为admin/admin。

简单整合

安装好两个组件后,编写一段简单的代码来展示一下效果。新建一个Maven项目,POM文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>your group id</groupId>
<artifactId>your artifact id</artifactId>
<version>your version</version>

<description>Demo of micrometer</description>

<properties>
<encoding>UTF-8</encoding>
<java.source>1.8</java.source>
<java.target>1.8</java.target>
</properties>

<dependencies>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.5.3</version>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-influx</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.source}</source>
<target>${java.target}</target>
<encoding>${encoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>

</project>

新建一个类编写Main方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public static void main(String[] args) {
InfluxMeterRegistry registry = InfluxMeterRegistry.builder(new InfluxConfig() {
@Override
public String db() {
return "db0";
}

@Override
public String userName() {
return "your user name";
}

@Override
public String password() {
return "your password";
}

@Override
public String uri() {
return "http://your influx db http url";
}

@Override
public Duration step() {
return Duration.ofSeconds(10);
}

@Override
public String get(String key) {
return null;
}
}).build();

Metrics.addRegistry(registry);

Counter counter = Metrics.counter("test.random.count", "tag1", "val1", "tag2", "val2");
Random random = new Random();

System.out.println("Start run ...");

ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
logger.info("Add counter ...");
counter.increment(random.nextInt(100));
},
2L, 2L, TimeUnit.SECONDS);

LockSupport.parkUntil(System.currentTimeMillis() + 101 * 1000);
System.out.println("Run over ...");
executor.shutdown();

registry.close();
}

这段代码每2秒给一个Counter(默认的InfluxDB Registry采用的是StepCounter)增加一次0~100的随机值,每10秒向InfluxDB发送一次数据,应该持续1000秒。可以先执行一下,然后查看InfluxDB中的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Connected to http://localhost:8086 version 1.8.1
InfluxDB shell version: 1.8.1

> use db0
Using database db0

> show measurements
name: measurements
name
----
test_random_count

> select * from test_random_count
name: test_random_count
time metric_type tag1 tag2 value
---- ----------- ---- ---- -----
1596166781072000000 counter val1 val2 195
1596166791046000000 counter val1 val2 291
1596166801047000000 counter val1 val2 305
1596166811047000000 counter val1 val2 343
...

可以看到InfluxDB中Meter的名称被自动转为了Snake格式,除了显式指定的两个tag之外还默认添加了metric_type这一个类型tag,此外只有一个名为value的field,也就是Counter的值。关于数据是如何转换发送的可以查看io.micrometer.influx.InfluxMeterRegistry#publish方法的源码,可以看到每种Meter的数据是如何读取并发送的,查看该源码对于理解各类Meter很有帮助。

在InfluxDB中有数据之后,打开Grafana,添加数据源,然后选择InfluxDB,填写HTTP url、用户名密码以及database名称(示例中为db0)

添加完数据源后点击create并添加Panel

按图配置即可得到图示效果