3W字干货深入分析基于Micrometer和Prometheus实现度量和监控的方案

前提

最近线上的项目使用了 spring-actuator 做度量统计收集,使用 Prometheus 进行数据收集, Grafana 进行数据展示,用于监控生成环境机器的性能指标和业务数据指标。一般,我们叫这样的操作为"埋点"。 SpringBoot 中的依赖 spring-actuator 中集成的度量统计API使用的框架是 Micrometer ,官网是 micrometer.io 。在实践中发现了业务开发者滥用了 Micrometer 的度量类型 Counter ,导致无论什么情况下都只使用计数统计的功能。这篇文章就是基于 Micrometer 分析其他的度量类型API的作用和适用场景。全文接近3W字,内容比较干,希望能够耐心阅读,有所收获。

Micrometer提供的度量类库

Meter 是指一组用于收集应用中的度量数据的接口,Meter单词可以翻译为"米"或者"千分尺",但是显然听起来都不是很合理,因此下文直接叫 Meter ,直接当成一个专有名词,理解它为度量接口即可。 Meter 是由 MeterRegistry 创建和保存的,可以理解 MeterRegistry 是 Meter 的工厂和缓存中心,一般而言每个JVM应用在使用Micrometer的时候必须创建一个 MeterRegistry 的具体实现。Micrometer中, Meter 的具体类型包括: Timer , Counter , Gauge , DistributionSummary , LongTaskTimer , FunctionCounter , FunctionTimer 和 TimeGauge 。下面分节详细介绍这些类型的使用方法和实战使用场景。而一个 Meter 具体类型需要通过名字和 Tag (这里指的是Micrometer提供的Tag接口)作为它的唯一标识,这样做的好处是可以使用名字进行标记,通过不同的 Tag 去区分多种维度进行数据统计。

MeterRegistry

MeterRegistry 在 Micrometer 是一个抽象类,主要实现包括:

  • 1、 SimpleMeterRegistry :每个 Meter 的最新数据可以收集到 SimpleMeterRegistry 实例中,但是这些数据不会发布到其他系统,也就是数据是位于应用的内存中的。
  • 2、 CompositeMeterRegistry :多个 MeterRegistry 聚合,内部维护了一个 MeterRegistry 的列表。
  • 3、全局的 MeterRegistry :工厂类 io.micrometer.core.instrument.Metrics 中持有一个静态 final 的 CompositeMeterRegistry 实例 globalRegistry

当然,使用者也可以自行继承 MeterRegistry 去实现自定义的 MeterRegistry 。 SimpleMeterRegistry 适合做调试的时候使用,它的简单使用方式如下:

MeterRegistry registry = new SimpleMeterRegistry();
Counter counter = registry.counter("counter");
counter.increment();

CompositeMeterRegistry 实例初始化的时候,内部持有的 MeterRegistry 列表是空的,如果此时用它新增一个 Meter 实例, Meter 实例的操作是无效的:

CompositeMeterRegistry composite = new CompositeMeterRegistry();

Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); // <- 实际上这一步操作是无效的,但是不会报错

SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple);  // <- 向CompositeMeterRegistry实例中添加SimpleMeterRegistry实例

compositeCounter.increment();  // <-计数成功

全局的 MeterRegistry 的使用方式更加简单便捷,因为一切只需要操作工厂类 Metrics 的静态方法:

Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = Metrics.counter("counter", "tag-1", "tag-2");
counter.increment();

Tag与Meter的命名

Micrometer 中, Meter 的命名约定使用英文逗号(dot,也就是".")分隔单词。但是对于不同的监控系统,对命名的规约可能并不相同,如果命名规约不一致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。 Micrometer 中使用英文逗号分隔单词的命名规则,再通过底层的命名转换接口 NamingConvention 进行转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖 NamingConvention 实现自定义的命名转换规则: registry.config().namingConvention(myCustomNamingConvention); 。在 Micrometer 中,对一些主流的监控系统或者存储系统的命名规则提供了默认的转换方式,例如当我们使用下面的命名时候:

MeterRegistry registry = ...
registry.timer("http.server.requests");

对于不同的监控系统或者存储系统,命名会自动转换如下:

  • 1、Prometheus - http_server_requests_duration_seconds。
  • 2、Atlas - httpServerRequests。
  • 3、Graphite - http.server.requests。
  • 4、InfluxDB - http_server_requests。

其实 NamingConvention 已经提供了5种默认的转换规则:dot、snakeCase、camelCase、upperCamelCase和slashes。

另外, Tag (标签)是 Micrometer 的一个重要的功能,严格来说,一个度量框架只有实现了标签的功能,才能真正地多维度进行度量数据收集。Tag的命名一般需要是有意义的,所谓有意义就是可以根据 Tag 的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。假设我们需要监控数据库的调用和Http请求调用统计,一般推荐的做法是:

MeterRegistry registry = ...
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")

这样,当我们选择命名为"database.calls"的计数器,我们可以进一步选择分组"db"或者"users"分别统计不同分组对总调用数的贡献或者组成。一个反例如下:

MeterRegistry registry = ...
registry.counter("calls", "class", "database", "db", "users");

registry.counter("calls", "class", "http", "uri", "/api/users");

通过命名"calls"得到的计数器,由于标签混乱,数据是基本无法分组统计分析,这个时候可以认为得到的时间序列的统计数据是没有意义的。可以定义全局的Tag,也就是全局的Tag定义之后,会附加到所有的使用到的Meter上(只要是使用同一个MeterRegistry),全局的Tag可以这样定义:

MeterRegistry registry = ...
registry.config().commonTags("stack", "prod", "region", "us-east-1");
// 和上面的意义是一样的
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1")));

像上面这样子使用,就能通过主机,实例,区域,堆栈等操作环境进行多维度深入分析。

还有两点点需要注意:

  • 1、 Tag 的值必须 不为NULL 。
  • 2、 Micrometer 中, Tag 必须成对出现,也就是 Tag 必须设置为 偶数个 ,实际上它们以Key=Value的形式存在,具体可以看 io.micrometer.core.instrument.Tag 接口:
public interface Tag extends Comparable<Tag> {
    String getKey();

    String getValue();

    static Tag of(String key, String value) {
        return new ImmutableTag(key, value);
    }

    default int compareTo(Tag o) {
        return this.getKey().compareTo(o.getKey());
    }
}

当然,有些时候,我们需要过滤一些必要的标签或者名称进行统计,或者为Meter的名称添加白名单,这个时候可以使用 MeterFilter 。 MeterFilter 本身提供一些列的静态方法,多个 MeterFilter 可以叠加或者组成链实现用户最终的过滤策略。例如:

MeterRegistry registry = ...
registry.config()
    .meterFilter(MeterFilter.ignoreTags("http"))
    .meterFilter(MeterFilter.denyNameStartsWith("jvm"));

表示忽略"http"标签,拒绝名称以"jvm"字符串开头的 Meter 。更多用法可以参详一下 MeterFilter 这个类。

Meter 的命名和 Meter 的 Tag 相互结合,以命名为轴心,以 Tag 为多维度要素,可以使度量数据的维度更加丰富,便于统计和分析。

Meters

前面提到Meter主要包括: Timer , Counter , Gauge , DistributionSummary , LongTaskTimer , FunctionCounter , FunctionTimer 和 TimeGauge 。下面逐一分析它们的作用和个人理解的实际使用场景(应该说是生产环境)。

Counter

Counter 是一种比较简单的 Meter ,它是一种单值的度量类型,或者说是一个单值计数器。 Counter 接口允许使用者使用一个固定值(必须为正数)进行计数。准确来说: Counter 就是一个增量为正数的单值计数器。这个举个很简单的使用例子:

MeterRegistry meterRegistry = new SimpleMeterRegistry();
Counter counter = meterRegistry.counter("http.request", "createOrder", "/
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值