对于一个健全的应用,性能指标的收集是必不可少的。通过这些指标,可以有效地对系统各方面进行监控,帮助开发及运维人员掌握系统运行状态,查找问题原因。
SLA指标,即服务等级协议的简称(全称:service level agreement)。是在一定开销下为保障服务的性能和可用性,服务提供商与用户间定义的一种双方认可的协定。对于互联网公司来说,SLA是网站可用性的一个保证。通常说的3个9(即99.9%)、4个9(即99.99%),9越多代表全年服务可用时间越长,服务越可靠,停机时间越短。
性能指标监控通常分2步实现:
1)代码中埋点,进行监控数据采集
2)后台监控系统对采集的数据进行聚合计算,生成相应的图表及进行可视化分析
MicroMeter是一款针对JVM应用的Metrics指标监测库,应用程序通过调用其提供的通用API来收集性能指标,并对接多种当前流行的监控系统,如Prometheus、Datadog。因其内部实现了对不同监控系统的适配工作,使得切换监控系统变得很容易。其设计宗旨即在提高可移植性的同时,几乎不增加指标收集活动的开销,号称监控界的SLF4J,对于SLA指标的测量非常方便。
SpringBoot 2.x版本中,spring-boot-actuator使用了Micrometer实现监控,也就是我们通常说的SpringBoot集成了Micrometer,这就使得在SpringBoot程序中使用Micrometer更加方便。
本文将简要介绍一下Micrometer官方文档中较常用的部分,并介绍其在SpringBoot应用程序中的用法。
一、Micrometer简介:
Micrometer官方文档地址如下:https://micrometer.io/docs
1.几个核心概念:
1)Registry:注册表
Meter是收集应用的测量指标的接口。Micrometer通过MeterRegistry创建和保存Meter,每个监控系统都会有MeterRegistry的实现。
Micrometer中包含一个SimpleMeterRegistry,它在内存中维护每个Meter的最新值,但是不对外提供。如果你还没有一个首选的监测系统,可以先用SimpleMeterRegistry:
MeterRegistry registry = new SimpleMeterRegistry();
如果用Spring的话,SimpleMeterRegistry是自动注入的。
2)Meter:计量器
Micrometer提供了以下几种不同类型的计量器。
- 计数器(Counter): 表示收集的数据是按照某个趋势(增加/减少)一直变化的,也是最常用的一种计量器,例如接口请求总数、请求错误总数、队列数量变化等。
- 计量仪(Gauge): 表示搜集的瞬时的数据,可以任意变化的,例如常用的 CPU Load、Mem 使用量、Network 使用量、实时在线人数统计等,
- 计时器(Timer): 用来记录事件的持续时间。
- 分布概要(Distribution summary): 用来记录事件的分布情况,表示一段时间范围内对数据进行采样,可以用于统计网络请求平均延迟、请求延迟占比等。
2.命名:
1)Micrometer命名规则采用点分隔的小写单词。不同的监控系统有不同的命名规则,他们在对Micrometer的实现中,会把命名按照自己的规则进行转换,同时清除那些本系统无法识别的特殊字符。还可以定义自己的名称转换器,在配置类中设置进来,例如:
原实现:
@Configuration
public class MeterConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
return registry -> registry.config().commonTags("application", applicationName);
}
}
改为:
return registry -> registry.config().commonTags("application", applicationName).namingConvention(myCustomNamingConvention);
例如:Micrometer中名称为http.server.requests的timer在不同监控系统中的名字如下:
registry.timer("http.server.requests");
① Prometheus - http_server_requests_duration_seconds
② Atlas – httpServerRequests
③ Graphite - http.server.requests
④ InfluxDB - http_server_requests
遵守Micrometer的点分小写单词的命名规范,可以最大的保证度量名称在各监控系统之间的可移植性。
2)Tag命名:也推荐使用点分小写单词的规则
例如:我们想要测http请求量和数据库的调用量。推荐命名如下:
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")
这样,我们查询database.calls 就可以得到所有数据库的调用量,然后进一步通过名称为db的tag来查询具体某个库的调用量,如users库的调用量。
不推荐的命名如下:
registry.counter("calls", "class", "database", "db", "users");
registry.counter("calls", "class", "http", "uri", "/api/users");
这种命名中,如果使用calls来查,得到的将是http请求量和数据库的调用量的一个聚合指标,如果不进一步根据标签查询,这个结果将毫无作用。
3)通用标签:
通用标签是定义在registry上的,运行时会被添加到每一个监控指标上,发布到监控系统。通常用于对操作环境的深度分析,比如:主机、实例、区域,stack堆栈等。
例如:(如下2个写法是等价的),调用commonTags()方法追加通用标签。
registry.config().commonTags("stack", "prod", "region", "us-east-1");
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));
在实际应用中通常这样写:
@Configuration
public class MeterConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
return registry -> registry.config().commonTags("application", applicationName);
}
}
3.速率聚合:
对于速率聚合,不同的监控系统有不同的偏好。有的期望在客户端发布数据之前聚合,有的则期望在服务端聚合,Micrometer 根据监控系统的需要做相应的适配。
但也并不是所有的指标都需要以速率的形式发布和查看,比如:gauge和长任务timer就不需要速率聚合。
- 服务端聚合:
执行服务端速率聚合的监控系统希望Micrometer在每一个时间间隔内发布监控指标的值。比如,从服务启动开始,在每个时间间隔内发布counter的增长的值。
例如,有一个随机的walk程序,每10ms counter 增长一次。如果我们在Prometheus里查询原始counter值,会看到一个逐步单调递增的函数(步长是Prometheus抓取数据的时间间隔。)
不做速率聚合直接展示Counter数据几乎是没有用处的。因为它和counter的增长速度还有程序的服务时长相关。比如我们上面的例子中,服务停掉时counter跌回0。新实例启动后,速率聚合图将会恢复到55左右,如下图。
如果已经实现了0停机部署,这样排除了counter归零的情况,使用counter原始值来报警就会比较困难,但你还可以轻松地在速率图上设置最低阈值来报警。在生产环境下,无论是报警还是自动数据分析,都是基于聚合速率自动实现的。(Prometheus属于服务端监控系统)
- 客户端聚合:
其它类型的监控系统,有以下2个特征。
1)他们需要速率聚合数据。他们都需要根据速率做出决策,而不是绝对的数值。
2)查询时不需要或只需要很少的数学运算。对于这些系统,发布提前聚合好的数据是很有必要的。
MicroMeter通过一个步长值来维护速率数据,这个步长值用于累积当前时间间隔内的数据。在被轮询(poll)的时候,如果这个步长值监测到当前时间间隔已经过期,就把当前的数据设置为“previous”状态。这个“previous”状态的数据也是会被发布的,直到下次“current”状态的数据覆盖掉它。
poll函数返回的值始终是:每秒的速率*时间间隔。 例如,上面数的步长值代表counter值,可见,在第一个时间间隔内的增长速率是 0.3每秒,在第二个时间间隔内,它被置为“previous”状态,但是它可以在第二个时间间隔内的任意时刻发布到后端。
Micrometer的timers至少单独跟踪一个count和总耗时。假设时间间隔为10秒,期间有20个请求,每个耗时100ms。
在第一个时间间隔内:
count = 10 seconds * (20 requests / 10 seconds) = 20 requests
totalTime = 10 seconds * (20 * 100 ms / 10 seconds) = 2 seconds
这个count的统计很有意义,它是吞吐量的指标。总耗时则是该时间间隔内所有请求的总延时。
totalTime / count = 2 seconds / 20 requests = 0.1 seconds / request = 100 ms / request
这个指标表示平均耗时。有些监控系统比如Atlas,会为计算这些统计数据的分布提供基础设施,Micrometer会分别发布总耗时和count这2个统计指标。其他的类似于Datadog等监控系统,没有此类内置的操作,Micrometer就会在客户端计算好这个平均分布指标,然后发送给监控系统。
发布间隔内的速率足够用来推断出任何大于或等于该间隔的时间窗内的速率。
例如,在给定的1分钟内,一个服务每10s接收20个请求,每个耗时100ms,我们可以得出结论:
1)在10s的时间窗内,Micrometer报告的count为20个请求。监控系统会简单的将这6个10s的间隔相加,得出120每分钟的结论。得出这个结论的是该监控系统,而不是Micrometer。
2)在10s的时间窗内,Micrometer报告的totaltime为2分钟。监控系统将1分钟内的总耗时相加,为12秒,这样平均耗时就是 12s/120requests = 100ms/request
4.常用Meter简介
1)Counters:记录一个计数指标,Counter接口允许以固定的正数值递增。
注意:不要用Counter来计那些可以使用Timer 和 DistributionSummary记录的指标(Timer 和 DistributionSummary 会在发布其他测量指标的同时发布一个计数指标)
根据counters建立图表或者报警时,通常需要关注的是在一个给定的时间区间内,某事件发生的比率。如一个队列,Counter可以用来衡量插入率和移除率。
相比于速率,人们比较容易对计数值产生可视化的印象。但是count是速度和时间的函数。 根据counter建图表或者报警的话,在服务启动很久后才能够看出异常。不推荐。
注意:由于Timer里已经记录了一个count,因此那些记录了Timer的代码,就不需要单独增加Counter了。
Normal rand = ...; // a random generator
MeterRegistry registry = ...
Counter counter = registry.counter("counter"); (1)
Flux.interval(Duration.ofMillis(10))
.doOnEach(d -> {
if (rand.nextDouble() + 0.1 > 0) { (2)
counter.increment(); (3)
}
}).blockLast();
(1)处,使用registry和name创建一个Countter。还可以添加一些列的Tag。
(3)处,就是使用counter的方式,也可是通过counter.increment(n)在一步操作中增加1以上的值。
Counter本身还有一个流式的构造器,可以调用那些不太常用的方法,如baseUnit()和description()。然后在构造器的最后一步调用register()方法将Counter注册到registry上。
Counter counter = Counter
.builder("counter")
.baseUnit("beans") // optional
.description("a description of what this counter does") // optional
.tags("region", "test") // optional
.register(registry);
2)Gauges:Guage用于获取当前值,典型的例子是获取集合、map或者运行中的线程数。
注意:Gauge对于监控那些有自然上限的事物很有用。不建议使用Gauge记录诸如请求次数之类的,因为这类指标会在应用程序的生命周期内不受限制的增长。另外,不要用Gauge记录那些可以用Counter记录的东西。
List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); (1)
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>()); (2)
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>()); (3)
(1)调用的方法为:
public <T> T gauge(String name, Iterable<Tag> tags, @Nullable T obj, ToDoubleFunction<T> valueFunction)
第一个参数为gauge的name;第二个参数为tag,这里设置为空;第三个参数就是待监测的对象,这里是一个新建的ArrayList;最后一个参数时待检测的指标,此处是List的size。后续在使用该list时,不需要感知Gauge接口的存在,当该list的size变化时,该变更值自然就会被记录。
与(1)中类似,(2)和(3)分别会记录当前list和map的size变化。
此外,Gauge还支持java.lang.Number的子类,java.util.concurrent.atomic包中的AtomicInteger和AtomicLong,还有Guava提供的AtomicDouble:
AtomicInteger n = registry.gauge("numberGauge", new AtomicInteger(0));
n.set(1);
n.set(2);
Gauge也支持流式构造函数,但是通常没有用,因为我们一般不需要操作Gauge实例。
Gauge gauge = Gauge
.builder("gauge", myObj, myObj::gaugeValue)
.description("a description of what this gauge does") // 可选
.tags("region", "test") // 可选
.register(registry);
3)Timer:用于测量较短的时延以及此类事件的频率。所有Timer接口的实现都会在每个时间序列里报告总耗时及请求次数2项指标。
Timers虽然可以在其他情况下使用,但注意不支持负值。而且记录过长的时间可能会导致总时间溢出(Long.MAX_VALUE纳秒,即292.3年)。
例如:使用图表显示web请求延时。服务器可以快速响应很多次请求,所以Timer将会每秒更新很多次。
Timer的使用会在后面详细介绍,此处不赘述。
4)Distribution summaries:分布概要
分布概要用于跟踪事件的分布情况,在结构上类似于Timer,但它记录的值不依赖于时间。分布概要根据每个事件所对应的值,把事件分配到对应的桶(bucket)中。Micrometer 默认的桶的值是从 1 到最大的 long 值。可以通过 minimumExpectedValue 和 maximumExpectedValue 来控制值的范围。如果事件所对应的值较小,可以通过 scale 来设置一个值来对数值进行放大。
分布概要DistributionSummary 可以从注册表中创建,也可以使用 DistributionSummary.builder() 提供的构建器来创建。例如,使用Distribution summaries监测服务器的负载。
创建一个distribution summary:
DistributionSummary summary = registry.summary("response.size");
使用流式的构造函数创建:
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);
(1)中添加基本单元,可以最大程度的获取可移植性—对于某些监控系统而言,基本单元是命名规则的一部分。如果不设置,也不会有什么不利影响。
(2)是可选的,设置比例因子,这样在记录样本的时候都会乘以这个比例。
如,该分布概要的比率范围是[0,1], 将其缩放100倍。
DistributionSummary.builder("my.ratio").scale(100).register(registry)
通过这种方式,该比率的最终范围是[0,100], 而且我们可以把maximumExpectedValue设置为100。如果你比较关心特定的比率,还可以自定义SLA边界,如下:
DistributionSummary.builder("my.ratio")
.scale(100)
.sla(70, 80, 90)
.register(registry)
注意:对于几个基本的DistributionSummary的实现,如:CumulativeDistributionSummary, StepDistributionSummary,Max表示TimeWindowMax。它的值是一个时间窗内的最大值。如果在这个时间窗长度内,没有记录新值,在新的时间窗开始时,Max将被置为0。时间窗的大小就是registry的step大小,除非显式的设置DistributionStatisticConfig的expire。使用TimeWindowMax的原因,是为了在资源压力过大导致延迟和并影响测量指标发布的情况下记录最大耗时。百分比也是指时间窗内的比分比TimeWindowPercentileHistogram。
与分布概要密切相关的是直方图和百分比(percentile)。大多数时候,我们并不关注具体的数值,而是数值的分布区间。比如在查看 HTTP 服务响应时间的性能指标时,通常关注是的几个重要的百分比,如 50%,75%和 90%等。即这些百分比数量的请求都在多少时间内完成。Micrometer 提供了两种不同的方式来处理百分比。
对于 Prometheus 这样本身提供了对百分比支持的监控系统,Micrometer 直接发送收集的直方图数据,由监控系统完成计算。对于其他不支持百分比的系统,Micrometer 会进行计算,并把百分比结果发送到监控系统。
创建一个带有直方图的Timer:
Timer.builder("my.timer")
.publishPercentiles(0.5, 0.95) // median and 95th percentile
.publishPercentileHistogram()
.sla(Duration.ofMillis(100))
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofSeconds(10))
-
publishPercentiles-用于发布你的应用中计算出的百分比值。这些值在跨维度上都是不可聚合的。
-
publishPercentileHistogram-用于发布直方图,可用于计算跨纬度的百分比近似值聚。Prometheus中使用histogram_quantile,Atlas中使用:percentile,Wavefront中使用hs()。
对于Prometheus和Atlas,结果直方图中的桶是由Micrometer根据经验合理预设的,以便为真实的Timer和Distribution Summary产生合理的误差范围。
默认情况会产生276桶,但Micrometer只会将那些minimumExpectedValue和maximumExpectedValue范围内的发送给后端。Micrometer默认将Timer限制在1毫秒到1分钟的范围内,在每个时间维度产生73个直方图桶。publishPercentileHistogram 对不支持聚合百分比近似值的系统没有影响,Micrometer不给这些系统发送直方图数据。 -
sla:用于发布基于SLA定义的桶的累积直方图。
与publishPercentileHistogram用法一致,在支持百分比聚合的监视系统中,此设置用于向已发布的直方图中添加额外的桶。对于不支持百分比聚合的系统上使用时,将仅使用这些桶发布直方图。 -
minimumExpectedValue/ maximumExpectedValue-控制publishPercentileHistogram桶的数量,同时控制基础HdrHistogram结构的准确性和内存占用量。
由于将百分比发送到监控系统会产生额外的时间序列,因此最好不要在应用程序依赖的核心库中配置它们。应用程序可以通过meter过滤器为某些timers/distribution summaries开启此行为。
例如,假设我们在一个公共库中有几个计时器。我们为这些Timer的名称添加了前缀myservice:
registry.timer("myservice.http.requests").record(..);
registry.timer("myservice.db.requests").record(..);
我们可以通过Meter过滤器为两个计时器启用客户端百分比:
registry.config().meterFilter(
new MeterFilter() {
@Override
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
if(id.getName().startsWith("myservice")) {
return DistributionStatisticConfig.builder()
.percentiles(0.95)
.build()
.merge(config);
}
return config;
}
});
二、Micrometer + Prometheus监控示例:
1.在SpringBoot项目中引入相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
2.添加相关配置:
server.port=8088
spring.application.name=micrometer-prometheus-teSt
management.endpoints.web.exposure.include=*
management.metrics.tags.application=${spring.application.name}
各配置项的含义如下:
1)server.port用于对外提供一个web端口。
2)spring.application.name用于配置当前服务的名称。
3)management.endpoints.web.exposure.include是Actuator相关配置,表示服务暴露的监控端点。监控端点说明如下:
除此之外,还可以使用management.endpoints.web.exposure.exclude指定服务不暴露的端点。例如
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=beans
首先,使用include暴露所有端点,接着使用exclud排除beans端点,这样就暴露了除beans外的所有actuator。
回到上面的配置中,我们是使用
management.endpoints.web.exposure.include=* 暴露了所有端点,这样SpringBoot Actuator会自动配置一个URL为/actuator/Prometheus 的 HTTP 服务来供 Prometheus 抓取数据。
4)management.metrics.tags.application 配置会将该工程应用名称添加到计量器注册表的 tag 中去,方便后边 Prometheus 根据应用名称来区分不同的服务。
3.在工程启动类或配置类中添加Micrometer相关配置:
这里采用单独的配置类,会使代码看起来清晰简洁一些。
@Configuration
public class MeterConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
return registry -> registry.config().commonTags("application", applicationName);
}
}
至此,就可以启动服务,并通过
http://localhost:8088/actuator/proMetheus 查看到一系列的metrics信息。如http_server_requests_seconds summary、jvm_memory_used_bytes gauge、jvm_gc_memory_promoted_bytes_total counter 等。
4.使用Micrometer监控服务接口耗时
计时器Timer本质上也是一种分布概要,通常用来记录服务响应时间的分布情况。大多数时候,我们关注的并不是具体的数值,而是几个百分比,如50%,75%,90%,99%和99.9%等,也就是这些百分比数量的请求都在多少时间内完成。
Timer的使用通常有以下2种方法:
1)创建Timer,并注册到MeterRegistry。在待监测的接口的起始和终止处手工标记。
@Data
@Component
public class TestMeter {
@Autowired
MeterRegistry registry;
//syncSend接口请求耗时
private Timer testTimer;
@PostConstruct
private void init() {
testTimer= Timer.builder("test_request")
.tags("uri", "pull")
.publishPercentiles(0.5, 0.80, 0.90, 0.95, 0.99, 0.999)
.register(registry);
}
}
//指标监测开始
Timer.Sample sample = Timer.start(consumerMeter.getRegistry());
//业务代码 ...
//指标监测结束
sample.stop(TestMeter.getTestTimer());
这种方式使用起来比较灵活,可以在指定的接口上满足定制化的监测需求。但是也存在一些问题:代码量较大,对于不同代码分支,异常情况等都需要人为考虑并做相应的处理。另外,接口数目较少时可以,如果接口数目众多,逐个接口标记计时的起止未免过于繁琐。
因此,我们可能会想到切面。通过切面对所有想要实现耗时指标监测的接口进行统一处理。也就是下面的方法,使用@Timed注解。
2)使用@Timed注解监测响应耗时。
例如:
@RestController
public class Controller {
//注释①
@Timed(value = "go", percentiles = {0.5, 0.80, 0.90})
@RequestMapping("/go")
public String go(){
return "test go …";
}
}
在没有注释①处的@Timed注解时,通过http://localhost:8088/actuator/prometheus访问监控指标可以看到:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 1.0
http_server_requests_seconds_sum{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.1156366
# HELP http_server_requests_seconds_max
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.1156366
这是SpringBoot 2.x Actuator自带的监控指标。用于监控http请求的调用情况,会显示10个请求量最大、耗时最长的URL,统计非200的请求量。
添加了@Timed注解后,发现统计数据中出现了该接口耗时指标的百分比分布情况,如下:
# HELP goRequest_seconds_max
# TYPE goRequest_seconds_max gauge
goRequest_seconds_max{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.0933378
# HELP goRequest_seconds
# TYPE goRequest_seconds summary
goRequest_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.5",} 0.092274688
goRequest_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.8",} 0.092274688
goRequest_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.9",} 0.092274688
goRequest_seconds_count{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 1.0
goRequest_seconds_sum{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.0933378
但是,如果我们将@Timed注解添加到非http接口上,发现不起作用。从官方文档得知,要想监测任意接口的耗时,需要在Spring中注入TimedAspect。方法如下:
@Configuration
public class MeterConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(@Value("${spring.application.name}") String applicationName) {
return registry -> registry.config().commonTags("application", applicationName);
}
//在配置类中注入TimedAspect
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
同时,需要引入如下依赖,开启对切面的支持:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
此时,再运行刚才的程序,却发现报了如下异常:
java.lang.IllegalArgumentException: Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named 'go_seconds' containing tag keys [application, class, method]. The meter you are attempting to register has keys [application, exception, method, status, uri].
at io.micrometer.prometheus.PrometheusMeterRegistry.lambda$collectorByName$9(PrometheusMeterRegistry.java:360)
at java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1877)
at io.micrometer.prometheus.PrometheusMeterRegistry.collectorByName(PrometheusMeterRegistry.java:347)
at io.micrometer.prometheus.PrometheusMeterRegistry.newTimer(PrometheusMeterRegistry.java:160)
at io.micrometer.core.instrument.MeterRegistry.lambda$timer$2(MeterRegistry.java:258)
at io.micrometer.core.instrument.MeterRegistry.getOrCreateMeter(MeterRegistry.java:567)
at io.micrometer.core.instrument.MeterRegistry.registerMeterIfNecessary(MeterRegistry.java:529)
at io.micrometer.core.instrument.MeterRegistry.timer(MeterRegistry.java:256)
at io.micrometer.core.instrument.Timer$Builder.register(Timer.java:447)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.stop(WebMvcMetricsFilter.java:183)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.record(WebMvcMetricsFilter.java:176)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:125)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:106)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
2020-07-13 14:09:46.603 [TID: N/A] [http-nio-8088-exec-1] ERROR o.s.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$SpelView - Cannot render error page for request [/go] and exception [Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named 'go_seconds' containing tag keys [application, class, method]. The meter you are attempting to register has keys [application, exception, method, status, uri].] as the response has already been committed. As a result, the response may have the wrong status code.
原因是加入了TimedAspect切面后,每个http接口的响应耗时的指标会自动添加上默认的tag。可以通过将$Timed注解上的value值去掉再尝试。
将@Timed(value = “go”, percentiles = {0.5, 0.80, 0.90})
改成 @Timed(percentiles = {0.5, 0.80, 0.90})
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.5",} 0.005505024
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.8",} 0.10878976
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.9",} 0.10878976
http_server_requests_seconds_count{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 2.0
http_server_requests_seconds_sum{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.1128193
# HELP http_server_requests_seconds_max
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.107252299
正如上面的异常显示,已经有一个名为’go_seconds’的Meter,tag为[application, exception, method, status, uri],我们设置了value的话相应Meter的tag是[application, class, method],有冲突,因此要想使用@Timed
注解监控http接口的响应耗时,不要加value。而如果是普通接口则需要加上value,否则没有监控数据(至于为什么,暂时不晓得)。例如:在一个普通方法doTest上使用。
@Timed(value="doTest",percentiles = {0.5, 0.80, 0.90})
public void doTest(String topicName){
//业务代码…
}
# HELP doTest_seconds_max
# TYPE doTest_seconds_max gauge
doTest_seconds_max{application="native-consumer-test",class="com.dangdang.nativeconsumer.Test",method="doTest",} 8.6792789
# HELP doTest_seconds
# TYPE doTest_seconds summary
doTest_seconds{application="native-consumer-test",class="com.dangdang.nativeconsumer.Test",method="doTest",quantile="0.5",} 8.589934592
doTest_seconds{application="native-consumer-test",class="com.dangdang.nativeconsumer.Test",method="doTest",quantile="0.8",} 8.589934592
doTest_seconds{application="native-consumer-test",class="com.dangdang.nativeconsumer.Test",method="doTest",quantile="0.9",} 8.589934592
doTest_seconds_count{application="native-consumer-test",class="com.dangdang.nativeconsumer.Test",method="doTest",} 1.0
doTest_seconds_sum{application="native-consumer-test",class="com.dangdang.nativeconsumer.Test",method="doTest",} 8.6792789
另外,从@Timed注解本身可以发现,其作用目标为
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD}) ,即该注解可应用在注解、接口、类、枚举、方法上。
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD})
@Repeatable(TimedSet.class)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Timed {
String value() default "";
String[] extraTags() default {};
boolean longTask() default false;
double[] percentiles() default {};
boolean histogram() default false;
String description() default "";
}
这样,对于http接口,只需在controller类上添加一个注解,就可以监测该类内的所有接口耗时。例如:
@RestController
@Timed(percentiles = {0.5, 0.80, 0.90})
public class Controller {
@RequestMapping("/go")
public String go(String topicName) throws Exception {
return "where";
}
@RequestMapping("/hello")
public String hello(String topicName) throws Exception {
return "world";
}
}
指标如下:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.5",} 0.075497472
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.8",} 0.075497472
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",quantile="0.9",} 0.075497472
http_server_requests_seconds_count{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 1.0
http_server_requests_seconds_sum{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.0778254
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",quantile="0.5",} 0.006815744
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",quantile="0.8",} 0.006815744
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",quantile="0.9",} 0.006815744
http_server_requests_seconds_count{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",} 1.0
http_server_requests_seconds_sum{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",} 0.006833101
# HELP http_server_requests_seconds_max
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{application="native-consumer-test",exception="None",method="GET",status="200",uri="/go",} 0.0778254
http_server_requests_seconds_max{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",} 0.006833101
而对于普通方法,亲测注解在类上是无效的,感兴趣的小伙伴可以再尝试和摸索,可能会有可行的用法。
我们观察耗时指标的百分比分布会发现,一段时间后,如果接口没有请求了,各百分比耗时都变为0,而请求次数、总耗时、最大耗时是不变的。
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",quantile="0.5",} 0.0
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",quantile="0.8",} 0.0
http_server_requests_seconds{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",quantile="0.9",} 0.0
http_server_requests_seconds_count{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",} 5.0
http_server_requests_seconds_sum{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",} 0.0171243
http_server_requests_seconds_max{application="native-consumer-test",exception="None",method="GET",status="200",uri="/hello",} 0.0033371
出现这种现象的原因是,Timer记录的是当前时间窗内的请求百分比分布,而不是从服务启动开始到当前时间的统计情况。时间窗的大小我们可以从官方文档有所了解。
就是说:时间窗的大小就是registry的step的大小,除非你显式的设置DistributionStatisticConfig 的expiry 参数。对应到代码里,也就是这个registry的step。
这个registry有好几个实现类,我们看prometheus的registry就好了:
点进去看PrometheusConfig
里面有个step()方法,可以看到这个step的大小是1分钟,也就是时间窗的大小:
如果要更改的话,就是在创建Timer后调用这个方法:
timer. distributionStatisticExpiry(Duration.ofSeconds(10))
比如这个语句是设置时间窗为10秒。不过从文档上来看,默认的时间窗应该已经是最优大小,不建议修改。
5.配置Prometheus抓取监控数据
Prometheus的安装步骤网上有很多教程,此处不赘述。例如,我们的Prometheus部署在192.168.47.199上,启动端口9090,在其prommetheus.yml文件中增加如下配置:
- job_name: micrometer-test
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['192.168.47.210:8088']
labels:
instance: micrometer-test
targets 即我们上文中启动的被监控服务地址,而/actuator/prometheus是获取监控数据的url。重启服务,就可以在页面上看到监控的系统:
访问地址如下:http://192.168.47.199:9090/targets
还可以在graph页面查询想要的指标:http://192.168.47.199:9090/graph
具体的查询语法可以查找Prometheus文档。
另外,我们发现prometheus.yml文件中有这样几行配置
global:
scrape_interval: 10s
evaluation_interval: 10s
其中,scrape_interval表示从监控目标拉取数据的周期。
evaluation_interval为告警规则周期。当然,还有很多配置项,想要详细了解Prometheus的小伙伴可以仔细研读文档。
6.Grafana 展示
Grafana是一款可视化监控指标展示工具,可以将上文中Prometheus抓取的监控数据作为数据源配置进来。然后import 4701模板,就可以展示prometheus中采样的系统的jvm监控数据:
我们在代码里埋点的监控数据要想在grafana里显示,可以点击加号,创建dashboard,输入prometheus的查询语句,然后保存。箭头处可以切换不同监控dashboard。例如: