本文从官方文档整理出一篇系统化全面了解的文章, 后续可能会慢慢补上源码层面的解析: https://micrometer.io/docs
学习本文的目的在于深入了解中间件的监控模块的设计, 先看看主流的做法于核心思想
本文的引用来的笔者的理解于备注
需要做的是:
- 先理解功能存在的理由
- 设计模式
- 源码核心实现
1. 简介
Micrometer是基于 JVM 的应用程序的指标检测库。它为最流行的监视系统提供了检测客户端的简单门面[门面设计模式],使您可以检测基于 JVM 的应用程序代码,而不会被供应商锁定。它旨在为指标收集活动增加很少甚至没有开销,同时最大限度地提高指标工作的可移植性。
提供了标准化接口,像Slf4j一样提供门面
Micrometer不是分布式跟踪系统或事件记录器。但是,在Micrometer 1.10 中,我们确实提供了一个插件机制,允许您添加包括跟踪功能在内的功能。您可以在Micrometer追踪文档中阅读有关此内容的更多信息。
提供分布式链路追踪的基础支持
为了更好地理解这些不同类型的系统(指标、分布式跟踪和日志记录)之间的差异,我们推荐 Adrian Cole 的演讲,题为“可观察性”。 3种方式。
简而言之,需要分布式的了解
1.1 代码依赖关系
最小依赖为:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
链路追踪模块
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
</dependency>
上下文扩展模块
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>context-propagation</artifactId>
</dependency>
还有适配各种中间件的[注册表]依赖
1.2 支持的监控系统>监控系统的三个重要特征
在选择技术方案时,非常非常重要的一个指标就是社区活跃度,micrometer几乎适配了所有监控系统
Micrometer包含一个带有仪器 SPI 的核心模块、一组包含各种监控系统实现的模块(每个模块称为注册表)和一个测试套件。您需要了解监控系统的三个重要特征:
SPI: 系统解耦非常常用
下面的三个特征,其实思考一下不难理解,简化介绍就是: 监控数据维度、数据采集、数据推送
维度
系统是否支持使用标签键/值对来扩充指标名称。如果系统不是维度的,则它是分层的,这意味着它仅支持平面指标名称。将指标发布到分层系统时,Micrometer会展平标签键/值对集,并将其添加到名称中。
也就是可以定义标签, 这些标签可以用来区分维度
聚合
在这种情况下,我们指的是在规定的时间间隔内聚合一组样本。某些监测系统期望应用程序在发布之前将某些类型的离散样本(如计数)转换为速率。其他系统希望始终发送累积值。还有一些人对此没有任何意见。
客户端采集聚合数据, 采集的数据量可随意定义
发布
一些系统希望在闲暇时轮询应用程序的指标,而另一些系统则希望定期将指标推送给它们。
客户端系统包括: Elastic、Influx、JMX
服务端包括:Prometheus…
支持的监控系统
太多了,口熟能详的包括:Prometheus、JMX、Graphite
不同特征的是不一样的,不关键反正很多
2. 代码层面设计
代码层面的设计也是核心概念的设计, 同样重要的理论与实际内容
2.1 注册表
Meter是收集关于应用程序的一组度量(我们单独称之为度量)的接口。Micrometer中的Meters 是从MeterRegistry中创建并保存的。每个支持的监控系统都有一个MeterRegistry的实现。注册中心的创建方式因实现而异。
适配每个监控系统接入Micrometer: 也就是Prometheus有自己的MeterRegistry, 一种门面模式屏蔽接入复杂度
SimpleMeterRegistry
Micrometer 包括一个SimpleMeterRegistry在内存中保存每个仪表的最新值并且不会将数据导出到任何地方。如果您还没有首选的监控系统,您可以使用SimpleMeterRegistry注册表开始创建指标:
MeterRegistry registry = new SimpleMeterRegistry();
1. 上面介绍的三特性, 其三"发布"在SimpleMeterRegistry中是没有的, 也就是SimpleMeterRegistry是不会发布数据的;
2. 从这里可以看出Register的运用应该是: 比如有个一个Tcp连接,为TCP连接创建一个Register, 为TCP的发送数据大小、传输时长等创建Meter;
CompositeMeterRegistry
Micrometer 提供了一个CompositeMeterRegistry可以添加多个注册表的工具,让您可以同时将指标发布到多个监控系统:
CompositeMeterRegistry composite = new CompositeMeterRegistry();
Counter compositeCounter = composite.counter("counter");
compositeCounter.increment(); (1)
SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple); (2)
compositeCounter.increment(); (3)
- 增量是 NOOP,直到组合中有注册表。此时计数器的计数仍为 0。
- 名为的计数器counter已注册到简单注册表。
- 简单注册表计数器以及组合中任何其他注册表的计数器都会递增。
前面说过注册表是面向[特征第三 发布]的
Metrics.globalRegistry
Micrometer 提供了一个静态全局注册表Metrics.globalRegistry和一组静态构建器,用于基于此注册表生成仪表(注意这globalRegistry是一个复合注册表):
代码写的好那不就得统一, 细分注册表注册到统一的注册表
class MyComponent {
Counter featureCounter = Metrics.counter("feature", "region", "test"); (1)
void feature() {
featureCounter.increment();
}
void feature2(String type) {
Metrics.counter("feature.2", "type", type).increment(); (2)
}
}
class MyApplication {
void start() {
// wire your monitoring system to global static state
Metrics.addRegistry(new SimpleMeterRegistry()); (3)
}
}
- 在可能的情况下(尤其是在仪器性能至关重要的情况下),将Meter实例存储在字段中以避免在每次使用时查找它们的名称或标签。
- 当需要从本地上下文确定标签时,您别无选择,只能在方法体内构造或查找 Meter。查找成本只是一次哈希查找,因此对于大多数用例来说是可以接受的。
- 在使用类似代码创建仪表后添加注册表是可以的Metrics.counter(…)。这些仪表被添加到每个注册表,因为它绑定到全局组合。
整体看下来这个是不适合具体模块使用的
2.2 Meter
Micrometer 支持一组Meter,包括Timer、Counter、Gauge、DistributionSummary、LongTaskTimer、FunctionCounter、FunctionTimer和TimeGauge。
不同的仪表类型会产生不同数量的时间序列指标。例如,只有一个指标表示 Gauge、Timer可以衡量定时事件的计数和所有定时事件的总时间。
每一种Meter记录一种维度的数据, 比如Gauge只记录一个数值,这个数值可以用来监控当前的CPU使用率, 而Timer可以用来记录历史的CPU使用率
一个Meter是由其名称和维度唯一标识的。我们可以互换使用术语“维度”和“标签”,Micrometer接口是Tag,因为它更短。作为一般规则,应该可以使用名称作为枢轴。维度让一个特定的命名指标被切片以向下钻取和推理数据。这意味着,如果仅选择了名称,您可以使用其他维度向下钻取并了解所显示值的原因。
如下图所示: 一个CpuRegister中包含三个Meter,但是记录的三个维度
2.3 命名规范
Meter
Meter的采用逗号(.)来命名。提供NamingConvention可以通过在注册表上实施和设置来覆盖注册表的默认命名约定:
就是规范Meter的命名,同时提供命名转换器来兼容对接监控平台
registry.config().namingConvention(myCustomNamingConvention);
通过适当的命名约定,以下在 Micrometer 中注册的计时器在各种监控系统中本机看起来不错:
registry.timer("http.server.requests");
其他系统的命名规范, 不一致时可以通过NamingConvention做转换
- Prometheus ——http_server_requests_duration_seconds
- Atlas -httpServerRequests
- Graphite -http.server.requests
- InfluxDB -http_server_requests
Tag
直接上例子
- 入参一是Meter命名
- 入参二、三是Tag的命名
registry.counter("database.calls", "db", "users")
registry.counter("http.requests", "uri", "/api/users")
符合分析监控需求的命名tag,在后续groupby、select的时候方便统计
2.4 公共Tag
通过公共Tag,可以用来标识当前环境等
也就是每个Meter都会被赋予公共Tag
registry.config().commonTags("stack", "prod", "region", "us-east-1");
registry.config().commonTags(Arrays.asList(Tag.of("stack", "prod"), Tag.of("region", "us-east-1"))); // equivalently
3. 细节设计
3.1 Meter过滤器
在Register中配置过滤器可以用来: 拒绝、转换、配置 Meter
registry.config()
.meterFilter(MeterFilter.ignoreTags("too.much.information"))
.meterFilter(MeterFilter.denyNameStartsWith("jvm"));
3.1.1 拒绝或接受Meter
相比过滤器更精细,提供三种状态:DENY(拒绝)、NEUTRAL(正常接收)、ACCEPT(立刻接收,不接受后续过滤影响)
new MeterFilter() {
@Override
public MeterFilterReply accept(Meter.Id id) {
if(id.getName().contains("test")) {
return MeterFilterReply.DENY;
}
return MeterFilterReply.NEUTRAL;
}
}
过滤器支持链式调用, 与提供一些通用过滤器
3.1.2 转换Meter
这个过滤器的方法可以用来转换Meter
new MeterFilter() {
@Override
public Meter.Id map(Meter.Id id) {
if(id.getName().startsWith("test")) {
return id.withName("extra." + id.getName()).withTag("extra.tag", "value");
}
return id;
}
}
用途不多的样子
3.1.3 配置分布统计
配置某个Meter只统计某百分几的数据,比如一种请求的SLO(99线)
下面配置的就是统计0.9到0.95之间的数据
new MeterFilter() {
@Override
public DistributionStatisticConfig configure(Meter.Id id, DistributionStatisticConfig config) {
if (id.getName().startsWith("prefix")) {
return DistributionStatisticConfig.builder()
.publishPercentiles(0.9, 0.95)
.build()
.merge(config);
}
return config;
}
};
这个还是有用的
3.2 速率聚合
三特征之二 速率聚合
Micrometer 可以在发布Meter数据之前在客户端进行速率聚合,作为服务器上查询的一部分临时进行。它根据监控系统期望的风格来累积指标。
这里率的意思是按服务器希望的数据长度进行聚合, 比如一定时间内或者一定标准内的统计成一个值
比如Timer类型Meter 可以按照一定时间范围内的值汇总给服务器
比如:虽然服务器说五秒一个记录,但是80的请求都在第一秒,你按在一秒多记录一次,这样子更精确
3.2.1 服务器端
执行服务器端速率数学运算的监控系统期望在每个发布间隔报告绝对值。例如,在每个发布间隔发送自应用程序开始以来计数器的所有增量的绝对计数。
假设我们有一个稍微正偏的随机游走,它选择每 10 毫秒增加一次计数器。如果我们在像 Prometheus 这样的系统中查看原始计数器值,我们会看到一个逐步单调递增的函数(步长的宽度是 Prometheus 轮询或抓取数据的间隔)。
每十毫秒+1 一秒就会加100次, 通常我们拉取监控数据的频率在5秒, 那么每五秒拉取一次数据, 客户端返回的是当前的数值, 服务端会根据先比上次增长的数据差进行计算显示;
3.2.2 客户端
- 期望速率聚合数据。鉴于对于大多数生产目的而言,我们的决策应该基于速率而不是绝对值这一关键见解,这样的系统受益于不必做更少的数学运算来满足查询。
- 有相对较少或没有数学运算,可以让我们通过查询对数据进行评级聚合。对于这些系统,发布预先聚合的数据是构建有意义的表示的唯一方法。
Micrometer 通过为当前发布间隔累积数据的步长值有效地维护速率数据。当轮询步长值时(例如发布时),如果步长值检测到当前间隔已经过去,它将当前数据移动到“上一个”状态。直到下一次当前数据覆盖它时,才会报告之前的状态。下图显示了当前和先前状态的交互,以及轮询:
可以看到虽然Current值一直在变化, 但是服务器获取的预览值是一个具有时间段内的代表值
但是可以发现发布数据的间隔不是服务器定好的十秒拉取,具体看下面解释
poll 函数返回的值始终是rate per second * interval。如果上图中显示的步长值表示计数器的值,我们可以说计数器在第一个时间间隔内看到“每秒 0.3 个增量”,这在第二个时间间隔内随时可报告给后端。
千分尺计时器至少跟踪计数和总时间作为单独的测量值。假设我们以 10 秒的间隔配置发布,并且我们看到 20 个请求,每个请求花费 100 毫秒。然后,对于第一个间隔:
- count= 10 秒 *(20 个请求/10 秒)= 20 个请求
- totalTime= 10 秒 * (20 * 100 毫秒 / 10 秒) = 2 秒
该统计数据本身就有意义:它是吞吐量count的度量。表示间隔内所有请求的总延迟。此外:totalTime
totalTime / count= 2 秒/20 个请求 = 0.1 秒/请求 = 100 毫秒/请求
这是平均延迟的有用度量。当将相同的想法应用于分布摘要totalAmount和count源自分布摘要时,该度量称为分布平均值。平均延迟只是按时间(计时器)测量的分布摘要的分布平均值。一些监控系统(例如 Atlas)提供了从这些统计数据计算分布平均值的工具,而 Micrometer 包括totalTime并count作为单独的统计数据。其他的(例如 Datadog)没有内置这种操作,而 Micrometer 计算分布平均客户端并发布。
发布间隔的传输速率足以推断大于或等于发布间隔的任何时间窗口内的速率。在我们的示例中,如果服务继续接收 20 个请求,每个请求在给定分钟内每 10 秒间隔花费 100 毫秒,我们可以说:
- count Micrometer每 10 秒报告一次“20 个请求” 。监控系统将这六个 10 秒间隔相加,得出每分钟有 120 个请求的结论。请注意,进行此求和的是监控系统,而不是 Micrometer。
- totalTime 测微计每 10 秒间隔报告一次“2 秒” 。监控系统可以将一分钟内的所有总时间统计数据相加,得出分钟间隔内总时间的“12 秒”。然后,平均延迟正如我们预期的那样:12 秒/120 个请求 = 100 毫秒/请求。
上面认真看看会得到一个结论: Meter会在本地统计一份变化速率, 也就是虽然服务器说十秒是间隔, 但是在这时秒内一开始的2秒的变化占据了90%, 那汇报数据的时候会产生两个点, 来体现这两秒的变化情况;
3.3 Counters
计数器报告单一指标:Counters。该Counter接口允许您按固定数量递增,该数量必须为正数。
Counters是Meter的一种, 记录一个数值例如: 请求数量
当从计数器构建图表和警报时,您通常应该最感兴趣的是测量某个事件在给定时间间隔内发生的速率。考虑一个简单的队列。您可以使用计数器来测量各种事物,例如插入和移除项目的速率。
例: 在一个高并发的几秒内,精准记录短暂的时间片段的增长. 具体的体现就是在Counters在降低和增长在某个节点的快速变化(如变化率达到了5000%),而不是固定时间去记录(这样子会丢失快速变化精确度)- 速率
代码例子:
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();
3.3.1 FunctionCounter
提供获取count的函数,而不是Meter自己统计
关联AtomicInteger 来获取值
SimpleMeterRegistry registry = new SimpleMeterRegistry();
AtomicInteger atomicInteger= new AtomicInteger();
atomicInteger.set(111);
FunctionCounter counter = FunctionCounter
.builder("counter", atomicInteger, AtomicInteger::get)
.baseUnit("beans") // optional
.description("a description of what this counter does") // optional
.tags("region", "test") // optional
.register(registry);
System.out.println(counter.count());
3.4 Gauges 仪表
仪表是获取当前值的句柄。仪表的典型示例是集合或映射的大小或处于运行状态的线程数。
仪表对于监视具有自然上限的事物很有用。我们不建议使用仪表来监控诸如请求计数之类的东西,因为它们可以在应用程序实例的生命周期内无限增长: 比如油表
实例:
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<>());
可以看到相比于Counters的无界限记录, Gauges 更适合有界限的统计
这个是弱引用需要注意监控对象被引用不被回收
Micrometer 的立场是应对仪表进行采样而不是设置,因此没有关于采样之间可能发生的情况的信息。当仪表值被报告给度量后端时,仪表上设置的任何中间值都会丢失,因此一开始就设置这些中间值没有什么价值。
没发生变化是否会不推送数据呢?
需要注意的是Gauges是靠装饰模式, 装饰后放回对象的引用, 在java里String、Long、Integer等基础类型的包装类型的引用值是会变化的, 所以用这种非固定的对象, 是会丢失监控的;
Java基础知识了,比如Long可能相同的值,但是引用值不一样
3.4.1 Gauges 仪表报告 NaN
也就是上面说的, 你可能创建的多变类型导致引用值丢失,被GC回收了
3.4.2 TimeGauge
TimeGauge是一个跟踪时间值的专用量规,可缩放到每个注册表实现所期望的基本时间单位。
TimeGauge可以注册TimeUnit如下:
AtomicInteger msTimeGauge = new AtomicInteger(4000);
AtomicInteger usTimeGauge = new AtomicInteger(4000);
TimeGauge.builder("my.gauge", msTimeGauge, TimeUnit.MILLISECONDS, AtomicInteger::get).register(registry);
TimeGauge.builder("my.other.gauge", usTimeGauge, TimeUnit.MICROSECONDS, AtomicInteger::get).register(registry);
3.4.3 Multi-gauge
Micrometer 支持最后一种特殊类型的Gauge,称为MultiGauge,以帮助管理衡量不断增长或缩小的标准列表。此功能允许您从诸如 SQL 查询之类的东西中选择一组边界明确但略有变化的标准,并将每行的一些指标报告为Gauge.
以下示例创建一个MultiGauge:
// SELECT count(*) from job group by status WHERE job = 'dirty'
MultiGauge statuses = MultiGauge.builder("statuses")
.tag("job", "dirty")
.description("The number of widgets in various statuses")
.baseUnit("widgets")
.register(registry);
...
// 每当重新运行查询时,定期运行此命令
statuses.register(
resultSet.stream()
.map(result -> Row.of(Tags.of("status", result.getAsString("status")), result.getAsInt("count")))
.collect(toList())
);
核心场景就是一个业务场景但又有细分区别, 比如多核心CPU, 共用一个MultiGauge,每个核心区分记录
3.5 Timers 定时器
Timer(计时器)适用于记录耗时比较短的事件的执行时间,通过时间分布展示事件的序列和发生频率。所有的Timer的实现至少记录了发生的事件的数量和这些事件的总耗时,从而生成一个时间序列。Timer的基本单位基于服务端的指标而定,但是实际上我们不需要过于关注Timer的基本单位,因为Micrometer在存储生成的时间序列的时候会自动选择适当的基本单位。
定时器是在一定时间内获取记录的最大值, 在每个时间片段内的值是独立的, 也就是在下个时间片段会被清0
场景: 记录每十秒内的最大请求时长, 观察当前服务器压力下的最大延迟
Timer timer = Timer
.builder("my.timer")
.description("a description of what this timer does") // optional
.tags("region", "test") // optional
.register(registry);
调用接口:
相同的是都要给定时间单位
public interface Timer extends Meter {
...
void record(long amount, TimeUnit unit);
void record(Duration duration);
double totalTime(TimeUnit unit);
//记录业务代码执行时长
void record(Runnable f);
Runnable wrap(Runnable f);
}
传入业务代码记录时长
3.5.1 存储开始状态在 Timer.Sample
意思就是: 可以先记录记录后决定怎么投递给Register, 比然后后置定义标签
Timer.Sample sample = Timer.start(registry);
// do stuff
Response response = ...
sample.stop(registry.timer("my.timer", "response", response.status()));
例: 比如在业务代码发生异常后, 后置决定标签, 方便分类
3.6 Function-tracking Timers
Micrometer 还提供了一种更不常用的定时器模式,它跟踪两个单调递增的函数(一个保持不变或随时间增加但从不减少的函数):一个计数函数和一个总时间函数。一些监控系统,例如 Prometheus,将计数器的累积值(在这种情况下适用于计数和总时间函数)推送到后端,但其他系统会发布计数器在推送间隔内递增的速率。通过采用这种模式,您可以让监控系统的 Micrometer 实现选择是否对计时器进行速率规范化,并且您的计时器在不同类型的监控系统之间保持可移植性。
IMap<?, ?> cache = ...; // suppose we have a Hazelcast cache
registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache,
//核心两个Lambda: 提供计数函数
c -> c.getLocalMapStats().getGetOperationCount(), (1)
//核心两个Lambda: 提供总时间函数
c -> c.getLocalMapStats().getTotalGetLatency(),
TimeUnit.NANOSECONDS (2)
);
- getGetOperationCount()是一个单调递增的函数,随着每个缓存从其生命周期的开始获取而递增。
- 这代表 所代表的时间单位getTotalGetLatency()。每个注册表实现指定其预期的基本时间单位是什么,并且报告的总时间将按比例缩放到该值。
看起来是适合在: 统计一个接口历史总用时与次数; 后面实际试试看
3.7 暂停检测
Micrometer 使用该LatencyUtils包来补偿协调遗漏 ——由系统和 VM 暂停引起的额外延迟,这些延迟会使您的延迟统计数据向下倾斜。分布统计数据(例如百分位数和 SLO 计数)受暂停检测器实施的影响,该实施会在各处添加额外的延迟以补偿暂停。
Micrometer 支持两种暂停检测器实现:基于时钟漂移的检测器和无操作检测器。在 Micrometer 1.0.10/1.1.4/1.2.0 之前,时钟漂移检测器默认配置为报告尽可能准确的指标,无需进一步配置。从 1.0.10/1.1.4/1.2.0 开始,no-op 检测器是默认配置的,但时钟漂移检测器可以配置如下一个示例所示。
基于时钟漂移的检测器具有可配置的睡眠间隔和暂停阈值。CPU 消耗与暂停检测精度成反比sleepInterval。这两个值的 100 毫秒是一个合理的默认值,可以提供对长暂停事件的良好检测,同时消耗的 CPU 时间可以忽略不计。
您可以按如下方式自定义暂停检测器:
simpleMeterRegistry.config().pauseDetector(new ClockDriftPauseDetector(Duration.ofMillis(100), Duration.ofMillis(100)));
simpleMeterRegistry.config().pauseDetector(new NoPauseDetector());
将来,我们可能会提供更多的检测器实现。在某些情况下,可以从 GC 日志记录中推断出一些暂停,例如,不需要持续的 CPU 负载,无论多么小。此外,未来的 JDK 可能会提供对暂停事件的直接访问。
大概意思是在语言层面的一些延迟,或者说系统的临时停摆,导致在业务上的监控产生了一些不准确。 通过配置这两种检测器,可以补偿些数据的精确
但笔者看来语言层面的特性业务需要被记录的,不应该加入这些检测器
3.8 内存占用预估
计时器是最耗内存的仪表,它们的总占用空间可能会有很大差异,具体取决于您选择的选项。下表内存消耗基于各种功能的使用。这些数字假设没有标签且环形缓冲区长度为 3。添加标签会稍微增加总数,增加缓冲区长度也是如此。总存储量也可能因注册表实现而有所不同。
- R = 环形缓冲区长度。我们假设所有示例中的默认值为 3。R 设置为Timer.Builder#distributionStatisticBufferLength。
- B = 总直方图桶。可以是 SLO 边界或百分位数直方图桶。默认情况下,计时器被限制在 1 毫秒的最小预期值和 30 秒的最大预期值,在适用时为百分位直方图产生 66 个桶。
- I = 暂停补偿的间隔估计器。1.7 KB。
- M = 时间衰减最大值。104 字节。
- Fb = 固定边界直方图。8b * B * R。
- Pp = 百分位精度。默认为1,一般在[0, 3]范围内。Pp 设置为Timer.Builder#percentilePrecision。
- Hdr(Pp) = 高动态范围直方图。
-
- 当 Pp = 0 时:1.9kb * R + 0.8kb
-
- 当 Pp = 1 时:3.8kb * R + 1.1kb
-
- 当 Pp = 2 时:18.2kb * R + 4.7kb
-
- 当 Pp = 3 时:66kb * R + 33kb
- 当 Pp = 3 时:66kb * R + 33kb
内存的消耗大头在设置百分比精确度,也就是Timer在统计的片段大小
3.9 DistributionSummary
DistributionSummary 跟踪事件的分布。它在结构上类似于定时器,但记录的是不代表时间单位的值。例如,您可以使用DistributionSummary 来衡量到达服务器的请求的负载大小:
DistributionSummary summary = registry.summary("response.size");
max基本DistributionSummary实现(例如CumulativeDistributionSummary和)的 最大值(命名为)StepDistributionSummary是时间窗口最大值 ( TimeWindowMax)。这意味着它的值是一个时间窗口内的最大值。如果没有记录时间窗口长度的新值,则最大值将在新时间窗口开始时重置为 0。时间窗口大小是仪表注册表的步长,除非 expiry inDistributionStatisticConfig明确设置为另一个值。在资源压力过大触发延迟并阻止指标发布后,使用时间窗口 max 捕获后续间隔中的最大延迟。百分位数也是时间窗百分位数 ( TimeWindowPercentileHistogram)。
3.9.1 Scale(缩放)&直方图
这个意思是我们可以通过设置缩放最大直方图基数,也就是不管多大都缩放到100的比例,这样子我们可以方便设置其他比如SLO
DistributionSummary.builder("my.ratio")
.scale(100)
.serviceLevelObjectives(70, 80, 90)
.register(registry)
这个不难理解,在不确定的上限下,统一缩放到100可以方便查看比率
3.9.3 内存情况
和Timer一样大头在设置百分比精度
3.10 长任务Timer
长任务计时器是一种特殊类型的计时器,可让您在正在测量的事件仍在运行时测量时间。一个普通的 Timer 只记录任务完成后的持续时间。
长任务计时器至少发布以下统计信息:
- 活动任务数
- 活动任务的总持续时间
- 活动任务的最长持续时间
与常规 不同Timer,长任务计时器不会发布有关已完成任务的统计信息。
LongTaskTimer longTaskTimer = LongTaskTimer
.builder("long.task.timer")
.description("a description of what this timer does") // optional
.tags("region", "test") // optional
.register(registry);
比较适合在监控项目核心线程池的执行情况
3.11 直方图和百分位数
计时器和分布摘要支持收集数据以观察它们的百分位分布。查看百分位数的主要方法有两种:
- 百分位数直方图:Micrometer 将值累积到底层直方图,并将一组预定的桶发送到监控系统。监控系统的查询语言负责计算此直方图的百分位数。histogram_quantile目前,只有 Prometheus、Atlas 和 Wavefront 分别通过、:percentile和支持基于直方图的百分位数近似hs()。如果您以 Prometheus、Atlas 或 Wavefront 为目标,则更喜欢这种方法,因为您可以跨维度聚合直方图(通过对一组维度中的桶值求和)并从直方图中得出可聚合的百分位数。
- 客户端百分位数:Micrometer 计算每个仪表 ID(一组名称和标签)的百分位数近似值,并将百分位数值发送到监控系统。这不如百分位直方图灵活,因为不可能跨标签聚合百分位近似值。然而,它为不支持基于直方图的服务器端百分位计算的监控系统提供了对百分位分布的某种程度的洞察。
以下示例构建了一个带有直方图的计时器:
Timer.builder("my.timer")
.publishPercentiles(0.5, 0.95) // median and 95th percentile (1)
.publishPercentileHistogram() // (2)
.serviceLevelObjectives(Duration.ofMillis(100)) // (3)
.minimumExpectedValue(Duration.ofMillis(1)) // (4)
.maximumExpectedValue(Duration.ofSeconds(10))
- publishPercentiles:用于发布在您的应用程序中计算的百分位值。这些值不可跨维度聚合。
- publishPercentileHistogramhistogram_quantile:用于发布适用于在 Prometheus(通过使用)、Atlas(通过使用:percentile)和 Wavefront(通过使用)中计算可聚合(跨维度)百分位近似值的直方图hs()。对于 Prometheus 和 Atlas,结果直方图中的桶由 Micrometer 根据 Netflix 根据经验确定的生成器进行预设,以在大多数真实世界的计时器和分布摘要上产生合理的误差范围。minimumExpectedValue默认情况下,生成器会产生 276 个桶,但 Micrometer 仅包括那些在和设置的范围内的桶maximumExpectedValue,包括在内。默认情况下,Micrometer 将定时器固定在 1 毫秒到 1 分钟的范围内,每个定时器维度产生 73 个直方图桶。publishPercentileHistogram对不支持可聚合百分位数近似值的系统没有影响。这些系统没有直方图。
- serviceLevelObjectives:用于发布具有由您的 SLO 定义的存储桶的累积直方图。publishPercentileHistogram当与支持可聚合百分位数的监控系统一起使用时,此设置会向已发布的直方图添加额外的桶。当在不支持可聚合百分位数的系统上使用时,此设置会导致发布仅包含这些桶的直方图。
- minimumExpectedValue/ maximumExpectedValue:控制运送的桶数publishPercentileHistogram并控制底层 HdrHistogram 结构的准确性和内存占用。
说的比较多,但是结论还是,图标需要一个明确上限基数,方便百分比统计
由于将百分位数传送到监控系统会生成额外的时间序列,因此通常最好不要在作为依赖项包含在应用程序中的核心库中配置它们。相反,应用程序可以通过使用仪表过滤器为某些计时器和分发摘要集打开此行为。
这类配置可以使用过滤器统一配置
例如,假设我们在一个公共库中有一些计时器。我们在这些计时器名称前加上myservice:
registry.timer("myservice.http.requests").record(..);
registry.timer("myservice.db.requests").record(..);
我们可以使用仪表过滤器为两个计时器打开客户端百分位数:
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;
}
});
实际对接
使用各个类型的Meter采集监控数据,在监控系统的实际效果
安装Prometheus
目前非常广泛使用的开源监控告警系统
1. 下载Prometheus
在Prometheus的官方网站(https://prometheus.io/download/)上下载最新版本的Prometheus。选择Windows版本的压缩包,解压到任意目录。
2. 配置Prometheus
在解压后的目录中,找到prometheus.yml文件,用文本编辑器打开。在文件中添加以下内容:
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:8080']
这个配置告诉Prometheus监控本地的8080端口,也就是提供数据的服务器
3. 启动Prometheus
在命令行中进入Prometheus的目录,执行以下命令启动Prometheus:
.\prometheus.exe
这个命令会启动Prometheus,并开始监控配置文件中指定的目标。
4. 访问Prometheus
在浏览器中访问http://localhost:9090,就可以看到Prometheus的Web界面了。在这个界面中,可以查看Prometheus监控的指标,并进行查询和图表展示等操作。
以上就是在Windows上安装Prometheus的步骤。需要注意的是,Prometheus的配置文件需要根据实际情况进行修改,以监控正确的目标。
安装Grafana
Prometheus负责抓取数据与存储,Grafana 提供复杂的图标展示
以下是在Windows上安装Grafana的步骤:
1. 下载Grafana
在Grafana的官方网站(https://grafana.com/grafana/download)上下载最新版本的Grafana。选择Windows版本的压缩包,解压到任意目录。
2. 启动Grafana
在解压后的目录中,找到grafana-server.exe文件,双击运行。这个命令会启动Grafana,并在浏览器中打开Grafana的Web界面 http://localhost:3000。
3. 配置Grafana
在浏览器中打开Grafana的Web界面后,需要进行一些基本的配置。首先,输入用户名和密码(默认为admin/admin),然后点击“Log In”按钮。
接下来,需要配置数据源。在左侧菜单中选择“Configuration”->“Data Sources”,然后点击“Add data source”按钮。选择要使用的数据源类型(例如Prometheus),然后输入数据源的URL和其他相关信息。最后,点击“Save & Test”按钮测试数据源是否配置成功。
4. 创建仪表盘
在左侧菜单中选择“Create”->“Dashboard”,然后点击“Add panel”按钮添加一个新的面板。在面板中选择要展示的指标和图表类型,然后进行配置。最后,点击“Save”按钮保存仪表盘。
以上就是在Windows上安装Grafana的步骤。需要注意的是,Grafana的配置需要根据实际情况进行修改,以展示正确的指标和图表。
对各类型的Meter设计实际用例
基本架构图
Micrometer源码
在实践使用一段时间后补出来