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. 命名
    1. 3.1. Meter
    2. 3.2. Tag
  4. 4. Meter filters
  5. 5. Meter类别
    1. 5.1. Counter
    2. 5.2. Guage
    3. 5.3. Timer
    4. 5.4. Distribution summaries
    5. 5.5. 其他
  6. 6. Reference