JohnShen's Blog.

[指标监控] JVM 指标框架 Micrometer

字数统计: 2.8k阅读时长: 12 min
2019/11/29 Share

作为 Micrometer + Prometheus + Grafana 的开篇,介绍Micrometer的基础应用。当前项目与数据库集打交道,但目前因为没有相关指标监控,项目运行情况一直是个黑盒。对于接口调用情况、连接池配置情况、性能情况若都是手动分析日志,则必然是不可行的。所以想要基于这三者搭建一台指标监控体系。

接触到Micrometer还是因为Spring Boot Actuator,在 Spring Boot 2.0之后,Micrometer 已经是 Actuator 的默认实现。文中默认有 Actuator 以及 Prometheus 的依赖,Actuator 在 1.x 和 2.x 区别较大,本文使用的版本是 2.1.7,且默认management.endpoints.web.exposure.include中包含 prometheus。

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

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

Metrics可以翻译成度量或者指标,这两者其实并无太大区别,只是各适用于特定场景而已,本文中统一叫作指标

介绍

Micrometer首页这么介绍自己:

Micrometer provides a simple facade over the instrumentation clients for the most popular monitoring systems, allowing you to instrument your JVM-based application code without vendor lock-in. Think SLF4J, but for metrics.

不同的监控体系有不同的数据收集、命名规约、三方依赖,Micrometer 的定位就是监控系统的 Facade,类比于日志系统的 Slf4j,Micrometer 可以很好地完成针对不同监控系统的适配与切换,使指标数据可移植性最大化。在Maven仓库中,可以看到各类 Registry (mvn, github)。

Registry

Meter是 Micrometer 最基础的接口,使用 Micrometer 时最常用的 TimerCounterGuageDistributionSummary就是继承自该接口,收集的系统数据就是用 Meter 来表示。Meter 在 Micrometer 中由 MeterRegistry创建并保存,每个支持的监控系统都有一个MeterRegistry实现(Prometheus的实现是PrometheusMeterRegistry)。如果事先没有指定监控系统,Micrometer 默认生成一个将 Meter 最新数据保留在内存的SimpleMeterRegistry

SimpleMetricsExportAutoConfiguration中就可以看到 SimpleMeterRegistry 的配置,该类包含: @ConditionalOnMissingBean(MeterRegistry.class)。

1
2
3
4
SimpleMeterRegistry simple = new SimpleMeterRegistry();
Counter counter = simple.counter("counter");
counter.increment();
System.out.println(counter.count()); // 1.0

除了作为基础实现的SimpleMeterRegistry外,Micrometer 提供CompositeMeterRegistry,可将多个 MeterRegistry 加入其中,使 Meter 数据同时发送到不同监控系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CompositeMeterRegistry composite = new CompositeMeterRegistry();
Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // no op
System.out.println(compositeCounter.count()); // 0.0

SimpleMeterRegistry simple1 = new SimpleMeterRegistry();
composite.add(simple1);
SimpleMeterRegistry simple2 = new SimpleMeterRegistry();
composite.add(simple2);

compositeCounter.increment();

System.out.println(simple1.counter("counter").count()); // 1.0
System.out.println(simple2.counter("counter").count()); // 1.0

此外,Micrometer 还持有一个Global MeterRegistry:Metrics.globalRegistry,其也是一个 CompositeMeterRegistry。如果没有引入 Prometheus 的依赖,Metrics.globalRegistry 中包含的就是 SimpleMeterRegistry,MeterRegistryPostProcessor会将生成的系统初始生成的 SimpleMeterRegistry 注入进全局 Registry 中。如果引入了 Prometheus 的依赖,注入进全局 Registry 的就是 PrometheusMeterRegistry。

1
2
3
4
5
6
7
8
public class Metrics {
public static final CompositeMeterRegistry globalRegistry = new CompositeMeterRegistry();

public static void addRegistry(MeterRegistry registry) {
globalRegistry.add(registry);
}
//...
}

命名

Meter

Micrometer 中规定单词用.(dot)隔开,而不同监控系统的命名规范并不一致,例如在 Micrometer 中可以这样命名一个 Meter:

1
registry.timer("http.server.requests");

在不同监控系统中则应该是:

system name
Prometheus http_server_requests_duration_seconds
Atlas httpServerRequests
Graphite http.server.requests
InfluxDB http_server_requests

所以需要一种命名转换体系将 Micrometer 中的命名转为特定系统的规范命名,Micrometer 提供了 NamingConvention供各系统实现,开发者可以使用以下方式自定义命名规则:

1
registry.config().namingConvention(myCustomNamingConvention);

Tag

既然有了指标,则必然包含维度的概念,只有使用不同的维度才能对指标进行更好的区分和收集。Micrometer 中 Tag 就扮演了这样的角色。

1
2
registry.counter("database.calls", "db", "users") // database.calls即为自行定义的指标, 而维度就是db, 含义即为db为users的数据库调用次数
registry.counter("http.requests", "uri", "/api/users") // uri为/api/users的http请求次数

Tag 参数必须以 TagKey=TagValue 的方式成对出现。当命名 Tag 时,也建议使用和 Meter 一样的 dot 命名方式。同时,Tag value 必须是非空。

此外,还有一种Common tags,对于每种 Meter,都会将 Tag 加入其中,一般用于指定 IP、实例、地区等信息,便于数据收集后进行区分。

1
registry.config().commonTags("stack", "prod", "region", "us-east-1");

Meter filters

MeterRegistry 提供 Meter Filter,可以控制 Meter 的注册以及忽略特定统计行为。官方示例如下:

1
2
3
registry.config()
.meterFilter(MeterFilter.ignoreTags("too.much.information"))
.meterFilter(MeterFilter.denyNameStartsWith("jvm"));

针对Filter进行简单的试验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SimpleMeterRegistry simple1 = new SimpleMeterRegistry();
simple1.config()
.meterFilter(MeterFilter.ignoreTags("db2", "db3"))
.meterFilter(MeterFilter.denyNameStartsWith("jvm"));
Counter c1 = simple1.counter("database.calls", "db1", "users");
Counter c2 = simple1.counter("database.calls", "db2", "users");
Counter c3 = simple1.counter("database.calls", "db3", "users");
Counter c4 = simple1.counter("jvm.requests", "uri", "/api/users");
c1.increment();
c2.increment();
c3.increment();
c4.increment();

List<Meter> meters = simple1.getMeters();
for (Meter meter : meters) {
System.out.println(meter.getId() + " " + meter.measure());
}

其输出结果为:

1
2
MeterId{name='database.calls', tags=[]} [Measurement{statistic='COUNT', value=2.0}]
MeterId{name='database.calls', tags=[tag(db1=users)]} [Measurement{statistic='COUNT', value=1.0}]

可以看见jvm.requests直接被过滤了,而 Tag db2db3由于被忽略了,其值添加到了name='database.calls', tags=[]中,其 count值为 2.0。

Meter filters 有很多复杂的功能,此处就不展开了。

Meter类别

Counter

Counter 是最简单的 Meter,其含义即为单值递增计数器,值必须是正数。

对于每种 Meter 而言,各有一套 fluent api 创建实例,Counter 的示例如下:

1
2
3
4
5
6
7
Counter counter = Counter
.builder("counter")
.baseUnit("beans") // optional
.description("a description of what this counter does") // optional
.tags("region", "test") // optional
.register(registry);
counter.increment();

FunctionCounter实际上和Counter本身差别不大,只是将计数的行为抽象成 ToDoubleFunction 接口。 比如上下文中有一个 AtomicInteger ,那么 FunctionCounter 可以直接使用这个原子类来完成相关计数操作。这样的好处是对程序上下文而言,不需要感知该 Meter 的存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MeterRegistry registry = new SimpleMeterRegistry();
AtomicInteger ai = new AtomicInteger(0);
FunctionCounter counter = FunctionCounter.builder("counter", ai, AtomicInteger::get)
.description("functionCounterTest")
.tag("region", "ABC")
.register(registry);
ai.incrementAndGet();
ai.incrementAndGet();
ai.incrementAndGet();

System.out.println(ai.get()); // 3
System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=3.0}]

ai.set(100);
System.out.println(ai.get()); // 100
System.out.println(counter.measure()); // [Measurement{statistic='COUNT', value=100.0}]

Guage

记录指标的当前值,其典型应用是集合或Map的大小、线程的数量、CPU或内存情况。Guages 主要适用于有上边界的指标,要将其和 Counter 区分开。其Fluent Api 接口同样是将计数逻辑交给 ToDoubleFunction 接口。

1
2
3
4
5
6
7
8
9
List<String> list = new ArrayList<>();
Gauge gauge = Gauge
.builder("gauge", list, List::size)
.description("description") // optional
.tags("region", "test") // optional
.register(registry);
System.out.println(gauge.measure()); // [Measurement{statistic='VALUE', value=0.0}]
list.add("");
System.out.println(gauge.measure()); // [Measurement{statistic='VALUE', value=1.0}]

官方文档中还提供了用于观察数值、集合、Map的方法:

1
2
3
4
5
6
// <T> T gauge(String name, Iterable<Tag> tags, @Nullable T obj, ToDoubleFunction<T> valueFunction)
List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size);
// <T extends Collection<?>> T gaugeCollectionSize(String name, Iterable<Tag> tags, T collection)
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>());
// <T extends Map<?, ?>> T gaugeMapSize(String name, Iterable<Tag> tags, T map)
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());

Micrometer 默认是不会创建对象的强引用(可以观察抽象类 MeterRegistry 的 newGauge 具体实现),如果 Guage 值无法观察到,那可能是对象已被执行垃圾回收。

Timer

Timer 用于记录时间相对较短的事件延迟情况和事件发生的频率,Timer 的所有实现至少记录了事件总时间消耗以及次数。以记录请求延时情况为例,由于可能瞬时记录大量信息,所以Timer每秒将更新很多次。

1
2
3
4
5
6
7
8
9
Timer timer = Timer
.builder("my.timer")
.description("a description of what this timer does") // optional
.tags("region", "test") // optional
.register(registry);
timer.record(12, TimeUnit.SECONDS);
timer.record(2, TimeUnit.SECONDS);
timer.record(23, TimeUnit.SECONDS);
System.out.println(timer.measure()); // [Measurement{statistic='COUNT', value=3.0}, Measurement{statistic='TOTAL_TIME', value=37.0}, Measurement{statistic='MAX', value=23.0}]

Timer 可以记录代码的执行耗时,其接收 Runnable 以及 Callable 参数。这个可以按照实际需要决定是否需要,个人不喜欢将执行代码的线程更换掉(还得考虑ThreadLocal及其他上下文情况),但不否认其拥有应用场景。

1
2
3
4
5
timer.record(() -> dontCareAboutReturnValue());
timer.recordCallable(() -> returnValue());

Runnable r = timer.wrap(() -> dontCareAboutReturnValue());
Callable c = timer.wrap(() -> returnValue());

Timer 还有个内部类Timer.Sample,可以通过手动调用 start 和 stop 记录执行情况:

1
2
3
4
Timer.Sample sample = Timer.start(registry);
// do stuff
Response response = ...
sample.stop(registry.timer("my.timer", "response", response.status()));

Distribution summaries

distribution summary 用来追踪事件的分布情况,其结构和 Timer 类似,但是其值不是由时间单位表示,也可以认为 Timer 只是特例化的 distribution summary ,但是 Micrometer 会针对监控系统自动适配存放时间序列的基本单位,所以只要是关于时间的指标,官方建议都使用 Timer。

官方示例的 Fluent API 使用如下:

1
2
3
4
5
6
7
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);

添加 baseUnit 可以进一步提升整体的可移植性,因为 baseUnit 本身是监控系统命名转换的一部分,但如果省略它,也不会有负面影响。scale 作为比例因子,用来乘以所有的记录值。

Timers 和 distribution summaries 都支持收集数据后观察比例分布信息,有两种使用形式:一是直方图(histogram),即分配一堆桶,观察数据在各个桶的分布情况;二是百分比(percentiles),系统会为每一个 Meter 计算一个百分比近似值。以下面这个分布摘要在 Prometheus 中的展示为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DistributionSummary summary = DistributionSummary.builder("ds.test")
.description("simple distribution summary")
.publishPercentiles(0.5, 0.6, 0.95)
.publishPercentileHistogram()
.minimumExpectedValue(1L)
.maximumExpectedValue(30L)
.register(meterRegistry);
summary.record(2);
summary.record(2);
summary.record(2);
summary.record(2);
summary.record(2);
summary.record(5);
summary.record(5);
summary.record(5);
summary.record(5);
summary.record(9);

除了 max / count / sum 这三种数据外,还会显示 quantile 信息以及直方图的桶信息。publishPercentiles即表示记录百分位数值,publishPercentileHistogram表示记录直方图信息,而桶的个数可以由minimumExpectedValuemaximumExpectedValue控制。

此外,还有一个sla方法,包含的桶信息会包含 sla 指定的数值。举例而言,下方的图中就包含了 18 和 19 两个桶。

1
2
3
4
5
6
7
8
DistributionSummary summary = DistributionSummary.builder("ds.test")
.description("simple distribution summary")
.publishPercentiles(0.5, 0.6, 0.95)
.publishPercentileHistogram()
.minimumExpectedValue(1L)
.maximumExpectedValue(30L)
.sla(18, 19)
.register(meterRegistry);

Timer 的 Histogram 和 Percentiles 使用也完全类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Timer timer = Timer.builder("my.timer")
.publishPercentiles(0.5, 0.95)
.publishPercentileHistogram()
.sla(Duration.ofMillis(200))
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofSeconds(1))
.register(meterRegistry);
timer.record(8, TimeUnit.MILLISECONDS);
timer.record(9, TimeUnit.MILLISECONDS);
timer.record(7, TimeUnit.MILLISECONDS);
timer.record(8, TimeUnit.MILLISECONDS);
timer.record(58, TimeUnit.MILLISECONDS);
timer.record(56, TimeUnit.MILLISECONDS);
timer.record(157, TimeUnit.MILLISECONDS);
timer.record(299, TimeUnit.MILLISECONDS);
timer.record(599, TimeUnit.MILLISECONDS);
timer.record(1999, TimeUnit.MILLISECONDS);

其他

简单吐槽一下,相同名称、不同 Tag 的数据不能聚和,其设计初衷可能就是让数据由上层应用处理。 Actuator 的 /metrics/logback.events 接口,对应的 Meter 是LogbackMetrics,可以看到内部 Meter 命名相同,但 Tag 不同。但是这个 uri 显示的 measurements 是聚和的,开始以为有没发现的 API 操作,查看MetricsEndpoint后发现,聚和操作也是由该类自己完成的。


Reference

https://micrometer.io/docs/concepts

https://www.throwable.club/2018/11/17/jvm-micrometer-prometheus/

http://yongyao.li/blog/article/使用micrometer进行业务指标上报

CATALOG
  1. 1. 介绍
  2. 2. Registry
  3. 3. 命名
  4. 4. Meter filters
  5. 5. Meter类别
  6. 6. Reference