当前位置: 首页 > news >正文

Micrometer | 基础 - [各种 Meter]

目录
  • Meter 对应的 metrics 一览
  • Gauges
    • 简介
    • 声明
    • 手动增减 Gauge 的值
    • 不要使用基础数据类型(包含装箱类型)
    • 流式 Builder
    • Gauge 未上报或上报 NaN
    • TimeGauge
    • Multi-gauge
  • Counters(计数器)
    • 简介
      • 计数 & 速率
    • 声明
    • @Counted 注解
    • @MeterTag on Method Parameters
  • Timers
    • 简介
    • 声明
    • Max
    • record 代码块
    • 通过 Timer.Sample 暂存开始状态
    • @Timed 注解
    • @MeterTag on Method Parameters
    • Function-tracking Timers(函数追踪计时器)
    • Pause Detection
    • Memory Footprint Estimation
  • Distribution Summaries
    • 简介
    • 声明
    • Max
    • Scaling and Histograms
    • Memory Footprint Estimation
  • Long Task Timers

Meter 对应的 metrics 一览

假设声明 Meter 名为 a.b.c,监控系统选为 prometheus 时,不同 Meter 类型对应监控系统中实际 metrics 如下表

类型 metrics 说明
Gauge a_b_c
Counter a_b_c_total
Timer a_b_c_seconds_count
a_b_c_seconds_sum
a_b_c_seconds_max
默认单位就是秒
DistributionSummary a_b_c_<unit>_count
a_b_c_<unit>_sum
a_b_c_<unit>_max
unit 是指定的 baseUnit,如果不知道会省略

Gauges

简介

Gauge 用于获取当前值,其典型应用是获取运行态的集合、Map 大小或线程数
Gauge 适用于监控具有天然上限的东西

不建议使用 Gauge 去监控类似 <请求数量> 这样在应用实例生命周期中可以无上限增长的东西
如果可以使用 Counter 去计数,那就不应该使用 Gauge

Micrometer 主张 Gauge 的值是基于采样(而不是设置)获取的,因此不关心 <两次采样之间发生了什么> 这样的信息

说来了 Gauge 的值是瞬时检测的,需要的时候进行一次检测,即一次采样,检测到什么值就是什么值

在某个时间,Gauge 的值被采样并上报到 Metric 后端(如 prometheus),所有中间值都会丢失

说是丢失,其实两次采样中间的所有值压根没记

可以认为 Gauge 是一种 "heisen-gauge":其值只有在被观察时才会变化
与之对比的,其他的 Meter 类型都会累积中间值,直到把这个累积的数据发送到 Metric 后端的那一刻

声明

MeterRegistry 接口提供了 Gauge 的构造方法来监控数字值、函数、集合和 Map,如下例

// 常见形式之一:用于监控非数字的值
// 从 Gauge 获取的值都是数字,但 Gauge 也可以监控一个不直接是数字的对象,但声明时必须指定一个从这个对象上获取数值的方法
List<String> list = registry.gauge("listGauge", Collections.emptyList(), new ArrayList<>(), List::size); 
// 基于上例,更便捷的方法,但仅限于获取集合大小
List<String> list2 = registry.gaugeCollectionSize("listSize2", Tags.empty(), new ArrayList<>()); 
Map<String, Integer> map = registry.gaugeMapSize("mapGauge", Tags.empty(), new HashMap<>());

通过所有不同方式创建的 Gauge ,都只保留对被监控对象的弱引用,以免影响 GC

public DefaultGauge(Meter.Id id, @Nullable T obj, ToDoubleFunction<T> value) {super(id);this.ref = new WeakReference<>(obj);this.value = value;
}

手动增减 Gauge 的值

Gauge 可以用于跟踪所有可以设置值的 java.lang.Number 的子类,比如 java.util.concurrent.atomic 包下的

  • AtomicInteger
  • AtomicLong
  • AtomicDouble
  • 和其他类似的类

如下示例

// 让 Gauge 里持有对象的弱引用
AtomicInteger myGauge = registry.gauge("numberGauge", new AtomicInteger(0));
// 然后在其他位置更新它的值
myGauge.set(27);
myGauge.set(11);

需要注意的是,不同于其他 Meter 类型,用上述方式声明后,得到的是被监控对象而不是 Gauge 的引用
这主要是受 "heisen-gauge" 的影响:Gauge 一旦创建,就可以自给自足(的行使自己的功能),永远不需要与它进行交互,而是直接操作
因此这个方式可以返回被监控的对象,这样的返回实现了快速单行操作,同时完成对象的创建以及对这个对象的监控

此方式(手动设置)相对于 DoubleFunction 方式是比较少见的(说白了就是不推荐)
需要注意的是,频繁的设置被监控的 Number 导致的众多中间值是无用的,它们不会被发布
只有在上报数据的时刻,那个当时的值才会被发布到监控系统中(只有这个值的设置动作才是有效的,其他的设置动作其实是无用功)

不要使用基础数据类型(包含装箱类型)

尝试用基本数据类型或 java.lang 下的数字类型去构建 Gauge 的行为永远是错误的
因为这些数字是 "立即的",因此,基于它们的 Gauge 的值永远不会改变

尝试重新基于新的数值去重新注册 Meter 是行不通的,因为 registry 对每组唯一的 name + tag 的组合,只会维持一个 Meter
(也就是说,尝试重新注册的行为只会导致返回旧的 Meter,而不是成功注册同名的新的)

但是重新注册有可能会<间接发生>,如由 MeterFilter 修改两个不同 Gauge 的name、tag,并在 filter 后,这两个 Gauge 相同了
尝试重新注册会导致如下警告

WARNING: This Gauge has been already registered (MeterId{name='my.gauge', tags=[]}), the registration will be ignored. Note that subsequent logs will be logged at debug level.

为什么基于它们的 Gauge 值不会改变
以 Integer 为例,如下代码

Integer a = registry.gauge("aaaaaaa", Collections.emptyList(), new Integer(500), Integer::doubleValue); //0
while(true){a+=1; //1System.out.println(a);
}

我们最终可以见到,//1 处的值确实会变化,但是 //0 处通过 /actuator/prometheus 接口导出的值始终是 500
这是因为 Integer 是一个 "立即的" 值,它不能被重复的设置值,连它内部的 value 都是 final 的
换句话说,虽然引用都是 a,但其实它每个值都是 [拆箱 -> 计算 -> 重装箱] 得到的新对象
图片

图片

图片

所以,注册到 registry 中的 Meter,只是持有了 500 的那个值的 Integer 对象,相当于是一个单一值的副本,后面的值和这个 Meter 没关系,不会变化

流式 Builder

Gauge 接口提供了流式 builder,如下

Gauge gauge = Gauge.builder("gauge", myObj, myObj::gaugeValue).description("a description of what this gauge does") // optional.tags("region", "test") // optional.register(registry);

通常情况下,返回的 Gauge 实例只有在测试场景中才会起到一些作用
这是因为 Gauge 本身就是一旦完成注册,就会设置为自动跟踪某个值

Gauge 未上报或上报 NaN

省流:绑定的对象没了(或者压根就没出现过)

对于被 Gauge 监控的状态对象,micrometer 不会创建强引用,以免这些对象永远不会被 GC(因为永远被 Gauge 持有)
因此,用户应注意持有一个强引用(以使这些对象可以正常工作)
一旦 Gauge对象不再被强引用执行过 GC,对此对象 micrometer 就会上报 NaN 或不再上报
因为 Gauge 只持有对象的弱引用,GC 后对象会被回收,具体是 NaN 还是不再上报取决于 registry 实现(其实就是取决于监控系统)

如果看到某个 Gauge 正常上报了几分钟,然后消失了或者开始上报 NaN,那基本可以确定是其底层被监控对象已经被 GC 了

TimeGauge

TimeGauge 是专用于跟踪时间值的 Meter,它会按照不同 registry 实现(监控系统)期望的基本时间单位刮取值
TimeGauge 也可以如下例指定时间单位

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);

如上例,比如 prometheus 基本时间单位是秒,那 TimeGauge 刮取的值默认的单位就是秒

  • my_gauge_seconds 4.0 是 4 秒,因为它是用 <毫秒> 为单位定义的 4000
  • my_other_gauge_seconds 0.004 是 4 ms,因为它是用 <微秒> 为单位定义的 4000
# HELP my_gauge_seconds
# TYPE my_gauge_seconds gauge
my_gauge_seconds 4.0
# HELP my_other_gauge_seconds
# TYPE my_other_gauge_seconds gauge
my_other_gauge_seconds 0.004

Multi-gauge

Micrometer 还支持最后一种特殊的 GaugeMultiGauge,它有助于管理对一组不断增长(或缩小)的标准的测量
此特性可以使你从某处(比如一个 sql 查询结果),挑选略有区别但边界良好的一组标准,并将每一行作为 Gauge 上报,如下例

// 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);...// run this whenever you re-run your query
statuses.register(resultSet.stream().map(result -> Row.of(Tags.of("status", result.getAsString("status")), result.getAsInt("count"))).collect(toList()),true // whether to overwrite the previous value or only record it once
);

Counters(计数器)

简介

Counter 上报单一的指标:计数
Counter 允许指标以一个固定量递增,这个量必须是正数

计数 & 速率

如果以 Counter 为基础来构筑图表、告警,通常情况下最受关注的是单位时间内某些事件发生的速率(而不是计数值本身)

假设用 Counter 去统计了 GC 次数,
截至上一分钟 100 次,截至当前时间 110 次,这两个计数单拿出来其实没有什么意义
而在最近的一分钟里,应用发生了 10 次 GC,这个指标显然更有意义的多

(把计数兑换成速率并)视之为一个简单的队列,可以用于测量很多东西,比如某些数据的增删速率

在一开始,人们更容易想到去监控绝对计数,而不是监控速率
但是,绝对计数是 (使用中的某物的)速度 & (计量中的)应用存续时长 的函数

说白了,绝对计数不是一个独立的值,它相当于 速度*时间,或 ∑速度

因此,如果你忽视应用的存续时间这个因素,还用单位时间内计数器增长速率来配置大屏、告警,
那就会在应用过刚启动后的一段时间里,持续看到异常的数

Counter & Timer
在确认需要使用 Counter 之前,应该确保已经阅读了 Timer 部分

如果一个指标可以用 Timer 计时,或用 DistributionSummary 总结,就不要单纯的使用 Counter 计数
Timer /DistributionSummary 在它们计量的指标之外,还会额外提供事件计数指标
通常意义下,Timer 是比 Counter 更有意义的
Timer 具有一组需要记录的指标,其中包括定时时间的计数

如果你希望计数的场景里,需要考虑时间相关的因素,那直接使用 Timer 即可,不用额外声明一个 Counter

声明

短期时间窗口内 Counter 的速率可能展现出一定的波动,如下代码

Normal rand = ...; // a random generatorMeterRegistry registry = ...
// 通过 registry 创建 counter,同时可以指定计数器名和(如果有需要) tags
Counter counter = registry.counter("counter"); 
Flux.interval(Duration.ofMillis(10)).doOnEach(d -> {//有轻微正向偏差的随机游走if (rand.nextDouble() + 0.1 > 0) { //这就是与 counter 交互的方式//increment(n) 可以使 counter 增长一个非 1 的值counter.increment(); }}).blockLast();

Counter 接口本身提供了一个流式 builder,它提供对少数几个高频选项的访问,如下例可以一句话就完成从 registry 声明一个 Counter

Counter counter = Counter.builder("counter").baseUnit("beans") // optional.description("a description of what this counter does") // optional.tags("region", "test") // optional.register(registry);

@Counted 注解

micrometer-core 模块提供了 @Counted 注解,基于此注解,框架可以增加对特定类型方法,甚至更通用的对所有方法添加计数支持

特定方法如:服务于web请求端点的方法

同时,此模块还提供了一个切面(孵化中),使用途径如下:

  • 编译/加载时织入 AspectJ 切面(AspectJ 的织入时机包括编译前、编译后、加载时)
  • 可以解释 AspectJ 切面的框架(静态织入需要相关的支持)
  • 其他代理目标方法的方式,如 Spring AOP

以下是一个 Spring AOP 的简单案例

```java
@Configurationpublic class CountedConfiguration {@Beanpublic CountedAspect countedAspect(MeterRegistry registry) {return new CountedAspect(registry);}
}
```

被 AspectJ 代理的实例的某些方法上如果标注了 @Counted,可以通过CountedAspect 使它们生效,如下例

@Service
public class ExampleService {@Countedpublic void sync() {// @Counted will record the number of executions of this method...}@Async@Countedpublic CompletableFuture<?> async() {// @Counted will record the number of executions of this methodreturn CompletableFuture.supplyAsync(...);}}

@Counted 可以不填任何 value,

  • 此时 Meter 名会使用缺省值 method.counted
  • 随后 CountedAspecttagsBasedOnJoinPoint 的默认逻辑会自动把类名、方法名打成 tag,这确实可以做到区分各个 Meter Id

但是并不推荐这个做法,这样应用 A 与应用 B 会共享 Meter 名:method.counted,然而他们本来没啥联系
推荐 @Counted 中指定 Meter
至少可以区分到应用维度,而后同一个应用的不同声明点可以通过 Counted.extraTags() 进一步区分

@MeterTag on Method Parameters

Timers

简介

Timer 用于测量短期延迟和此类事件的频率

上文是直译,这里的意思应该是
一些事件会具有较短的持续时间(比如一个正常的请求,这里的较短应该是和 LongTaskTimer
Timer 用于测量这样的事件的时长与频率

Timer 的所有实现都至少上报两个独立的时间序列

  • 总时间
  • 时间计数

同时,还可以根据后端支持的内容上报其他时间序列,如最大值、百分位数、直方图

使用 Timer 时需要注意:

  • 不支持负值
  • 记录过长的总时长可能导致总时长溢出,总时长是用 Long.MAX_VALUE 记录的纳秒值,最长 292.3 年
    设想一个场景,典型的服务器请求响应时间。我们期望服务器可以快速想用众多请求,因此每秒 Timer 会被更新很多次
    换句话说对于一个 Timer 而言,并不是完全没有可能溢出这个最大时长

理由充分的情况下,可以通过后端指标改变适用的计时器单位,micrometer 本身不限制此行为
但为了防止(直接操作)可能导致的问题,micrometer 要求通过 TimeUnitTimer 实现进行交互
基于 Timer 的不同实现,micrometer 知道所有(micrometer)实现的倾向(比如prometheus倾向于以秒为单位)
并使用适用的单位上报计时,下面是 Timer 接口的部分

public interface Timer extends Meter {...void record(long amount, TimeUnit unit);void record(Duration duration);double totalTime(TimeUnit unit);
}

声明

Timer 本身提供了一个流式 builder

Timer timer = Timer.builder("my.timer").description("a description of what this timer does") // optional.tags("region", "test") // optional.register(registry);

Max

Timer 会生成一个表示最大值的 metric,并以 max 命名,即 <metername>_seconds_max
其基础实现中,此 max 值是基于时间窗口实现的,即 TimeWindowMax

比如 CumulativeTimer/StepTimer

这意味着这个值,是当时的时间窗口里最大的。如果在一个时间窗口的跨度里没有记录新值,那在下一个时间窗口开始时,max 值会被记录为 0

时间窗口大小主要受两个参数影响,这两个参数都在 DistributionStatisticConfig

  • bufferLength,是一个整数,默认值是 3
  • expiry,是一个时间跨度 Duration,这里的默认值是 2 分钟
  • 时间窗口总长 = bufferLength * expiry

但,不同的监控系统可能不会完全使用上述默认值,比如 Prometheus,其初始化时使用的 TimeWindowMax
环形缓冲区大小沿用 DistributionStatisticConfig.bufferLength,即 3
而其 expiry 默认的时间跨度会被覆盖成 1 分钟,也就是说 prometheus 的默认时间窗口是 3分钟

时间窗口最大值用于捕获在繁重的资源压力触发延迟并阻止指标发布后的后续间隔中的最大延迟

时间窗口的 max 值通常用于捕捉这样的最大值

  • 对某个资源定义了 Meter,这个 meter 会发布 metric,比如给一个 web 端点上加了个 Timer
  • 现在资源压力增加,这会导致此刻起的一段时间内,此 meter 记录的持续时间相对于正常值有延长
  • 时间窗口的 max 通常捕捉的是这样有延时的时长中的最大值

百分位也是时间窗口百分位,即 TimeWindowPercentileHistogram
直方图桶的行为与 Counter 类似,基于不同的后端,它们可能会作为 <累计值> 上报,也可能作为 <上报区间内的计数增量速率> 上报

record 代码块

Timer 的接口提供了易用性重载,用于记录行内定时,包括如下形式

timer.record(() -> dontCareAboutReturnValue());
timer.recordCallable(() -> returnValue());Runnable r = timer.wrap(() -> dontCareAboutReturnValue()); 
Callable c = timer.wrap(() -> returnValue());

上面是官方原文,这里的意思是,常规的 Timer 的使用方式本质上是多行的,下面是 Timer 接口最核心的方法

void record(long amount, TimeUnit unit);

它接受一个时长,然后 count + 1,sum + amount,然后把 amount 扔到时间窗口里去记录 max(不知道这里说的啥的,翻到最上面的 metric 表格看一眼)
但是这里的 amount 是个时长值,这个值一般是两个时间相减得到的,即正确情况下需要多行代码才能完成一次记录,类似下面

void aMethod(){long start = System.currentTimeMillis();// 被监控的逻辑,这里不是空的// ...代码...Timer timer = Timer.builder("meter.name").register(registry).record(System.currentTimeMillis()-start,TimeUnit.MICROSECONDS);
}

那么,现在有一段代码(被计时操作),如何在一行里完成 Timer 的记时?
通过把这段代码块打成一个 @FunctionalInterface 的方式即可,于是 Timer 提供了对应的重载方法,他们具有如下基本形式

  • 入参必然是 @FunctionalInterface,里面就是原始的逻辑,下文注释里注明了其核心方法
  • 这些重载可能有返回值,返回的是实际逻辑的返回
  • 这些重载方法已经提供了初步的实现,比如在 AbstractTimer 中,此实现已经完成计时与 record,所以可以省略
    所以,如果你直接从 Timer 接口重写上述方法的实现,并且没有在其中处理计时,就会使这些方法丢失行内 record 的特性
// T get();
<T> T record(Supplier<T> f);
// V call() throws Exception;
<T> T recordCallable(Callable<T> f) throws Exception;
// void run();
void record(Runnable f);

为了更进一步易用,Timer 在上述基本形式的基础上通过了扩展形式,如下所示(但还有很多)

default Runnable wrap(Runnable f) {return () -> record(f);}
default <T> Callable<T> wrap(Callable<T> f) {return () -> recordCallable(f);}
default int record(IntSupplier f) {return record((Supplier<Integer>) f::getAsInt);}

通过 Timer.Sample 暂存开始状态

Timer.Sample 实例可以暂存 Timer 的开始状态(就是调用了 Timer.start()),并且可以在后续停止(Timer.stop()),来触发计时
Sample 实例基于监控系统 registry 的时钟记录,使用步骤与案例如下所示

  • Timer.start() 同时得到 Sample 实例
  • 执行被计时的代码
  • 最后通过 Sample 停止计时,并完成时间的累计
Timer.Sample sample = Timer.start(registry);
// 被计时的代码
Response response = ...
sample.stop(registry.timer("my.timer", "response", response.status()));

需要注意的是,通过 Sample 实例停止计时时,才会决定将计时结果累计入哪个 Timer
这(结束时才决定往哪累计这个事)使我们可以根据被计时操作的结束状态,动态的决定 tag

其实不这么写也能动态的决定 tag,因为计时的时间一定是被计时操作结束后才能决定,此时所有需要被记录的东西都已经可以确定了

@Timed 注解

micrometer-core 模块提供了 @Timed 注解,基于此注解,框架可以增加对特定类型方法,甚至更通用的对所有方法添加计时支持

特定方法如:服务于web请求端点的方法

同时,此模块还提供了一个切面(孵化中),使用途径如下:

  • 编译/加载时织入 AspectJ 切面(AspectJ 的织入时机包括编译前、编译后、加载时)
  • 可以解释 AspectJ 切面的框架(静态织入需要相关的支持)
  • 其他代理目标方法的方式,如 Spring AOP
    以下是一个 Spring AOP 的简单案例
@Configurationpublic class TimedConfiguration {@Beanpublic TimedAspect timedAspect(MeterRegistry registry) {return new TimedAspect(registry);}
}

被 AspectJ 代理的实例的某些方法上如果标注了 @Timed,可以通过 TimedAspect 使它们生效,如下例

@Servicepublic class ExampleService {@Timedpublic void sync() {// @Timed 会记录此方法的执行时间,从它开始计时到它退出结束,无论是正常退出还是异常退出...}@Async@Timedpublic CompletableFuture<?> async() {// @Timed 会记录此方法的执行时间,从它开始计时到它返回的 CompletableFuture 完成后才结束,无论是正常退出还是异常退出return CompletableFuture.supplyAsync(...);}
}

TimedAspect 不支持带有 @Timed 的元注释
@Timed 本身是个元注解,即其上标记了 @Target(ElementType.ANNOTATION_TYPE)
若有另一个注解,定义为携带 @Timed 的元注解,如下面的定义,则不被 TimedAspect 支持(原因不明,回头试试)

@Timed
@Target(ElementType.ANNOTATION_TYPE)
public @interface XXX{}

@Timed 可以不填任何 value

  • 此时 Meter 名会使用缺省值 method.timed,这是 TimedAspect 里决定的
  • 随后 TimedAspecttagsBasedOnJoinPoint 的默认逻辑会自动把类名、方法名打成 tag

这确实可以做到区分各个 Meter Id
但是并不推荐这个做法,这样应用 A 与应用 B 会共享 Meter 名:method.timed,然而他们本来没啥联系
推荐 @Timed 中指定 Meter 名,至少可以区分到应用维度,而后同一个应用的不同声明点可以通过 Timed.extraTags() 进一步区分

@MeterTag on Method Parameters

micrometer-core 模块提供了 @MeterTag 注解,用于从方法入参上抽取 Meter 的 Tag
开启此能力需要在 @TimedAspect 上配置 MeterTagAnnotationHandler,如下所示

ValueResolver valueResolver = parameter -> "Value from myCustomTagValueResolver [" + parameter + "]";
// Example of a ValueExpressionResolver that uses Spring Expression Language
ValueExpressionResolver valueExpressionResolver = new SpelValueExpressionResolver();
// Setting the handler on the aspect
timedAspect.setMeterTagAnnotationHandler(new MeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver));

@MeterTag 是对 @Timed 的增强,所以使用 @MeterTag 的场景也必须使用 @Timed,可参考下面示例

下面一共有 4 个示例,每个示例均由三部分组成

  • 方法声明,请注意 @Timed 与 @MeterTag 注解的使用
  • 方法调用 case,在注释中标明,用于举一个实际调用的例子
  • 等效声明 Meter,在注释中标明,用于说明上面的声明与调用下,等效于声明了一个怎么样的 Meter
interface MeterTagClassInterface {//调用示例:service.getAnnotationForArgumentToString(15L)//  Meter:Timer.builder("method.timed").tags("test", "15")@Timedvoid getAnnotationForArgumentToString(@MeterTag("test") Long param);//调用示例:service.getAnnotationForTagValueResolver("foo")//  Meter:Timer.builder("method.timed").tags("test", "Value from myCustomTagValueResolver [foo]")@Timedvoid getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test);//调用示例:service.getAnnotationForTagValueExpression("15L")//  Meter:Timer.builder("method.timed").tags("test", "hello characters")@Timedvoid getAnnotationForTagValueExpression(@MeterTag(key = "test", expression = "'hello' + ' characters'") String test);//调用示例:service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe"))//  Meter:Timer.builder("method.timed").tags("value1", "value1: zxe", "value2", "value2: qwe")@Timedvoid getMultipleAnnotationsForTagValueExpression(@MeterTag(key = "value1", expression = "'value1: ' + value1")@MeterTag(key = "value2", expression = "'value2: ' + value2") DataHolder param);}

Function-tracking Timers(函数追踪计时器)

官方文档说这个不常用,那就忽略了

micrometer 还提供了一种更不常用的计时器模式,用于跟踪两个单调递增的函数:一个计数函数,一个计时函数

  • 部分监控系统,比如 Prometheus,把计数器(这里指上述两个函数)的累计值推送到后端
  • 其他监控系统推送的是两次推送间隔中增量的速率

此模式下,你可是使你监控系统的 Micrometer 实现选择是否对 Timer 进行速率规范化,这使你的 Timer 可以在不同监控系统直接迁移

IMap<?, ?> cache = ...; // suppose we have a Hazelcast cache
registry.more().timer("cache.gets.latency", Tags.of("name", cache.getName()), cache,c -> c.getLocalMapStats().getGetOperationCount(), c -> c.getLocalMapStats().getTotalGetLatency(),TimeUnit.NANOSECONDS 
);

Pause Detection

micrometer 可以通过 LatencyUtils 包来处理 协调性遗漏 问题
协调性遗漏:是指监控工具在结果集中丢失了大量潜在样本的问题

这与监控工具是怎么工作的有关,当它捕捉到一个应该被记录的坏数据点后,反而忽略了这个坏点
监控工具不会故意的去忽略它们,这仅基于监控工具的工作方式带来的副作用,遗漏坏数据点是一种意外

系统、虚拟机暂停导致计时统计向下倾斜时,LatencyUtils 会附加额外的计时

应该是这个意思暂停导致时间变长,但被监控系统丢弃,所以导致计时统计反而向下倾斜

如百分位或 SLO 计数,这样的分布式统计会受到暂停检测器具体实现的影响,暂停检测器会对暂停进行补偿

Micrometer 支持两种暂停检测器的实现

  • 时间漂移暂停检测器,1.0.10/1.1.4/1.2.0 之前,默认配置此检查器,其缺省配置下旨在向监控系统上报尽可能精准的指标

此类型检测器可以配置睡眠间隔暂停阈值,此 2 配置与 CPU 消耗成反比,即配置越小 CPU 消耗越大
对这两个配置而言,100ms 是比较合理的默认值,此值下可以提供对长暂停事件的检查,且消耗的 CPU 时间可以忽略不计

  • 无操作暂停检查器,1.0.10/1.1.4/1.2.0 及之后,默认配置此检查器

1.0.10/1.1.4/1.2.0 及之后,可以如下例方式配置时间漂移检测器

registry.config().pauseDetector(new ClockDriftPauseDetector(sleepInterval, pauseThreshold));
registry.config().pauseDetector(new NoPauseDetector());

Micrometer 后续可能会提供更进一步的访问

  • 比如,直接从 GC 日志中推断出部分暂停,这样就不再需要恒定的 CPU 负载

从 GC 日志推断,就不再需要消耗 CPU 去轮询着监测,这样就可以节省这里的 CPU 开销,即使这个开销也不大(如上问,双 100ms 配置下其消耗可以忽略)

  • 又比如,未来 JDK 可能会允许直接访问暂停事件

可能是这样:如果 JDK 提供了此能力,那 Micrometer 可以每次记录时都检查一下对应时间的暂停事件,并根据此进行补偿
而不需要向现在这样进行轮询检测

Memory Footprint Estimation

Timer 是最消耗内存的 Meter,其总内存占用,根据你做出的不同选择会显著的不一样
下面会列出一些影响的因素,或相关计算公式

  • R:环形缓冲区长度

默认值为 3,在 Timer.Builder#distributionStatisticBufferLength 中设置

  • B:直方图桶总量,可以是 SLO 边界或百分位直方图桶

如果某场景下 Timer 是适用的 Meter 类型
Timer 默认限制其最大最小期待值为 1ms ~ 30s,对百分位直方图产生 66 个桶

  • I:暂停补偿用的间隔推算器,1.7 K
  • Pp:百分位精度

默认值 1,通常设置范围 [0,3],可通过 Timer.Builder#percentilePrecision 设置
在动态直方图上计算百分位近似值时,保留的数字精度
此数字越大,百分位近似值越精确,但是需要消耗更多内存

  • M = 104 B,最大时间衰减

Micrometer 里和时间衰减有关的应该是是数据的过期时间,Micrometer 用 Duration 表示,所以这个大小可能是此对象的大小

  • Fb = 8b * B * R,固定边界直方图
  • Hdr(Pp),高动态范围直方图,与上面的固定边界直方图相对
    • Pp = 0:Hdr(Pp) = 1.9kb * R + 0.8kb
    • Pp = 1:Hdr(Pp) = 3.8kb * R + 1.1kb
    • Pp = 2:Hdr(Pp) = 18.2kb * R + 4.7kb
    • Pp = 3:Hdr(Pp) = 66kb * R + 33kb

接下来会列出一个表格,体现不同场景下的内存占用
下表数据基于无 tag,且环形缓冲区长度为 3 的场景下估算

  • 增加 tag 量,或增加环形缓冲区长度都会在一定程度上增加内存占用
  • 不同监控系统的 registry 实现也会影响内存总量
是否暂停检查 是否是客户端侧的百分位 直方图/SLO 计算公式 示例
❌️ ❌️ ❌️ M 104 B,≈ 0.1k
❌️ ❌️ ✅️ M + Fb 66 个 bucket 时 ≈ 6k
❌️ ✅️ ✅️ M + Hdr(Pp) 若附加一个 0.95 百分位,其他条件默认不变,增加 0.1 + 3.8 * 3 + 1.1 ≈ 12.6k
✅️ ❌️ ❌️ I + M 1.7KB + 104B ≈ 1.8Kb
✅️ ❌️ ✅️ I + M + Fb 66 个 bucket 时,≈ 7.8k,但其实这是按 B=276 计算时的数值
✅️ ✅️ ✅️ I + M + Hdr(Pp) 若附加一个 0.95 百分位,其他条件默认不变,增加 1.7 + 0.1 + 3.8 * 3 + 1.1 ≈14.3k

对 Prometheus 而言,始终 R==1,Timer.Builder 中可以配置但是不会生效
这是因为 Prometheus 希望它的直方图数据始终积累而不会滚动
这里说的,大概率是下面这段代码
图片

Distribution Summaries

简介

DistributionSummary 用于跟踪分布式事件。
DistributionSummary 结构上与 Timer 类似,但其值不用于表示时间单位
例如,基于 DistributionSummary 监控发送到某个服务器的请求的负载大小(报文体积)

可以参考 Meter 对应的 metrics 一览
DistributionSummary 也是和 timer 一样具有 3 个指标,count、sum、max
这俩在 Micrometer 的实现里也都是靠 record()来完成记录的,sum 都是由一个内部属性 amount 记录的
但是 Timer 的 amount 锁死了单位为时间,prometheus 下默认是秒
DistributionSummary 允许用户自行定义自己记录的指标是什么

声明

如下例可以创建

DistributionSummary summary = registry.summary("response.size");
与其他 Meter 类似,DistributionSummary 也提供了流式 builder
DistributionSummary summary = DistributionSummary.builder("response.size").description("a description of what this summary does") // optional.baseUnit("bytes") // optional .tags("region", "test") // optional.scale(100) // optional .register(registry);
  • baseUnit() 可以添加基本单位,可以使指标具有最大的可迁移性
    对于某些监控系统而言,单位是 Meter 命名约定的一部分(别某些了,prometheus 就是,导出 metric 时会自动把单位拼接在 metric 名上)
    如果你习惯性的去指定这个基本单位,即使你忘了你所使用的健康系统的命名约定,并且已经违反它了,也不会产生什么影响
    它大约得意思是:
    如果你不想无脑指定单位,那就要区分不同监控系统的命名约束,否则拼完 metirc 名可能是不符合它命名约束的
    如果你指定了,那就可以随便更换监控系统(所谓的最大可迁移性),最后拼完了就都是正常的

  • scale() 用于设定一个缩放系数,每个样本在记录时都会乘以这个系数

Max

DistributionSummary 会生成一个表示最大值的 metric,并以 max 命名,即 <metername>_<unit>_max
其基础实现中,此 max 值是基于时间窗口实现的,即 TimeWindowMax
比如在 CumulativeDistributionSummary/StepDistributionSummary
这意味着这个值,是当时的时间窗口里最大的。如果在一个时间窗口的跨度里没有记录新值,那在下一个时间窗口开始时,max 值会被记录为 0

时间窗口大小主要受两个参数影响,这两个参数都在 DistributionStatisticConfig

  • bufferLength,是一个整数,默认值是 3
  • expiry,是一个时间跨度 Duration,这里的默认值是 2 分钟
  • 时间窗口总长 = bufferLength * expiry

但,不同的监控系统可能不会完全使用上述默认值,比如 Prometheus,其初始化时使用的 TimeWindowMax
环形缓冲区大小沿用 DistributionStatisticConfig.bufferLength,即 3
而其 expiry 默认的时间跨度会被覆盖成 1 分钟,也就是说 prometheus 的默认时间窗口是 3分钟

时间窗口最大值用于捕获在繁重的资源压力触发延迟并阻止指标发布后的后续间隔中的最大延迟

时间窗口的 max 值通常用于捕捉这样的最大值

  • 对某个资源定义了 Meter,这个 meter 会发布 metric,比如给一个 web 端点上加了个 Timer
  • 现在资源压力增加,这会导致此刻起的一段时间内,此 meter 记录的持续时间相对于正常值有延长
  • 时间窗口的 max 通常捕捉的是这样有延时的时长中的最大值

百分位也是时间窗口百分位,即TimeWindowPercentileHistogram

Scaling and Histograms

Micrometer 的文档不怎么说人话,这个章节我实在不怎么理解他说了个啥,如有人可以精确的理解这个章节,请联系我调整此文档

Micrometer 预设的百分位直方图桶,是 1 - Long.MAX_VALUE 的所有整数
同时,minimumExpectedValuemaximumExpectedValue 两个参数用于控制百分位直方图桶的基数

如果 Micrometer 发现,你的最大最小值夹出了一个比较小的范围,
然后由 Micrometer 尝试把预设的桶缩放到你的 summary 的范围,那就没有其他的杠杆去控制桶的基数了

相反的,如果你 summary 的范围更受限,你应该用一个固定的系数你 summary 的范围
截至目前,常见用例是你 summary 的范围用一个比率来表示,即 [0,1],然后在这个基础上,用下面的代码得到 [0,100] 的值

DistributionSummary.builder("my.ratio").scale(100).register(registry)

通过此方式,[0,1] 的比率最终得到 [0,100] 的范围,然后我们可以指定 maximumExpectedValue == 100
如果你关心特定的比率,可以自定义 SLO(service level object),这样最大 100 的范围和你的 SLO 是有联系的

DistributionSummary.builder("my.ratio").scale(100).serviceLevelObjectives(70, 80, 90).register(registry)

上面是直译的,下面我强行尝试理解一下,不保证理解到位了,甚至不保证和文档说的是同一个事

我们可以这样理解直方图桶的选取逻辑

  • 确定完整的范围1 - Long.MAX_VALUE
  • 对完整范围划分桶,按预设的算法划分出了 276 个桶,注意这些桶整体上是指数分布在整个完整范围上的,如下图刻度所示

图片

  • 最后由最大最小值决定,从这些桶中截取需要使用的部分,如上图 A~B 所示,其间的桶即实际使用的部分

但是,如果最大最小值夹出了一个很小的区域

也就是说,不是最大值很小,也不是最小值很小,而是 (最大值 - 最小值) 很小,这可能导致区间内的值包含很少的桶

我们取一个极端的场景,如上图 C~B 所示,只截取了一个桶
就算在加上最大最小值本身,一共 3 个桶参与后面的直方图统计,明显其精度是有问题的

这通常是因为被监测值只具有狭小的取值范围,所以其对应的最大最小值的区间也会随着小

注意:被检测值是大是小不是关注的焦点,而是其上下限夹出来的范围

此时,有两种思路来应对这个场景

  • 让预设桶适配这个区间:将预设桶缩小到这个小区间上
  • 让这个区间的值适配预设桶:将实际值缩放到预设桶尺度上

Micrometer 认为第一个思路是行不通的,作为尝试我们把这个思路套用在上例中,它只能如下实现

  • 确定新的完整范围为 C ~ B

因为要预设桶适配这个区间,那必然是以这个区间为准

  • 对完整范围 C ~ B 划分桶,将预设桶缩小到这个小区间上

这里其实是想重新划分桶,但预设桶的数值和算法是固定的,无法灵活调整
所以只能先按原先的完整范围计算原始预设桶,然后把原始预设桶等比例缩放 C ~ B 里
即 所有新桶 = C + 所有原始桶 * (C - B) / Long.MAX_VALUE,大约这样计算

  • 最后应该在所有的预设桶中,用最大/最小值截选出实际投入使用的桶,但是它们已经被用于确定新的完整范围了

于是将军了,此路不通,只能用第二个思路:将实际值缩放到预设桶尺度上
说的直白一些,这个问题就是被监测值的区间落在了预设桶分布稀疏的区间
破局的核心思想是让被监测值的区间落在预设桶分布相对密集的区间里,无论放大还是缩小被监控值都有可能做到这一点
这也是为什么把 scale 译为缩放因子

Memory Footprint Estimation

DistributionSummary 的总内存占用,根据你做出的不同选择会显著的不一样
下面会列出一些影响的因素,或相关计算公式

  • R:环形缓冲区长度

默认值为 3,在 DistributionSummary.Builder#distributionStatisticBufferLength 中设置

  • B:直方图桶总量,可以是 SLO 边界或百分位直方图桶

默认情况下,DistributionSummary 没有最大最小预期值,所以 Micrometer 提供了 276 个预设的直方图桶
准备上报一个直方图时,应该用 minimumExpectedValue/maximumExpectedValue 锁定 DistributionSummary 范围

  • Pp:百分位精度

默认值 1,通常设置范围 [0,3],可通过 DistributionSummary.Builder#percentilePrecision 设置
在动态直方图上计算百分位近似值时,保留的数字精度
此数字越大,百分位近似值越精确,但是需要消耗更多内存

  • M = 104 B,最大时间衰减

Micrometer 里和时间衰减有关的应该是是数据的过期时间,Micrometer 用 Duration 表示,所以这个大小可能是此对象的大小

  • Fb = 8b * B * R,固定边界直方图
  • Hdr(Pp),高动态范围直方图,与上面的固定边界直方图相对
    • Pp = 0:Hdr(Pp) = 1.9kb * R + 0.8kb
    • Pp = 1:Hdr(Pp) = 3.8kb * R + 1.1kb
    • Pp = 2:Hdr(Pp) = 18.2kb * R + 4.7kb
    • Pp = 3:Hdr(Pp) = 66kb * R + 33kb

接下来会列出一个表格,体现不同场景下的内存占用
下表数据基于无 tag,且环形缓冲区长度为 3 的场景下估算

  • 增加 tag 量,或增加环形缓冲区长度都会在一定程度上增加内存占用
  • 不同监控系统的 registry 实现也会影响内存总量
是否是客户端侧的百分位 直方图/SLO 计算公式 示例
❌️ ❌️ M 104 B,0.1k
❌️ ✅️ M + Fb 66 个 bucket 时,≈ 6k
✅️ ✅️ M + Hdr(Pp) 若附加一个 0.95 百分位,其他条件默认不变,增加 0.1 +3.8*3+1.1,≈ 12.6k

对 Prometheus 而言,始终 R==1,DistributionSummary.Builder 中可以配置但是不会生效
这是因为 Prometheus 希望它的直方图数据始终积累而不会滚动
这里说的,大概率是下面这段代码
图片

Long Task Timers

http://www.jsqmd.com/news/776364/

相关文章:

  • Bottleneck在微服务架构中的应用:如何实现跨服务统一限流
  • 医院锦旗定制哪家好?感谢医生专用,杨浦及周边可加急制作 - 品牌推荐大师
  • 2026诚信婚恋服务平台:无套路婚姻介绍所指南 - 深度智识库
  • 兰州儿童摄影推荐:想拍那种风格?这5家各有所长! - charlieruizvin
  • 2026年郑州铝单板与氟碳铝单板市场深度横评:5大品牌选购完全指南 - 年度推荐企业名录
  • 2026年乌鲁木齐断桥平开窗选购指南:源头直供vs中间商陷阱全对比 - 优质企业观察收录
  • 2026年清镇别墅装修深度横评:从毛坯到拎包入住的高端一站式定制指南 - 企业名录优选推荐
  • 2026年食品级聚乙烯储罐相关塑料制品厂家推荐:重庆谨百塑料制品有限公司,饮用水储罐/加厚耐酸碱塑料储罐/耐酸碱储罐等多种周转筐及相关塑料制品 - 品牌推荐官
  • 2026年郑州铝单板全景指南:从氟碳到双曲,本地头部供应商与竞品深度横评 - 年度推荐企业名录
  • 2026年郑州铝单板全景选购指南:从氟碳涂层到双曲异形,5大品牌深度横评与官方联系方式汇总 - 年度推荐企业名录
  • 2026年乌鲁木齐断桥平开窗选购指南:源头工厂直供vs中间商加价,如何快速找到靠谱供应商 - 优质企业观察收录
  • 国内专业砖雕厂家实力排行:工艺与交付能力实测对比 - 奔跑123
  • 2026年乌鲁木齐断桥平开窗源头直供|龙秋系统门窗省30%中间商差价 - 优质企业观察收录
  • 2026年郑州铝单板、氟碳铝单板、蜂窝铝单板全景采购指南:5大品牌深度横评与官方联系方式汇总 - 年度推荐企业名录
  • 大润发购物卡回收攻略:高效处理闲置卡 - 购物卡回收找京尔回收
  • 2026年乌鲁木齐断桥平开窗选购指南:源头工厂直供vs市场品牌深度横评 - 优质企业观察收录
  • 滴,预批函!澳星出国最新爱尔兰移民成功案例批量来袭! - 博客万
  • 2026年贵阳全屋整装深度横评:从预算陷阱到透明决算的一站式解决方案 - 企业名录优选推荐
  • 2026年乌鲁木齐断桥平开窗选购指南:源头工厂直供vs中间商加价的真相对比 - 优质企业观察收录
  • 2026年实测8款降AI率工具(含免费降AI率版)!这款论文降AIGC工具救活我的99%AI率 - 降AI实验室
  • 循环队列
  • Openaibot:模块化LLM聊天机器人框架的设计、部署与优化实践
  • 国内主流吸附塔制造企业实力排行及核心能力解析 - 奔跑123
  • Saltcorn CLI工具详解:命令行操作与批量处理技巧
  • 《DNESP32P4开发指南_V1.0》第二十一章 RGBLCD实验
  • 【AISMM模型落地指南】:上市前90天合规冲刺清单与3大高频雷区避坑手册
  • 2026年清镇别墅装修与贵阳旧房翻新深度横评:从预算黑洞到透明决算的一站式整装完全指南 - 企业名录优选推荐
  • 2026年山东沥青筑路设备采购全攻略:德州霖垚与业界四大品牌深度横评 - 精选优质企业推荐官
  • 2026年郑州铝单板与氟碳铝单板市场深度横评:5大品牌选购指南与工程应用实测 - 年度推荐企业名录
  • 2026年郑州铝单板、氟碳铝单板、蜂窝铝单板全景选购指南:从幕墙到吊顶,官方联系与深度横评 - 年度推荐企业名录