给你的SpringBoot做埋点监控--JVM应⽤度量框架Micrometer 这世上有三样东西是别⼈抢不⾛的:⼀是吃进胃⾥的⾷物,⼆是藏在⼼中的梦想,三是读进⼤脑的书
JVM应⽤度量框架Micrometer实战
前提
spring-actuator做度量统计收集,使⽤Prometheus(普罗⽶修斯)进⾏数据收集,Grafana(增强ui)进⾏数据展⽰,⽤于监控⽣成环境机器的性能指标和业务数据指标。⼀般,我们叫这样的操作为”埋点”。SpringBoot中的依赖spring-actuator中集成的度量统计API使⽤的框架是Micrometer,官⽹是Micrometer.io。在实践中发现了业务开发者滥⽤了Micrometer的度量类型Counter,导致⽆论什么情况下都只使⽤计数统计的功能。这篇⽂章就是基于Micrometer分析其他的度量类型API的作⽤和适⽤场景。
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:⼯⼚类instrument.Metrics中持有⼀个静态final的CompositeMeterRegistry实例globalRegistry。
当然,使⽤者也可以⾃⾏继承MeterRegistry去实现⾃定义的MeterRegistry。SimpleMeterRegistry适合做调试的时候使⽤,它的简单使⽤⽅式如下:
MeterRegistry registry = new SimpleMeterRegistry();
Counter counter = unter("counter");
counter.increment();
CompositeMeterRegistry实例初始化的时候,内部持有的MeterRegistry列表是空的,如果此时⽤它新增⼀个Meter实例,Meter实例的操作是⽆效的CompositeMeterRegistry composite = new CompositeMeterRegistry();
Counter compositeCounter = unter("counter");
compositeCounter.increment(); // <- 实际上这⼀步操作是⽆效的,但是不会报错
SimpleMeterRegistry simple = new SimpleMeterRegistry();
composite.add(simple); // <- 向CompositeMeterRegistry实例中添加SimpleMeterRegistry实例
compositeCounter.increment(); // <-计数成功
全局的MeterRegistry的使⽤⽅式更加简单便捷,因为⼀切只需要操作⼯⼚类Metrics的静态⽅法:
Metrics.addRegistry(new SimpleMeterRegistry());
Counter counter = unter("counter", "tag-1", "tag-2");
counter.increment();
Tag与Meter的命名
Micrometer中,Meter的命名约定使⽤英⽂逗号(dot,也就是”.”)分隔单词。但是对于不同的监控系统,对命名的规约可能并不相同,如果命名规约不⼀致,在做监控系统迁移或者切换的时候,可能会对新的系统造成破坏。Micrometer中使⽤英⽂逗号分隔单词的命名规则,再通过底层的命名转换接⼝NamingConvention进⾏转换,最终可以适配不同的监控系统,同时可以消除监控系统不允许的特殊字符的名称和标记等。开发者也可以覆盖NamingConvention实现⾃定义的命名转换规则:fig().namingConvention(myCustomNamingConvention);。在Micrometer中,对⼀些主流的监控系统或者存储系统的命名规则提供了默认的转换⽅式,例如当我们使⽤下⾯的命名时候:
MeterRegistry registry = ...
registry.timer("quests");
对于不同的监控系统或者存储系统,命名会⾃动转换如下:
1、Prometheus - http_server_requests_duration_seconds。
2、Atlas - httpServerRequests。
3、Graphite - quests。
4、InfluxDB - http_server_requests。
其实NamingConvention已经提供了5种默认的转换规则:dot、snakeCase、camelCase、upperCamelCase和slashes。
另外,Tag(标签)是Micrometer的⼀个重要的功能,严格来说,⼀个度量框架只有实现了标签的功能,才能真正地多维度进⾏度量数据收集。Tag的命名⼀般需要是有意义的,所谓有意义就是可以根据Tag的命名可以推断出它指向的数据到底代表什么维度或者什么类型的度量指标。假设我们需要监控数据库的调⽤和Http请求调⽤统计,⼀般推荐的做法是:
MeterRegistry registry = ...
这样,当我们选择命名为”database.calls”的计数器,我们可以进⼀步选择分组”db”或者”users”分别统计不同分组对总调⽤数的贡献或者组成。⼀个反例如下:
MeterRegistry registry = ...
"class", "database",
"db", "users");
"class", "http",
"uri", "/api/users");
通过命名”calls”得到的计数器,由于标签混乱,数据是基本⽆法分组统计分析,这个时候可以认为得到的时间序列的统计数据是没有意义的。可以定义全局的Tag,也就是全局的Tag定义之后,会附加到所有的使⽤到的Meter上(只要是使⽤同⼀MeterRegistry),全局的Tag可以这样定义:
MeterRegistry registry = ...
"class", "database",
"db", "users");
"class", "http",
"uri", "/api/users");
MeterRegistry registry = ...
// 和上⾯的意义是⼀样的
像上⾯这样⼦使⽤,就能通过主机,实例,区域,堆栈等操作环境进⾏多维度深⼊分析。
还有两点点需要注意:
1、Tag的值必须不为null。
2、Micrometer中,Tag必须成对出现,也就是Tag必须设置为偶数个,实际上它们以Key=Value的形式存在,具体可以
看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) {
Key()Key());
}
}
当然,有些时候,我们需要过滤⼀些必要的标签或者名称进⾏统计,或者为Meter的名称添加⽩名单,这个时候可以使⽤MeterFilter。MeterFilter本⾝提供⼀些列的静态⽅法,多个MeterFilter可以叠加或者组成链实现⽤户最终的过滤策略。例如:
MeterRegistry registry = ...
.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 = unter("quest", "createOrder", "/order/create");
counter.increment();
System.out.asure()); // [Measurement{statistic='COUNT', value=1.0}]
使⽤场景:
Counter的作⽤是记录XXX的总量或者计数值,适⽤于⼀些增长类型的统计,例如下单、⽀付次数、Http请求总量记录等等,通过Tag可以区分不同的场景,对于下单,可以使⽤不同的Tag标记不同的业务来源或者是按⽇期划分,对于Http请求总量记录,可以使⽤Tag区分不同的URL。⽤下单业务举个例⼦:
//实体
@Data
public class Order {
private String orderId;
private Integer amount;
private String channel;
private LocalDateTime createTime;
}
public class CounterMain {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
static {
Metrics.addRegistry(new SimpleMeterRegistry());
}
public static void main(String[] args) throws Exception {
Order order1 = new Order();
order1.setOrderId("ORDER_ID_1");
order1.setAmount(100);
order1.setChannel("CHANNEL_A");
order1.w());
createOrder(order1);
Order order2 = new Order();
order2.setOrderId("ORDER_ID_2");
order2.setAmount(200);
order2.setChannel("CHANNEL_B");
order2.w());
createOrder(order2);
Search.in(Metrics.globalRegistry).meters().forEach(each -> {
StringBuilder builder = new StringBuilder();
builder.append("name:")
.
Id().getName())
.append(",tags:")
.Id().getTags())
.append(",type:").Id().getType())
.append(",value:").asure());
System.out.String());
});
}
private static void createOrder(Order order) {
//忽略订单⼊库等操作
"channel", Channel(),
"createTime", FORMATTER.CreateTime())).increment();
}
}
控制台输出
ate,tags:[tag(channel=CHANNEL_A), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
ate,tags:[tag(channel=CHANNEL_B), tag(createTime=2018-11-10)],type:COUNTER,value:[Measurement{statistic='COUNT', value=1.0}]
上⾯的例⼦是使⽤全局静态⽅法⼯⼚类Metrics去构造Counter实例,实际上,instrument.Counter接⼝提供了⼀个内部建造器类Counter.Builder去实例化Counter,Counter.Builder的使⽤⽅式如下:
public class CounterBuilderMain {
public static void main(String[] args) throws Exception{
Counter counter = Counter.builder("name") //名称
.baseUnit("unit") //基础单位
.description("desc") //描述
.tag("tagKey", "tagValue") //标签
.register(new SimpleMeterRegistry());//绑定的MeterRegistry
counter.increment();
}
}
FunctionCounter
FunctionCounter是Counter的特化类型,它把计数器数值增加的动作抽象成接⼝类型ToDoubleFunction,
这个接⼝JDK1.8中对于Function的特化类型接⼝。FunctionCounter的使⽤场景和Counter是⼀致的,这⾥介绍⼀下它的⽤法:
public class FunctionCounterMain {
public static void main(String[] args) throws Exception {
MeterRegistry registry = new SimpleMeterRegistry();
AtomicInteger n = new AtomicInteger(0);
//这⾥ToDoubleFunction匿名实现其实可以使⽤Lambda表达式简化为AtomicInteger::get
FunctionCounter.builder("functionCounter", n, new ToDoubleFunction<AtomicInteger>() {
@Override
public double applyAsDouble(AtomicInteger value) {
();
}
}).baseUnit("function")
.description("functionCounter")
.tag("createOrder", "CHANNEL-A")
.register(registry);
//下⾯模拟三次计数
n.incrementAndGet();
n.incrementAndGet();
n.incrementAndGet();
}
}
FunctionCounter使⽤的⼀个明显的好处是,我们不需要感知FunctionCounter实例的存在,实际上我们
只需要操作作为FunctionCounter实例构建元素之⼀的AtomicInteger实例即可,这种接⼝的设计⽅式在很多框架⾥⾯都可以看到。
Timer
Timer(计时器)适⽤于记录耗时⽐较短的事件的执⾏时间,通过时间分布展⽰事件的序列和发⽣频率。所有的Timer的实现⾄少记录了发⽣的事件的数量和这些事件的总耗时,从⽽⽣成⼀个时间序列。Timer的基本单位基于服务端的指标⽽定,但是实际上我们不需要过于关注Timer 的基本单位,因为Micrometer在存储⽣成的时间序列的时候会⾃动选择适当的基本单位。Timer接⼝提供的常⽤⽅法如下:
public interface Timer extends Meter {
...
void record(long var1, TimeUnit var3);
default void record(Duration duration) {
}
<T> T record(Supplier<T> var1);
<T> T recordCallable(Callable<T> var1) throws Exception;
void record(Runnable var1);
default Runnable wrap(Runnable f) {
return () -> {
};
}
default <T> Callable<T> wrap(Callable<T> f) {
return () -> {
dCallable(f);
};
}
long count();
double totalTime(TimeUnit var1);
default double mean(TimeUnit unit) {
unt() == 0L ? 0.0D : alTime(unit) / (unt();
}
double max(TimeUnit var1);
...
}
实际上,⽐较常⽤和⽅便的⽅法是⼏个函数式接⼝⼊参的⽅法:
Timer timer = ...
Runnable r = timer.wrap(() -> dontCareAboutReturnValue());
Callable c = timer.wrap(() -> returnValue());
使⽤场景:
根据个⼈经验和实践,总结如下:
1、记录指定⽅法的执⾏时间⽤于展⽰。
2、记录⼀些任务的执⾏时间,从⽽确定某些数据来源的速率,例如消息队列消息的消费速率等。
这⾥举个实际的例⼦,要对系统做⼀个功能,记录指定⽅法的执⾏时间,还是⽤下单⽅法做例⼦:
public class TimerMain {
private static final Random R = new Random();
static {
Metrics.addRegistry(new SimpleMeterRegistry());
}
public static void main(String[] args) throws Exception {
Order order1 = new Order();
order1.setOrderId("ORDER_ID_1");
order1.setAmount(100);
order1.setChannel("CHANNEL_A");
order1.w());
Timer timer = Metrics.timer("timer", "createOrder", "cost");
}
private static void createOrder(Order order) {
try {
TimeUnit.SECONDS.Int(5)); //模拟⽅法耗时
} catch (InterruptedException e) {
//no-op
}
}
}
在实际⽣产环境中,可以通过spring-aop把记录⽅法耗时的逻辑抽象到⼀个切⾯中,这样就能减少不必要的冗余的模板代码。上⾯的例⼦是通过Mertics构造Timer实例,实际上也可以使⽤Builder构造:
MeterRegistry registry = ...
Timer timer = Timer
.builder("my.timer")
.description("a description of what this timer does") // 可选
.tags("region", "test") // 可选
.register(registry);
另外,Timer的使⽤还可以基于它的内部类Timer.Sample,通过start和stop两个⽅法记录两者之间的逻辑的执⾏耗时。例如:
Timer.Sample sample = Timer.start(registry);
// 这⾥做业务逻辑
Response response = ...
sample.stop(registry.timer("my.timer", "response", response.status()));
FunctionTimer
FunctionTimer是Timer的特化类型,它主要提供两个单调递增的函数(其实并不是单调递增,只是在使⽤中⼀般需要随着时间最少保持不变或者说不减少):⼀个⽤于计数的函数和⼀个⽤于记录总调⽤耗时的函数,它的建造器的⼊参如下:
public interface FunctionTimer extends Meter {
static <T> Builder<T> builder(String name, T obj, ToLongFunction<T> countFunction,
springboot其实就是springToDoubleFunction<T> totalTimeFunction,
TimeUnit totalTimeFunctionUnit) {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论