跳到主要内容

计数器

DeepSeek V3 中英对照 Counters

计数器报告一个单一的指标:计数。Counter 接口允许你按固定数量递增,该数量必须为正数。

提示

永远不要手动统计那些你可以使用 Timer 计时或使用 DistributionSummary 汇总的内容!无论是 Timer 还是 DistributionSummary,除了其他测量值外,它们始终会发布事件的计数。

在基于计数器构建图表和警报时,通常最关注的是测量某个事件在给定时间间隔内发生的速率。考虑一个简单的队列。你可以使用计数器来测量各种指标,例如项目插入和移除的速率。

一开始,人们可能会倾向于设想可视化绝对计数而不是比率,但绝对计数通常是某物使用速度被检测的应用程序实例的寿命两者的函数。构建基于某个时间间隔内计数器比率的仪表板和警报,可以忽略应用程序的寿命,让你在应用程序启动后很长时间内仍能看到异常行为。

备注

在开始使用计数器之前,请务必阅读计时器部分,因为计时器会记录定时事件的计数,作为计时指标套件的一部分。对于那些你打算计时的代码片段,你不需要单独添加一个计数器。

以下代码模拟了一个真实计数器,其速率在短时间内表现出一定的扰动:

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();
java
  • 大多数计数器都可以通过注册表本身创建,并附带一个名称,还可以选择性地附带一组标签。

  • 一个略微正向偏置的随机游走。

  • 这是你与计数器交互的方式。你也可以调用 counter.increment(n) 来在一次操作中增加超过一个单位。

Counter 接口上的流畅构建器提供了对不常用选项的访问,例如基本单位和描述。你可以通过调用 register 方法将计数器注册为构建的最后一步:

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

@Counted 注解

micrometer-core 模块包含一个 @Counted 注解,框架可以使用该注解为特定类型的方法(例如那些服务于 Web 请求端点的方法)或更普遍地为所有方法添加计数支持。

此外,micrometer-core 中还包含了一个孵化中的 AspectJ 切面。你可以通过编译/加载时 AspectJ 织入,或者通过框架功能(如 Spring AOP)来解释 AspectJ 切面并以其他方式代理目标方法,在你的应用程序中使用它。以下是一个 Spring AOP 配置的示例:

@Configuration
public class CountedConfiguration {
@Bean
public CountedAspect countedAspect(MeterRegistry registry) {
return new CountedAspect(registry);
}
}
java

应用 CountedAspect 使得 @Counted 可以在 AspectJ 代理实例的任何方法上使用,如下例所示:

@Service
public class ExampleService {

@Counted
public void sync() {
// @Counted will record the number of executions of this method
...
}

@Async
@Counted
public CompletableFuture<?> async() {
// @Counted will record the number of executions of this method
return CompletableFuture.supplyAsync(...);
}

}
java

方法参数上的 @MeterTag

为了支持在方法参数上使用 @MeterTag 注解,你需要配置 @CountedAspect 以添加 CountedMeterTagAnnotationHandler

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
countedAspect.setMeterTagAnnotationHandler(
new CountedMeterTagAnnotationHandler(aClass -> valueResolver, aClass -> valueExpressionResolver));
java

假设我们有以下接口。

interface MeterTagClassInterface {

@Counted
void getAnnotationForTagValueResolver(@MeterTag(key = "test", resolver = ValueResolver.class) String test);

@Counted
void getAnnotationForTagValueExpression(
@MeterTag(key = "test", expression = "'hello' + ' characters'") String test);

@Counted
void getAnnotationForArgumentToString(@MeterTag("test") Long param);

@Counted
void getMultipleAnnotationsForTagValueExpression(
@MeterTag(key = "value1", expression = "'value1: ' + value1") @MeterTag(key = "value2",
expression = "'value2: ' + value2") DataHolder param);

}
java

当它的实现被调用时使用不同的参数(记住,实现也需要使用 @Counted 注解进行标注),将会创建以下计数器:

// Example for returning <toString()> on the parameter
service.getAnnotationForArgumentToString(15L);

assertThat(registry.get("method.counted").tag("test", "15").counter().count()).isEqualTo(1);

// Example for calling the provided <ValueResolver> on the parameter
service.getAnnotationForTagValueResolver("foo");

assertThat(registry.get("method.counted")
.tag("test", "Value from myCustomTagValueResolver [foo]")
.counter()
.count()).isEqualTo(1);

// Example for calling the provided <ValueExpressionResolver>
service.getAnnotationForTagValueExpression("15L");

assertThat(registry.get("method.counted").tag("test", "hello characters").counter().count()).isEqualTo(1);

// Example for using multiple @MeterTag annotations on the same parameter
// @MeterTags({ @MeterTag(...), @MeterTag(...) }) can be also used
service.getMultipleAnnotationsForTagValueExpression(new DataHolder("zxe", "qwe"));

assertThat(registry.get("method.counted")
.tag("value1", "value1: zxe")
.tag("value2", "value2: qwe")
.counter()
.count()).isEqualTo(1);
java
备注

CountedAspect 不支持带有 @Counted 的元注解。

函数跟踪计数器

Micrometer 还提供了一种较少使用的计数器模式,用于跟踪单调递增的函数(即随着时间的推移保持不变或增加但从不减少的函数)。某些监控系统(如 Prometheus)会将计数器的累计值推送到后端,而其他系统则会发布计数器在推送间隔内的递增速率。通过采用这种模式,您可以让适用于您的监控系统的 Micrometer 实现选择是否对计数器进行速率归一化,从而使您的计数器在不同类型的监控系统中保持可移植性。

Cache cache = ...; // suppose we have a Guava cache with stats recording on
registry.more().counter("evictions", tags, cache, c -> c.stats().evictionCount()); 1
java
  • evictionCount() 是一个单调递增的函数,从它生命周期开始,每次缓存逐出时都会递增。

函数跟踪计数器与监控系统的速率归一化功能(无论是查询语言的特性还是数据推送到系统的方式)相结合,在函数本身的累积值之上增加了一层丰富性。你可以推断出值的增长速率,判断该速率是否在可接受的范围内,是否随着时间增加或减少,等等。

注意

Micrometer 无法保证函数的单调性。通过使用此签名,您基于对其定义的了解来断言其单调性。

FunctionCounter 接口本身提供了一个流畅的构建器,用于访问较少使用的选项,例如基本单位和描述。你可以通过调用 register(MeterRegistry) 将其作为构建的最后一步来注册计数器:

MyCounterState state = ...;

FunctionCounter counter = FunctionCounter
.builder("counter", state, state -> state.count())
.baseUnit("beans") // optional
.description("a description of what this counter does") // optional
.tags("region", "test") // optional
.register(registry);
java