Java监控度量Micrometer全解析
# 写在文章开头
之前的一篇文章中笔者介绍了应用埋点监控的主流方案,这其中涉及到应用指标计量器,由于计量器涉及的概念比较多,所以笔者才考虑抽出这篇文章专门来介绍一下java监控度量库Micrometer。Micrometer作为最流行的客观测系统简单门面,使得Java开发能够基于jvm的应用进行仪表化,无需受限于特定的提供商,即无缝衔接主流观测系统例如elastic、Prometheus、grafana等。
通过本文的阅读,您将会对Micrometer维度模型及常见计量器guage、counter、timer以及Distribution Summary有着深刻的理解和掌握,让你编写符合自己需求的应用监控指标且能够跨维度进行深入分析。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 详解监控系统核心概念
# 纬度性
维度性是Micrometer中最显著的一个优势,相比于其他的计量器,它可以针对应用指标通过标签键值对来丰富指标,可以让prometheus此类观测系统能够针对指标进行维度化区分,例如针对http请求数的计量器(后文会展开详细介绍),就可以通过请求的接口映射构成一个维度进行区分:

这里笔者也贴出从spring actual上找到的自定义的http_requests_total来印证这一点(后文会介绍counter指标),可以看到针对相同的指标http_requests_total笔者针对不同的http接口映射名称打上了不同的标签来层次化区分:
# HELP http_requests_total
# TYPE http_requests_total counter
http_requests_total{application="web-service",uri="/function",} 0.0
http_requests_total{application="web-service",uri="/hello",} 0.0
2
3
4
5
# 速率聚合
大部分应用进行监控时时候,更多是观察应用的趋势和变化,所以针对某个时间段的样本指进行聚合。这其中就涉及服务端聚合和客户端聚合两种情况。
先来说说服务端速率聚合,这种方式本质上就是通过Prometheus轮询或者抓取期间抓取的数据,通过服务端自带的表达式例如grafana等promql针对从过去到现在的应用指标进行收敛计算,按照micrometer官网的说法,它要求无论是告警监控还是金丝雀发布涉及到的指标分析,我们都应该以速率聚合数据位基础进行自动化,这也就是为什么本文针对micrometer计量指标进行介绍时会在一些带有时间特性的分布式摘要计量指标进行重点说明的原因所在:

然后就是客户端聚合,即符合:
- 针对单位时间内的数据进行聚合,并通过少量的数学计算满足查询而非通过绝对值进行分析
- 若几乎或者没有数学操作,则通过发布预聚合构建有意义的数据指标
我们还是以micrometer为例,它再进行客户端速率时用到一个步进值来维护速率数据,例如:我们假设现在有个计量数据需要统计变化的速率,按照每10s进行轮询发布,如果micrometer检测到当前间隔已过,则会移动到先前一个间隔窗口获取数据进行收敛聚合,如下图:
- 第1个10s的窗口进行速率聚合时,因为没有上1个窗口,所以没有值,poll函数拉取到的聚合指标是0增长
- 第2个窗口进行速率聚合,获取上一个间隔窗口,算得两个数据,由此可知速率0.2/s的增长量
- 第3个窗口同理算得速率为0.3/s的增长量

基于这种步进值的算法,若micrometer的计时器以10s作为间隔进行数据发布到监控系统,同时我们观察到有20个请求,每个请求耗时为100ms,就可以得出第一个间隔的请求总数和总耗时:
平均间隔请求总数 count = 间隔时间 * 平均每秒请求数
= 10 * (20/10) = 20
请求总耗时 totalTime = 间隔时间 * 请求总耗时/请求间隔
= 10 * (20 * 100 /10 )
= 2s
2
3
4
5
6
注意:可能因为数据原因,读者觉得这种计算方式繁琐啰嗦,实际上micrometer平均数据计算聚合都是通过窗口时间 * 平均速率得出,同时基于micrometer这些计量数据,我们还可以得出每个请求的平均耗时:
totalTime/count= 2s / 20 = 100 ms
其他监控系统可以基于分布式摘要得到的总数和平均值推算出:
count为20,对应1分钟也就是6个间隔的请求总数为 6 * 20 = 120- 基于
totalTime为2s,也就是1分钟的时延为12s,由此得出平均时延为 12/ 120 = 100ms
# 发布
关于指标的发布主流的是有两种方式:
- 客户端主动推动:
AppOptics,Atlas,Azure Monitor,Datadog,Dynatrace,Elastic,Graphite,Ganglia,Humio,Influx,JMX,Kairos,New Relic,SignalFx,Wavefront - 服务端轮询拉取:
Prometheus, 所有StatsD变种
# 详解Micrometer中常见的计量器
# 计时器
介绍完了监控系统的基本概念之后,我们就来介绍一些Micrometer中一些比较常见的度量器,在此之前我们需要先引入micrometer的核心依赖包:
<!--暴露spring监控指标-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.4.1</version>
</dependency>
<!--用于导出prometheus系统类型的指标数据-->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.1.4</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
配置文件也将指标暴露,方便我们通过http://127.0.0.1:18080/actuator/prometheus调测理解这些指标:
# 暴露并开启所有的端点,Spring Boot Actuator会自动配置一个 URL 为 /actuator/Prometheus 的 HTTP 服务来供 Prometheus 抓取数据
management.endpoints.web.exposure.include=*
# 展示所有的健康信息
management.endpoint.health.show-details=always
# 默认/actuator/Prometheus,添加这个tag方便区分不同的工程
management.metrics.tags.application=${spring.application.name}
# Actuator 监控端点独立端口设置为 18080(与主应用端口分离)
management.server.port=18080
# Actuator 端点的根路径改为 /management(默认是 /actuator)
#management.endpoints.web.base-path=/management
2
3
4
5
6
7
8
9
10
先来说说计时器timers,该度量器适用于对于应用时间指标的采集与分析,即短时延及其事件发生的频率,通过该计量器我们只需简单将采集到的时间指标交给timer,timer底层的数据结构和算法就会帮我得出总时间和计数的时间序列,并推算出采集数据的:
- 最大耗时
- 百分位
- 直方图
这里以java开发常用的spring框架为例演示一下timers最经典的用法——http接口请求时延图表,例如:笔者现在有一个hello映射的接口,我们希望通过timer计时器来获取针对接口时延,对应的服务器请求时延指标的开发步骤为:
- 全局
register注册一个timer计时器 - 计时器打上接口映射作为URL标签,实现维度化
- 接口请求结束,告知timer本次请求耗时,由timer进行各种时延总数、平均数的计算:

对应的代码如下,首先controller注入注册器并声明timer计时器:
@Autowired
private MeterRegistry registry;
private Timer reqHelloTimer;
2
3
随后通过PostConstruct将计时器初始化,并指明可被理解的名称同时打上URL标签指定接口名称,最后再声明要发布的百分位图:
@PostConstruct
private void init() {
//名称设置为http.timer,标签设置为uri为/hello,选用合适的名称辅助开发推断理解
reqHelloTimer = Timer
.builder("http.timer")
.publishPercentiles(0.5, 0.95) //发布百分位数区间
.description("接口请求耗时统计") // 指标的描述
.tags("uri", "/hello") // url标签指明为hello
.register(registry);
}
2
3
4
5
6
7
8
9
10
11
12
此时我们就可以使用其record方法记录本次耗时并指明耗时单位避免数据指标存在二义性混淆,此时计时器主动获取根据采集到数据维护时间序列:
@GetMapping("/hello")
public String helloWorld() {
long begin = System.currentTimeMillis();
int sleepTime = RandomUtil.randomInt(1000);
log.info("休眠 {} 毫秒", sleepTime);
ThreadUtil.sleep(sleepTime);
//记录休眠时间,指明单位为毫秒
reqHelloTimer.record(System.currentTimeMillis() - begin, TimeUnit.MILLISECONDS);
int sleepTime = RandomUtil.randomInt(1000);
log.info("休眠 {} 毫秒", sleepTime);
ThreadUtil.sleep(sleepTime);
//记录耗时
reqHelloTimer.record(System.currentTimeMillis() - begin, TimeUnit.MILLISECONDS);
//获取请求总数
long count = reqHelloTimer.count();
//获取最大耗时
double max = reqHelloTimer.max(TimeUnit.MILLISECONDS);
//获取最小耗时
double totalTime = reqHelloTimer.totalTime(TimeUnit.MILLISECONDS);
//获取平均耗时
double mean = reqHelloTimer.mean(TimeUnit.MILLISECONDS);
log.info("请求次数:{},最大值:{},总耗时:{},平均耗时:{}", count, max, totalTime, mean);
return "Hello, World!";
}
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
对应的我们试着去请求两次该接口,即可看到timer计量器针对时间所得出的应用指标信息数据:
2025-10-18 01:01:48.057 INFO 57066 --- [nio-8080-exec-1] c.sharkchili.controller.TestController : 用户请求了 /hello 接口
2025-10-18 01:01:48.057 INFO 57066 --- [nio-8080-exec-1] c.sharkchili.controller.TestController : 休眠 10 毫秒
2025-10-18 01:01:48.073 INFO 57066 --- [nio-8080-exec-1] c.sharkchili.controller.TestController : 请求次数:1,最大值:13.0,总耗时:13.0,平均耗时:13.0
2025-10-18 01:16:15.549 INFO 57066 --- [nio-8080-exec-2] c.sharkchili.controller.TestController : 用户请求了 /hello 接口
2025-10-18 01:16:15.549 INFO 57066 --- [nio-8080-exec-2] c.sharkchili.controller.TestController : 休眠 335 毫秒
2025-10-18 01:16:15.890 INFO 57066 --- [nio-8080-exec-2] c.sharkchili.controller.TestController : 请求次数:2,最大值:0.0,总耗时:353.0,平均耗时:176.5
2
3
4
5
6
对应的这些指标也会在spring actual上发布显示,可以看到,按照prometheus风格,我们的http.timer被转换为http_timer并拼接上采集数据计量指标名渲染输出。
这里笔者需要着重说明一个quantile这就是我们上文中指定百分位的作用,通过这个分布图我们可以知晓有50%的请求耗时小于0.276824064以及95%的请求耗时小于0.46137344:
# HELP http_timer_seconds 接口请求耗时统计
# TYPE http_timer_seconds summary
http_timer_seconds{application="web-service",uri="/hello",quantile="0.5",} 0.276824064
http_timer_seconds{application="web-service",uri="/hello",quantile="0.95",} 0.46137344
http_timer_seconds_count{application="web-service",uri="/hello",} 3.0
http_timer_seconds_sum{application="web-service",uri="/hello",} 0.967
# HELP http_timer_seconds_max 接口请求耗时统计
# TYPE http_timer_seconds_max gauge
http_timer_seconds_max{application="web-service",uri="/hello",} 0.464
2
3
4
5
6
7
8
9
micrometer-core内置了计时器的注解@Timed,spring框架可以直接使用该注解处理维护web请求添加计时指标的支持,而使用的方式也比较简单:
- 全局配置
timer注解的切面TimedAspect,该切面底层会声明一个维护类名和方法名的timer计时器 - 在需要计时支持的接口上注明
@Timed注解
由此,当接口触发调用时,切面就会将请求耗时信息维护到时间序列中。
对应切面配置如下:
@Configuration
public class TimedConfiguration {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
2
3
4
5
6
7
步入TimedAspect构造方法也印证了笔者的说法,基于类名和方法名构建计时器:
public TimedAspect(MeterRegistry registry) {
this(registry, pjp ->
Tags.of("class", pjp.getStaticPart().getSignature().getDeclaringTypeName(),
"method", pjp.getStaticPart().getSignature().getName())
);
}
2
3
4
5
6
使用时也只需在接口上注明@Timed注解即可:
@Timed
@GetMapping("/function")
public String function() {
int sleepTime = RandomUtil.randomInt(1000);
log.info("休眠 {} 毫秒", sleepTime);
ThreadUtil.sleep(sleepTime);
reqFunctionCounter.increment();
return "function";
}
2
3
4
5
6
7
8
9
10
11
12
当我们尝试发起请求,并通过/actuator/prometheus映射查看,本次接口请求的总数、总耗时、最大耗时等信息都输出渲染了,后续我们就可以通过监控系统轮询采集这些指标进行速率聚合得到单位时间内最大耗时和平均耗时(文章后续系列会给出grafana的实践):
method_timed_seconds_count{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="function",} 1.0
method_timed_seconds_sum{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="function",} 0.360479125
# HELP method_timed_seconds_max
# TYPE method_timed_seconds_max gauge
method_timed_seconds_max{application="web-service",class="com.sharkchili.controller.TestController",exception="none",method="function",} 0.360479125
2
3
4
5
这里补充说明一点,很多读者可能会基于该计量器进行别的用途,在使用时需要注意:
- 避免记录负值
- 避免记录过多长耗时导致总时间溢出溢出
- 最大值不要超过
Long.MAX_VALUE
# 计数器
计数器只能报告单一的指标,允许单调递增且不能为负数,所以计数器常用语统计一些事件的增长速率,需要特别注意一点,永远不要手动用计数器实现那些timer或者DistributionSummary能够做到的计数汇总,例如手动用计数器维护请求接口总数并计算接口平均耗时。
这里我们counter的最佳实践,也给出一个自服务运行以来的请求总数,使用方式也比较简单和计时器类似,全局声明counter并将其注册到注册器中:
//统计hello请求的计数器
private Counter reqHelloCounter;
@PostConstruct
private void init() {
//名称设置为http.requests,标签设置为uri为/hello,选用合适的名称辅助开发推断理解
reqHelloCounter = registry.counter("http.requests", "uri", "/hello");
//......
}
2
3
4
5
6
7
8
9
10
11
当我向hello接口发起请求时,对应actual映射就会看到如下指标,可以看到hello映射的请求总数counter:
# HELP http_requests_total
# TYPE http_requests_total counter
http_requests_total{application="web-service",uri="/hello",} 1.0
2
3
使用countrer时需要注意一下几点:
counter是个自增计量器,对于需要进行计时和汇总的内容,优先考虑使用timer或DistributionSummarycounter在应用重启后会重置为0counter适用于单调递增的计量需求,让监控系统分析增长速率
# 仪表
仪表是一个用于获取当前数值的句柄,典型的仪表包括集合映射大小或者出于运行状态的线程池,所以micrometer认为guages是一个应被采样而不可被设置的,即不可设置中间值(因未及时被监控系统采集就会丢失),它更多作用是在观察时才会动态变化的一个监控指标,这也是为什么guage常用于监控那些具有自然上限的属性。
gauge常的用法的也比较简单,再进行必要的声明和注册后,就像使用原子类一样,笔者这里就直接给出大体的用例就不多做介绍了:
//全局声明
private AtomicInteger myGauge ;
@PostConstruct
private void init() {
myGauge= registry.gauge("numberGauge", new AtomicInteger(0));
}
2
3
4
5
6
7
8
9
10
对应使用示例如下,可以直接原子set亦或者用cas进行自增或自减:
//修改值
myGauge.set(RandomUtil.randomInt(100));
//自增
myGauge.incrementAndGet();
//自减
myGauge.decrementAndGet();
2
3
4
5
6
7
# 分布式摘要
针对非时间指标的事件分布情况的分析,我们更推荐使用分布式摘要也就是DistributionSummary,例如我们需要统计每个接口请求的响应消息大小,就可以使用声明一个分布式摘要,并通过切面捕获这些信息,对应的全局声明代码示例如下
private DistributionSummary summary;
@PostConstruct
private void init() {
//声明
summary = DistributionSummary
.builder("response.size")
.description("消息大小")
.baseUnit("bytes")
.tags("url", "test")
.register(registry);
//......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用时,我们也只需获取切面返回的消息大小,并记录到summary即可,同时我们可以基于summary的常见api实时打印监控信息:
//使用
summary.record(byteSize);
double max = summary.max();
long count = summary.count();
double avg = summary.mean();
log.info("最大值:{},请求次数:{},平均值:{}", max, count, avg);
2
3
4
5
6
7
由此我们可以在actual上看到当前接口请求返回消息大小:
- 请求总数
- 总大小
- 最大值
# HELP response_size_bytes 消息大小
# TYPE response_size_bytes summary
response_size_bytes_count{application="web-service",url="/test",} 5.0
response_size_bytes_sum{application="web-service",url="/test",} 283.0
# HELP response_size_bytes_max 消息大小
# TYPE response_size_bytes_max gauge
response_size_bytes_max{application="web-service",url="/test",} 79.0
2
3
4
5
6
7
# 详解micrometer高级特性
# Meter Filters过滤
默认情况下我们spring应用都是暴露所有指标,如果我们希望过滤特定指标的话可以通过MeterFilter实现,使用方式也比较简单,通过MeterFilter的deny并声明过滤的表达式即可:
@Configuration
public class MeterFilterConfig {
@Bean
public MeterFilter meterFilter() {
//过滤actual上jvm或者tomcat开头的监控指标
return MeterFilter.deny(id -> id.getName().startsWith("jvm.")|| id.getName().startsWith("tomcat."));
}
}
2
3
4
5
6
7
8
9
此时,guage就不会再输出显示这些指标了:

# 小结
本文介绍了监控系统的维维度性、速率聚合和发布,同时介绍了micrometer中常见的计时器、计数器和分布摘要并给出相应的实践,在此基础之上也给出了监控指标的过滤技巧,希望对你有帮助。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 参考
Micrometer 官方文档 (opens new window)