Java服务_Java指标度量工具Metrics与Micrometer

Java服务_Java指标度量工具Metrics与Micrometer

一、Metrics工具包

1.简介

Metrics是一个给JAVA服务的各项指标提供度量工具的包,在JAVA代码中嵌入Metrics代码,可以方便的对业务代码的各个指标进行监控,同时,Metrics能够很好的跟Ganlia、Graphite结合,方便的提供图形化接口。基本使用方式直接将core包导入pom文件即可,配置如下:

1
2
3
4
5
<dependency>
<groupId>com.codahale.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>3.0.1</version>
</dependency>

core包主要提供如下核心功能:

  • 核心类Metrics Registries类似一个metrics容器,维护一个Map,可以是一个服务一个实例。
  • 支持五种metric类型:Gauges、Counters、Meters、Histograms和Timers。
  • 可以将metrics值通过JMX、Console,CSV文件和SLF4J loggers发布出来。

Metrics也提供了对Ehcache、Apache HttpClient、JDBI、Jersey、Jetty、Log4J、Logback、JVM等的集成,可以方便地将Metrics输出到Ganglia、Graphite中,供用户图形化展示,具体用法遇到具体需求再自行探索即可。

2.度量类型

2.1 Gauges

Gauges是一个最简单的度量类,一般用来统计瞬时状态的数据信息,本质是一个可以随时调用的方法。如下案例中使用Gauges统计系统中队列中元素个数,通过以下步骤将会向MetricsRegistry容器中注册一个名字为com.netease.test.metrics .TestGauges.pending-job.size的metrics,实时获取队列长度的指标:

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
54
/**
* 测试Gauges,实时统计pending状态的job个数
*/
public class TestGauges {
/**
* 实例化一个registry,最核心的一个模块,相当于一个应用程序的metrics系统的容器,维护一个Map
*/
private static final MetricRegistry metrics = new MetricRegistry();

private static Queue<String> queue = new LinkedBlockingDeque<String>();

/**
* 在控制台上打印输出
*/
private static ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();

public static void main(String[] args) throws InterruptedException {
reporter.start(3, TimeUnit.SECONDS);

//实例化一个Gauge
Gauge<Integer> gauge = new Gauge<Integer>() {
@Override
public Integer getValue() {
return queue.size();
}
};

//注册到容器中
metrics.register(MetricRegistry.name(TestGauges.class, "pending-job", "size"), gauge);

//测试JMX
JmxReporter jmxReporter = JmxReporter.forRegistry(metrics).build();
jmxReporter.start();

//模拟数据
for (int i=0; i<20; i++){
queue.add("a");
Thread.sleep(1000);
}

}
}

/*
console output:
14-2-17 15:29:35 ===============================================================
-- Gauges ----------------------------------------------------------------------
com.netease.test.metrics.TestGauges.pending-job.size
value = 4
14-2-17 15:29:38 ===============================================================
-- Gauges ----------------------------------------------------------------------
com.netease.test.metrics.TestGauges.pending-job.size
value = 6
*/

2.2 Counter

Counter是Gauge的一个特例,维护一个计数器,可以通过inc()和dec()方法对计数器进行修改,本质就是一个计数器。使用步骤与Gauge基本类似,在MetricRegistry中提供了静态方法可以直接实例化一个Counter。使用案例如下:

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
54
55
56
/**
* 测试Counter
*/
public class TestCounter {

/**
* 实例化一个registry,最核心的一个模块,相当于一个应用程序的metrics系统的容器,维护一个Map
*/
private static final MetricRegistry metrics = new MetricRegistry();

/**
* 在控制台上打印输出
*/
private static ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();

/**
* 实例化一个counter,同样可以通过如下方式进行实例化再注册进去
* pendingJobs = new Counter();
* metrics.register(MetricRegistry.name(TestCounter.class, "pending-jobs"), pendingJobs);
*/
private static Counter pendingJobs = metrics.counter(name(TestCounter.class, "pedding-jobs"));
// private static Counter pendingJobs = metrics.counter(MetricRegistry.name(TestCounter.class, "pedding-jobs"));

private static Queue<String> queue = new LinkedList<String>();

public static void add(String str) {
pendingJobs.inc();
queue.offer(str);
}

public String take() {
pendingJobs.dec();
return queue.poll();
}

public static void main(String[]args) throws InterruptedException {
reporter.start(3, TimeUnit.SECONDS);
while(true){
add("1");
Thread.sleep(1000);
}

}
}

/*
console output:
14-2-17 17:52:34 ===============================================================
-- Counters --------------------------------------------------------------------
com.netease.test.metrics.TestCounter.pedding-jobs
count = 4
14-2-17 17:52:37 ===============================================================
-- Counters --------------------------------------------------------------------
com.netease.test.metrics.TestCounter.pedding-jobs
count = 6
*/

2.3 Meters

Meters用来度量某个时间段的平均处理次数(request per second),如每1、5、15分钟的TPS,本质是使用一个map存储下事件和对应发生时间,然后基于该map中的数据进行预设的统计计算。比如一个service的请求数,通过metrics.meter()实例化一个Meter之后,然后通过meter.mark()方法就能将本次请求记录下来,统计结果有总的请求数,平均每秒的请求数,以及最近的1、5、15分钟的平均TPS。使用案例如下:

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
/**
* 测试Meters
*/
public class TestMeters {
/**
* 实例化一个registry,最核心的一个模块,相当于一个应用程序的metrics系统的容器,维护一个Map
*/
private static final MetricRegistry metrics = new MetricRegistry();

/**
* 在控制台上打印输出
*/
private static ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();

/**
* 实例化一个Meter
*/
private static final Meter requests = metrics.meter(name(TestMeters.class, "request"));

public static void handleRequest() {
requests.mark();
}

public static void main(String[] args) throws InterruptedException {
reporter.start(3, TimeUnit.SECONDS);
while(true){
handleRequest();
Thread.sleep(100);
}
}
}

/*
14-2-17 18:43:08 ===============================================================
-- Meters ----------------------------------------------------------------------
com.netease.test.metrics.TestMeters.request
count = 30
mean rate = 9.95 events/second
1-minute rate = 0.00 events/second
5-minute rate = 0.00 events/second
15-minute rate = 0.00 events/second
14-2-17 18:43:11 ===============================================================
-- Meters ----------------------------------------------------------------------
com.netease.test.metrics.TestMeters.request
count = 60
mean rate = 9.99 events/second
1-minute rate = 10.00 events/second
5-minute rate = 10.00 events/second
15-minute rate = 10.00 events/second
*/

2.4 Histograms

Histograms主要使用来统计数据的分布情况,最大值、最小值、平均值、中位数,百分比(75%、90%、95%、98%、99%和99.9%),本质是使用一个list存储一组数值数据,然后基于该list中的数据进行预设的统计计算。我自己写的HBase压测工具PerformanceEvaluation中使用到了该metrics类来进行请求响应时间分布情况统计。Histograms简单使用案例如下:

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
54
55
56
57
58
59
60
61
62
63
/**
* 测试Histograms
*/
public class TestHistograms {
/**
* 实例化一个registry,最核心的一个模块,相当于一个应用程序的metrics系统的容器,维护一个Map
*/
private static final MetricRegistry metrics = new MetricRegistry();

/**
* 在控制台上打印输出
*/
private static ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();

/**
* 实例化一个Histograms
*/
private static final Histogram randomNums = metrics.histogram(name(TestHistograms.class, "random"));

public static void handleRequest(double random) {
randomNums.update((int) (random*100));
}

public static void main(String[] args) throws InterruptedException {
reporter.start(3, TimeUnit.SECONDS);
Random rand = new Random();
while(true){
handleRequest(rand.nextDouble());
Thread.sleep(100);
}
}
}

/*
14-2-17 19:39:11 ===============================================================
-- Histograms ------------------------------------------------------------------
com.netease.test.metrics.TestHistograms.random
count = 30
min = 1
max = 97
mean = 45.93
stddev = 29.12
median = 39.50
75% <= 71.00
95% <= 95.90
98% <= 97.00
99% <= 97.00
99.9% <= 97.00
14-2-17 19:39:14 ===============================================================
-- Histograms ------------------------------------------------------------------
com.netease.test.metrics.TestHistograms.random
count = 60
min = 0
max = 97
mean = 41.17
stddev = 28.60
median = 34.50
75% <= 69.75
95% <= 92.90
98% <= 96.56
99% <= 97.00
99.9% <= 97.00
*/

2.5 Timers

Timers主要是用来统计某一块代码段的执行时间以及其请求分布情况,具体是基于Histograms和Meters来实现的,本质就是同时使用一个map存储下事件和对应发生时间,使用一个list存储一组数值数据,然后基于map和list中的数据进行预设的统计计算。使用案例如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 测试Timers
*/
public class TestTimers {
/**
* 实例化一个registry,最核心的一个模块,相当于一个应用程序的metrics系统的容器,维护一个Map
*/
private static final MetricRegistry metrics = new MetricRegistry();

/**
* 在控制台上打印输出
*/
private static ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics).build();

/**
* 实例化一个Meter
*/
// private static final Timer requests = metrics.timer(name(TestTimers.class, "request"));
private static final Timer requests = metrics.timer(name(TestTimers.class, "request"));

public static void handleRequest(int sleep) {
Timer.Context context = requests.time();
try {
//some operator
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
context.stop();
}

}

public static void main(String[] args) throws InterruptedException {
reporter.start(3, TimeUnit.SECONDS);
Random random = new Random();
while(true){
handleRequest(random.nextInt(1000));
}
}
}

/*
14-2-18 9:31:54 ================================================================
-- Timers ----------------------------------------------------------------------
com.netease.test.metrics.TestTimers.request
count = 4
mean rate = 1.33 calls/second
1-minute rate = 0.00 calls/second
5-minute rate = 0.00 calls/second
15-minute rate = 0.00 calls/second
min = 483.07 milliseconds
max = 901.92 milliseconds
mean = 612.64 milliseconds
stddev = 196.32 milliseconds
median = 532.79 milliseconds
75% <= 818.31 milliseconds
95% <= 901.92 milliseconds
98% <= 901.92 milliseconds
99% <= 901.92 milliseconds
99.9% <= 901.92 milliseconds
14-2-18 9:31:57 ================================================================
-- Timers ----------------------------------------------------------------------
com.netease.test.metrics.TestTimers.request
count = 8
mean rate = 1.33 calls/second
1-minute rate = 1.40 calls/second
5-minute rate = 1.40 calls/second
15-minute rate = 1.40 calls/second
min = 41.07 milliseconds
max = 968.19 milliseconds
mean = 639.50 milliseconds
stddev = 306.12 milliseconds
median = 692.77 milliseconds
75% <= 885.96 milliseconds
95% <= 968.19 milliseconds
98% <= 968.19 milliseconds
99% <= 968.19 milliseconds
99.9% <= 968.19 milliseconds
*/

3.Reporters导出度量数据

3.1 ConsoleReporters

ConsoleReporters常用于定时向控制台发送服务的各项指标数据。使用案例如下:

1
2
3
4
5
final ConsoleReporter reporter = ConsoleReporter.forRegistry(registry)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
reporter.start(1, TimeUnit.MINUTES);

3.2 CsvReporter

CsvReporter常用于定时向给定目录下的.csv文件追加服务各项指标数据,对于每一项指标都会在指定目录下创建一个.csv文件,然后定时向每个文件中追加指标最新数据。使用案例如下:

1
2
3
4
5
6
final CsvReporter reporter = CsvReporter.forRegistry(registry)
.formatFor(Locale.US)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build(new File("~/projects/data/"));
reporter.start(1, TimeUnit.SECONDS);

3.3 JmxReporter

JmxReporter将服务的各项度量指标通过JMX MBeans暴露出来,之后可以使用VisualVM查看指标数据,不建议在生产环境中使用。使用案例如下:

1
2
final JmxReporter reporter = JmxReporter.forRegistry(registry).build();
reporter.start();

3.4 Slf4jReporter

Slf4jReporter允许我们定时将服务的指标数据作为日志记录到日志文件中。使用案例如下:

1
2
3
4
5
final Slf4jReporter reporter = Slf4jReporter.forRegistry(registry).outputTo(LoggerFactory.getLogger("com.example.metrics"))
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.build();
reporter.start(1, TimeUnit.MINUTES);

二、Micrometer

1.简介

1.1 Micrometer与Metrics异同

Micrometer与Metrics一样也是java生态中用于对业务和应用性能度量进行记录和监控的工具,提供了一些列工具用于手机、存储和报告各种运行时度量,如请求次数、处理时间、系统资源使用等。

Micrometer与Metrics在功能上相似,都在内部维护了一组度量指标的集合,如使用map、list等在应用运行过程中收集相应数据,且都会启动一个后台进程或任务,负责周期性地将收集到的度量数据上报到配置的存储或后端监控系统中,开发者可以基于这些信息构建报警系统来响应特定的度量阈值。

但是Micrometer与Metrics作为两个不同的度量工具库,也有一些差异,Micrometer在设计之初就更强调作为不同度量的适配和规范中间层,支持用户方便的自定义实现,支持多维度标签,适合集群监控;而Metrics更偏重独立的监控系统解决方案,主要面向固定维度监控,也不适合现代化云集群部署监控。且Micrometer在设计上更强调通过通用api支持更多的监控后端,如Prometheus等现代化监控系统;虽然Metrics也可以通过实现Reporter接口来扩展新的后端支持,但是自定义工作较复杂。总而言之,当涉及到集群部署应用监控,且需要集成灵活美观的监控系统,使用Micrometer更好,且Micrometer天然与Spring Boot框架集成;当只需要单机使用且偏工具性质时,如压测工具等场景,使用Metrics更好。

1.2 Micrometer核心组件

Micrometer中核心两个内容便是meter监控组件和registry注册器,meter就是各种监控数据存储和计算工具,比如计数器,计量器,计时器等;registry是一个监控组件管理类,用于创建、管理各种meter,并决定将meter数据输出到哪里。不同的监控系统就会有不同的registry注册器实现,它们的共同父类都是MeterRegistry,如对应Prometheus的注册器是PrometheusMeterRegistry。

Meter的类型包括:Timer,Counter,Gauge,DistributionSummary,LongTaskTimer,FunctionCounter,FunctionTimer和TimeGauge。而一个Meter的具体实现类型需要通过名字和Tag(这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的Tag去区分多种维度进行数据统计。

2.Registry

MeterRegistry是Micrometer的一个抽象类,对其几个比较特殊的实现类进行一下介绍:

2.1 SimpleMeterRegistry

SimpleMeterRegistry是最简单的Register,在Spring-based的应用程序中自动装配,它会在内存中保存每个meter的最新值,但是不会将这个值发布到任何地方:

1
MeterRegistry registry = new SimpleMeterRegistry();

2.2 CompositeMeterRegistry

Micrometer提供了一个CompositeMeterRegistry,允许开发者通过添加多个registry的方式将指标数据同时发布到多个监控系统中:

1
2
3
4
5
6
7
8
9
10
11
12
13
CompositeMeterRegistry composite = new CompositeMeterRegistry();

Counter compositeCounter = composite.counter("counter");
// 此处increment语句处于等待状态,直到CompositeMeterRegistry注册了一个registry。
// 此时counter计数器值为0
compositeCounter.increment(); (1)

SimpleMeterRegistry simple = new SimpleMeterRegistry();
// counter计数器注册到simple registry
composite.add(simple); (2)

// simple registry counter 与CompositeMeterRegistry中的其他registries的counter一起递增
compositeCounter.increment(); (3)

2.3 全局的MeterRegistry

工厂类io.micrometer.core.instrument.Metrics中持有一个final静态的全局实现类globalRegistry,它也是一个CompositeMeterRegistry实例,其内部提供了一系列用于构建meters的方法。

2.4 自定义Registry

Micrometer为我们提供了很多开箱即用的Registry,基本上可以满足大多数的业务场景,同时也支持用户根据实际场景需求,自定义registry。通常我们可以通过继承MeterRegistry、PushMeterRegistry或者StepMeterRegistry来创建定制化的输出到不同监控系统的Registry。

3. Micrometer基于SpirngBoot、Prometheus、Grafana集成

集成了Micrometer框架的JVM应用使用到Micrometer的API收集的度量数据位于内存之中,因此,需要额外的存储系统去存储这些度量数据,需要有监控系统负责统一收集和处理这些数据,还需要有一些UI工具去展示数据。常见的存储系统就是时序数据库,主流的有Influx、Datadog等。比较主流的用于数据收集和处理的监控系统就是Prometheus(普罗米修斯)。展示的UI目前相对用得比较多的就是Grafana。另外Prometheus已经内置了一个时序数据库的实现,因此在做一套相对完善的度量数据监控的系统只需要依赖目标JVM应用、Prometheus组件和Grafana组件即可。

一般通过Micrometer提供的开箱即用的Registry,就可以覆盖docker、服务器、SpringBoot应用程序等基本的系统指标监控,也可以通过实现自定义指标监控给到prometheus进行采集和可视化。但是我们一般通过prometheus进行系统指标的监控,业务指标通常使用ELK来监控,毕竟ELK面向的是日志数据,只要我们记录下了日志就可以把日志数据清洗出来做业务指标面板监控。

线上问题的引入原因主要有变更、过载和强依赖故障,强依赖故障又可递归分为强依赖变更和强依赖过载。那么这两个不同的监控系统可分别用来应对不用的系统线上问题排查,其中ELK业务监控可用于监控变更带来的业务逻辑问题,如查询sql生成链路查看等;Prometheus系统监控可用于监控变更带来的程序错误,如接口可用率下降,也可用于监控过载带来的系统负载问题,如内存打满等。

更多搭建和实现细节自行查看参考文献。

参考文献

Metrics:

Metrics-Java版的指标度量工具之一

Metrics-Java版的指标度量工具之二

微服务性能监控系列-基于Metrics的实时监控系统

Micrometer:

服务性能监控之Micrometer详解

给你的SpringBoot做埋点监控–JVM应用度量框架Micrometer

Java类应用监控应该监控哪些?