作为
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 | <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 时最常用的 Timer
、Counter
、Guage
、DistributionSummary
就是继承自该接口,收集的系统数据就是用 Meter 来表示。Meter 在 Micrometer 中由 MeterRegistry
创建并保存,每个支持的监控系统都有一个MeterRegistry
实现(Prometheus的实现是PrometheusMeterRegistry
)。如果事先没有指定监控系统,Micrometer 默认生成一个将 Meter 最新数据保留在内存的SimpleMeterRegistry
。
在SimpleMetricsExportAutoConfiguration
中就可以看到 SimpleMeterRegistry 的配置,该类包含: @ConditionalOnMissingBean(MeterRegistry.class)。
1 | SimpleMeterRegistry simple = new SimpleMeterRegistry(); |
除了作为基础实现的SimpleMeterRegistry
外,Micrometer 提供CompositeMeterRegistry
,可将多个 MeterRegistry 加入其中,使 Meter 数据同时发送到不同监控系统。
1 | CompositeMeterRegistry composite = new CompositeMeterRegistry(); |
此外,Micrometer 还持有一个Global MeterRegistry:Metrics.globalRegistry
,其也是一个 CompositeMeterRegistry。如果没有引入 Prometheus 的依赖,Metrics.globalRegistry 中包含的就是 SimpleMeterRegistry,MeterRegistryPostProcessor
会将生成的系统初始生成的 SimpleMeterRegistry 注入进全局 Registry 中。如果引入了 Prometheus 的依赖,注入进全局 Registry 的就是 PrometheusMeterRegistry。
1 | public class Metrics { |
命名
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 | registry.counter("database.calls", "db", "users") // database.calls即为自行定义的指标, 而维度就是db, 含义即为db为users的数据库调用次数 |
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 | registry.config() |
针对Filter进行简单的试验:
1 | SimpleMeterRegistry simple1 = new SimpleMeterRegistry(); |
其输出结果为:
1 | MeterId{name='database.calls', tags=[]} [Measurement{statistic='COUNT', value=2.0}] |
可以看见jvm.requests
直接被过滤了,而 Tag db2
和db3
由于被忽略了,其值添加到了name='database.calls', tags=[]
中,其 count值为 2.0。
Meter filters 有很多复杂的功能,此处就不展开了。
Meter类别
Counter
Counter 是最简单的 Meter,其含义即为单值递增计数器,值必须是正数。
对于每种 Meter 而言,各有一套 fluent api 创建实例,Counter 的示例如下:
1 | Counter counter = Counter |
FunctionCounter
实际上和Counter
本身差别不大,只是将计数的行为抽象成 ToDoubleFunction 接口。 比如上下文中有一个 AtomicInteger ,那么 FunctionCounter 可以直接使用这个原子类来完成相关计数操作。这样的好处是对程序上下文而言,不需要感知该 Meter 的存在。
1 | MeterRegistry registry = new SimpleMeterRegistry(); |
Guage
记录指标的当前值,其典型应用是集合或Map的大小、线程的数量、CPU或内存情况。Guages 主要适用于有上边界的指标,要将其和 Counter 区分开。其Fluent Api 接口同样是将计数逻辑交给 ToDoubleFunction 接口。
1 | List<String> list = new ArrayList<>(); |
官方文档中还提供了用于观察数值、集合、Map的方法:
1 | // <T> T gauge(String name, Iterable<Tag> tags, @Nullable T obj, ToDoubleFunction<T> valueFunction) |
Micrometer 默认是不会创建对象的强引用(可以观察抽象类 MeterRegistry 的 newGauge 具体实现),如果 Guage 值无法观察到,那可能是对象已被执行垃圾回收。
Timer
Timer 用于记录时间相对较短的事件延迟情况和事件发生的频率,Timer 的所有实现至少记录了事件总时间消耗以及次数。以记录请求延时情况为例,由于可能瞬时记录大量信息,所以Timer每秒将更新很多次。
1 | Timer timer = Timer |
Timer 可以记录代码的执行耗时,其接收 Runnable 以及 Callable 参数。这个可以按照实际需要决定是否需要,个人不喜欢将执行代码的线程更换掉(还得考虑ThreadLocal及其他上下文情况),但不否认其拥有应用场景。
1 | timer.record(() -> dontCareAboutReturnValue()); |
Timer 还有个内部类Timer.Sample
,可以通过手动调用 start 和 stop 记录执行情况:
1 | Timer.Sample sample = Timer.start(registry); |
Distribution summaries
distribution summary 用来追踪事件的分布情况,其结构和 Timer 类似,但是其值不是由时间单位表示,也可以认为 Timer 只是特例化的 distribution summary ,但是 Micrometer 会针对监控系统自动适配存放时间序列的基本单位,所以只要是关于时间的指标,官方建议都使用 Timer。
官方示例的 Fluent API 使用如下:
1 | DistributionSummary summary = DistributionSummary |
添加 baseUnit 可以进一步提升整体的可移植性,因为 baseUnit 本身是监控系统命名转换的一部分,但如果省略它,也不会有负面影响。scale 作为比例因子,用来乘以所有的记录值。
Timers 和 distribution summaries 都支持收集数据后观察比例分布信息,有两种使用形式:一是直方图(histogram),即分配一堆桶,观察数据在各个桶的分布情况;二是百分比(percentiles),系统会为每一个 Meter 计算一个百分比近似值。以下面这个分布摘要在 Prometheus 中的展示为例:
1 | DistributionSummary summary = DistributionSummary.builder("ds.test") |
除了 max / count / sum 这三种数据外,还会显示 quantile 信息以及直方图的桶信息。publishPercentiles
即表示记录百分位数值,publishPercentileHistogram
表示记录直方图信息,而桶的个数可以由minimumExpectedValue
和maximumExpectedValue
控制。
此外,还有一个sla
方法,包含的桶信息会包含 sla 指定的数值。举例而言,下方的图中就包含了 18 和 19 两个桶。
1 | DistributionSummary summary = DistributionSummary.builder("ds.test") |
Timer 的 Histogram 和 Percentiles 使用也完全类似:
1 | Timer timer = Timer.builder("my.timer") |
其他
简单吐槽一下,相同名称、不同 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/