前言
好久没输出了,近期比较忙,1星期把一个季度的东西干完才有机会输出。
倘若编码速度非常快(质量亦非常高),超出一个极限(超出其他所有职能分部的速度),那样不可能有人会占用你的时间。
就像Integer.MAX_VALUE+1,超出极限了便是负数。
本章基于springboot3.0.7分析micrometer1.10.7关联源码。
本章不会太多关注: prometheus客户端怎样运用PromQLspringboot3放弃sleuth,转而运用micrometer-tracingpush模式收集metrics本章仅限于分析micrometer: 以micrometer接入prometheus的实现为抓手,瞧瞧怎样创立一套metrics模型pull模式收集metricsmicrometer做业务埋点,api怎样运用更恰当
从SpringBoot入手
其实通常状况下,我都不太会看和Spring集成关联的东西,除了有些脱离Spring没法(或不会去选取)单独运用的框架,例如Seata。
这儿看一下Micrometer有那些核心的Bean,好找入口分析。
通常状况下不必看Spring部分,无非是怎么去new一个对象,无论你用什么看起来高挑上的办法,本质便是这个。
PrometheusMetricsExportAutoConfiguration
Properties配置类PrometheusConfig(micrometer供给):
PrometheusMeterRegistry(micrometer供给):
核心类,MeterRegistry实现,保留所有Meter,micrometer与prometheus集成
CollectorRegistry(prometheus供给):
prometheus收集metrics的api
DefaultExemplarSampler(prometheus供给):
prometheus针对Exemplar的支持,metrics相关trace
PrometheusScrapeEndpoint(springboot供给):
暴露/actuator/prometheus端点给prometheus拉metrics
MetricsAutoConfiguration
Clock(micrometer供给):时钟,用于获取时间,便是System.currentTimeMillis。
MeterRegistryPostProcessor(springboot供给):有两个功效
1)针对MeterRegistry类型的Bean做有些全局配置处理,例如加入全局MeterFilter,做Meter过滤;
2)针对MeterBinder类型的Bean,主动触发bind办法注册meter到MeterRegistry;
PropertiesMeterFilter(springboot供给):
基于management.metrics配置项的MeterFilter;
Mircometer模型
Meter
Meter聚合了一组度量(Measurement),用Id表率这组度量。
每次measure办法返回都是相同数量、相同次序的一组瞬时value。
Id
Id重点由三部分构成:name-meter名,tags-meter标签集合,type-meter的类型。
Id#hashCode:然则Id的独一性,仅由name和tag决定。(equals代码不贴了,太长)
Tag
Tags由多个Tag构成;而Tag由一对kv构成。
当kv相同期,Tag相同;当所有Tag相同,Tags相同。
Tag#hasCode:
Measurement
度量包括两个属性: DoubleSupplier:一个返回double的supplier函数;Statistic:枚举,针对一个度量的描述;
转换Prometheus格式
例如jvm内存运用状况,暴露给prometheus的数据格式如下:
jvm_memory_used是Id.name,bytes是单位;{application="sb3-app",area="nonheap",id="Compressed Class Space",}是Tags;这儿一行是一个Measurement度量,这个Meter仅有一个度量;例如某个端点的http请求,暴露给prometheus的数据格式如下: ini复制代码http_server_requests_seconds_count{application="sb3-app",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/metric",} 2.0
http_server_requests_seconds_sum{application="sb3-app",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/metric",} 0.006369008
http_server_requests_seconds_max{application="sb3-app",error="none",exception="none",method="GET",outcome="SUCCESS",status="200",uri="/metric",} 0.004348599
http_server_requests是Id.name,seconds是单位;{...uri="/metric",}是Tags;一个端点的http请求(一个Id)有3个Measurement度量,count是请求总数,sum是请求总时长,max是请求最大时长;MeterRegistry
一个抽象类,内存中存储Meter。
MeterRegistry供给有些方便运用的注册Meter办法,例如注册一个Counter类型的meter。
为此,子类需要实现有些构建Meter的办法,例如构建一个Counter。
MeterRegistry供给注册Meter的骨架办法getOrCreateMeter流程如下: MeterFilter#accept过滤Meter.IdMeterFilter#configure按照Meter.Id二次处理DistributionStatisticConfigbuilder#apply执行用户回调构建Meter同义meter注册meter注册Listener通告注册mter
MeterBinder
除了直接调用MeterRegistry能够注册Meter之外。
部分Metrics会实现MeterBinder接口,将Meter注册到MeterRegistry。
在SpringBoot中,MeterRegistryPostProcessor会来触发所有MeterBinder的注册动作。
几种Meter
Counter
Counter只会返回一个double类型的度量,是一个只增不减的计数器。
micrometer针对Prometheus的实现是PrometheusCounter。
PrometheusCounter除了一个DoubleAddr计数器之外,还有一个Exemplar。
每次更新计数器,还会更新Exemplar。
这个Exemplar的功效便是相关metric和trace,见OpenMetrics。
在客户端侧,倘若加入tracing关联组件,就能运用Exemplar,见SpringBoot+micrometer-tracing。
在prometheus侧,需要开启exemplar-storage特性,见Prometheus+exemplars-storage。
在grafana侧,能够经过metric拿到Exemplar,而Exemplar中包括trace_id,方便定位问题。
需要重视的是,日前Exemplar仅支持prometheus,因此这两个模型都是prometheus客户端供给的。
PrometheusCounter#updateExemplar:
每次increment都会调用CounterExemplarSampler采集一个Exemplar,替换Counter中的Exemplar。
因此promethues配置scrape_interval=15s抓取一次,那样只会抓到近期一次Counter记录到的trace。
DefaultExemplarSampler#doSample:
当满足下面两个要求的状况下,才会采集新的Exemplar,Exemplar包括当前线程中的trace信息 倘若trace要采集(isSampled=true)距离上次采样时间超过一按时间(minRetentionIntervalMs=7109ms)不满足以上要求,PrometheusCounter中的Exemplar不会更新。
Gauge
Gauge只会返回一个double类型的度量,可增可减。
micrometer针对Prometheus无特殊实现,运用DefaultGauge。 value:一个Function函数,获取double类型度量,不像Counter通常自己就能守护一个计数器;ref:做为Function入参对象,一个弱引用,通常来讲ref引用的对象是一个长生命周期对象;重视Gauge没法供给Exemplar,即没法关联trace。
Timer
Timer用于跟踪海量短期运行事件的计时器,通常这些事件在一分钟以内,例如http请求。
Timer有三个度量:count-总数、totalTime-总耗时、max-最长耗时。
Timer用于统计事件时长,埋点方式有多种。
例如,在事件起始需要Timer#start拿到一个Timer.Sample。
在事件结束需要Timer.Sample#stop,调用Timer实例的record办法记录事件耗时。
又例如,Timer#record直接记录耗时,需要子类实现。
AbstractTimer实现了record规律,忽略Histogram和IntervalEstimator,子类需要实现recordNonNegative。
PrometheusTimer#recordNonNegative:
三个度量计算,重点关注一下TimeWindowMax。
重视,单纯PrometheusTimer并不支持Exemplar,Exemplar依附着Histogram运用。
TimeWindowMax属性如下,重点是经过一个环形数组,每一个数组元素存储1分钟内的最大时长。
TimeWindowMax的滚动间隔时长和环形数组容量,都取自于DistributionStatisticConfig。
在记录最值的时候,先滚动,而后用采样值更新所有桶中的最值。
滚动办法如下,初期版本(1.6.11之前)有bug(issue=2647),可能没法采集到正确的最值。
TimeWindowMax#rotate:超过一分钟未滚动,才进行滚动。
TimeWindowMax#rotate:第一起滚动规律
这个是issue=2647新增的规律,倘若超过3分钟未滚动,清空所有桶,并更新上次滚动时间。
TimeWindowMax#rotate:第二块滚动规律
初期版本仅有这一起滚动规律,问题在于timer长时间未被调用,lastRotateTimestampMillis上次滚动时间没法正确更新。
TimeWindowMax#poll:获取最值的办法,先尝试滚动,而后获取当前桶中的最值。
注:滚动和获取最值做了同步synchronized。
以官方测试用例为例,规律如下。
需要理解的是,TimeWindowMax最值并不是1分钟内的最值,是3分钟内的最值。
官方叫作为:基于可配置环形缓冲区的分布衰减最大值。
LongTaskTimer
LongTaskTimer和Timer完全区别。
LongTaskTimer用于度量正在执行的任务数量和这些任务已然执行的时长,重点便是in-flight。
LongTaskTimer亦支持Histogram。
举个例子: typescript复制代码@RestController
public class OrderMetrics implements MeterBinder {
private LongTaskTimer longTaskTimer;
@Override
public void bindTo(MeterRegistry meterRegistry) {
longTaskTimer = LongTaskTimer.builder("order.long.task")
.publishPercentileHistogram(true)
.publishPercentiles(0.5, 0.95)
.minimumExpectedValue(Duration.ofSeconds(10))
.maximumExpectedValue(Duration.ofSeconds(20))
.register(meterRegistry);
}
@GetMapping("/order/longtask")
public String longtask() {
EXECUTOR.submit(() -> {
LongTaskTimer.Sample sample = longTaskTimer.start();
int time = RANDOM.nextInt(10, 20);
try {
TimeUnit.SECONDS.sleep(time);
} catch (InterruptedException e) {
sample.stop();
return;
}
sample.stop();
});
return "success";
}
}虽然历史曾经有过LongTaskTimer采集数据(从histogram数据能够看到)。
然则active_count、duration_sum、max都为0,由于LongTaskTimer的度量仅针对in-flight数据,当所有任务执行完毕,这些度量都会变成0。
Prometheus接入的实现是CumulativeHistogramLongTaskTimer,仅有Histogram关联的takeSnapshot办法有点特殊,规律都走DefaultLongTaskTimer。
Histogram部分放在后面统一看,这儿Histogram的特点和其他Prometheus接入的实现类似,桶计数要运用累积数量。
DefaultLongTaskTimer#start:创建Sample实例,放入activeTasks集合存储。
DefaultLongTaskTimer.SampleImpl#stop:从activeTasks集合移除Sample。
获取度量,duration统计所有正在执行任务的累计时长,activeTasks统计正在执行的任务数量。
max统计此时执行最长的时长。
DistributionSummary
在micrometer侧,DistributionSummary和Timer几乎无区别,仅仅是单位上的区别。
Timer是时间,而DistributionSummary是任意值。
A distribution summary tracks the distribution of events. It is similar to a timer structurally, but records values that do not represent a unit of time. For example, you could use a distribution summary to measure the payload sizes of requests hitting a server.
Histogram
Histogram不属于Meter,依附着Timer和DistributionSummary。
案例
默认状况下,普通的Timer不会开启Histogram,案例经过Timer统计订单详情的P50和P95。
注:运用micrometer供给的TimedAspect加Timed注解亦能够,api更灵活。 typescript复制代码@RestController
public class OrderMetrics implements MeterBinder {
private Timer orderDetailTimer;
@Override
public void bindTo(MeterRegistry meterRegistry) {
orderDetailTimer = Timer.builder("order.detail")
.publishPercentileHistogram(true)
.publishPercentiles(0.5, 0.95)
// 默认1ms-30s
.minimumExpectedValue(Duration.ofMillis(1))
.maximumExpectedValue(Duration.ofSeconds(5))
.register(meterRegistry);
}
public void orderDetailRecord(long time, TimeUnit unit) {
orderDetailTimer.record(time, unit);
}
@GetMapping("/order/detail")
public String detail() {
log.info("订单详情起始"); // for trace
int time = RANDOM.nextInt(1, 2000);
this.orderDetailRecord(time, TimeUnit.MILLISECONDS);
log.info("订单详情结束"); // for trace
return "success";
}
}暴露给prometheus的数据格式如下: order_detail_seconds_bucket,表率1ms到5s每一个桶中的请求数量,这个数量是依次累计的,第10个桶包括前1-9个桶中所有请求数量的总和,支持Exemplar;order_detail_seconds{quantile},表率P50和P95对应的值;
HistogramSnapshot
针对部分Meter,例如Timer和Summary,支持Histogram,会实现takeSnapshot办法。
takeSnapshot返回一个Histogram的快照,不属于Measurement度量。
HistogramSnapshot除了传统的count、total、max这种度量之外,还有两个数组: percentileValues:分位数对应值,例如案例P50对应值在percentileValues[0]的位置,P95对应值在percentileValues[1]的位置,即案例order_detail_seconds{quantile}histogramCounts:每一个桶中的数量,即案例order_detail_seconds_bucket
PrometheusTimer#takeSnapshot:
针对PrometheusTimer来讲,桶计数需要走自己的histogram来获取,而分位数值走父类的histogram获取。
至于为何需要这么繁杂分两个Histogram来获取数据,是由于Prometheus针对桶计数需要完整的数据,而不是滚动的Histogram。
言外之意,桶计数需要从进程起步到进程停止,完整生命周期内累计的数据;而分位数值是滚动数据。
父类AbstractTimer的Histogram走TimeWindowPercentileHistogram。
自己的Histogram走PrometheusHistogram。
PrometheusHistogram底层是micrometer的TimeWindowFixedBoundaryHistogram。
全部Histogram触及到的继承关系如下,一点一点来看吧。
Histogram
Histogram自己具备takeSnapshot能力,因此通常Timer和Summary都会委派底层Histogram生成快照。
除此以外,record办法记录埋点值。
AbstractTimeWindowHistogram
和TimeWindowMax类似,经过一个环形数组存储埋点值。
只不外数组元素是泛型T,即AbstractTimeWindowHistogram能够理解为存储了x个时间窗口内的T。
相较于TimeWindowMax多了有些Histogram专属的属性,泛型U是一个累积Histogram。
这个累计Histogram大概是啥意思,后面再看。
记录采集值和TimeWindowMax规律类似,recordLong记录bucket需要子类实现。
AbstractTimeWindowHistogram#takeSnapshot:获取HistogramSnapshot的骨架办法。
子类需要实现三个抽象办法用于获取HistogramSnapshot: accumulate:累计Histogram,后面再看;valueAtPercentile:获取分位数对应的值,例如P95对应2秒;countAtValue:获取某个桶中的采样总数,例如传入1秒,得到0.5-1秒总共有5个请求;
TimeWindowPercentileHistogram
在PrometheusTimer中,仅用于获取分位数值,例如P95=2秒。
DoubleRecorder即泛型T,是底层环形数组中的元素,用于记录每一个时间窗口内的采样值;
DoubleHistogram即泛型U,是底层定义的累计Histogram,当takeSnapshot会将当前窗口的采样值T累计到U;
这两个类都不是micrometer自己的,是HdrHistogram供给的,无其他任何依赖,当jdk用就可。
在记录采样值时,调用HdrHistogram记录。
在创建snapshot时,将当前窗口的DoubleRecorder灌入DoubleHistogram。
由于Prometheus只用这儿的分位数值,因此只用看valueAtPercentile,调用DoubleHistogram#getValueAtPercentile这个api就可。
因此根据这个规律,是不是能采集到分位数值其实和采集频率相关。
倘若在一个时间窗口(1分钟)内不来采集,那样就不会调用累积Histogram办法,那样这个时间窗口的数据就会随着底层环形数组滚动而丢失。
另外,HdrHistogram统计Pxx总归是有误差的,不会把所有采样值都放到内存中。
能够经过调节DistributionStatisticConfig#percentilePrecision来提高精度,然则耗费内存会更多。
默认精度是1,详细规律我亦不深究了。
TimeWindowFixedBoundaryHistogram
在Prometheus场景下,TimeWindowFixedBoundaryHistogram仅用于统计每一个桶中的采样数量。
特点是PrometheusHistogram在构造父类时,写死了两个参数: expiry:1825天,寓意着底层的环形数组永远不滚动;bufferLength:1,寓意着底层的环形数组仅有一个元素;
从TimeWindowFixedBoundaryHistogram的泛型参数来看: T:底层环形数组元素,是内部类TimeWindowFixedBoundaryHistogram.FixedBoundaryHistogram;U:Void,因此不支持累计Histogram;
从成员变量来看,重点是有一组buckets。
例如案例会划分为57个bucket,每一个bucket的元素是一个时间值,从1毫秒到5秒。
TimeWindowFixedBoundaryHistogram记录采样值:
TimeWindowFixedBoundaryHistogram获取桶内的采样数量,走cumulativeBucketCounts=true:
FixedBoundaryHistogram守护了一个和bucket长度一致的原子长整型数组values。
FixedBoundaryHistogram#record办法: 从bucket数组中,找到最大的少于等于采样值的bucket下标(算法题二分查询变形);values对应下标元素自增,即统计采样值到某个bucket中;
FixedBoundaryHistogram#countAtValueCumulative:
获取桶内数量的时候,Prometheus需要把该桶之前的数据累加。
即0-1秒有1个请求,1-2秒有1个请求,入参2秒获取的数据实质是2个,和案例中一致。
PrometheusHistogram
PrometheusHistogram守护了一组和父类同样的buckets数组,一组死值。
特点在于PrometheusHistogram支持记录每一个bucket对应的Exemplar。
每次采集数据,找到采样值所属bucket(二分),并记录Exemplar,规律和PrometheusCounter类似。
关于业务埋点
怎样定义业务Meter
1、固定tag的Meter
其实大部分Meter的tag都是可穷举可预知的。
为了减少内存和cpu的浪费,不必在运行时去注册Meter到MeterRegistry,虽然MeterRegistry#getOrCreateMeter支持判断同一个Meter仅注册一次。
例如:Jvm内存分堆和非堆,那样运用MeterBinder在ioc容器起步周期就能够注册Meter了。
例如我要统计区别订单的下单数量的走势,按照订单类型枚举OrderType直接在ioc容器起步周期注册Counter就可,运行时只需要调用increment api,而不需要再去走getOrCreateMeter。 typescript复制代码@Component
public class OrderMetrics implements MeterBinder {
private static final Map<OrderType, Counter> ORDER_COUNT = new HashMap<>();
@Override
public void bindTo(MeterRegistry meterRegistry) {
for (OrderType orderType : OrderType.values()) {
Counter counter = meterRegistry.counter("order.count", "order.type", orderType.getCode());
ORDER_COUNT.put(orderType, counter);
}
}
public void orderIncr(OrderType orderType) {
ORDER_COUNT.get(orderType).increment();
}
}2、非固定tag的Meter
有些状况,tag纬度非常多,且tag不太能穷举,只能在运行周期注册Meter到MeterRegistry
如DefaultMeterObservationHandler(springboot2不是这个),统计http请求耗时,默认tag纬度很多(uri+status+exception+method+outcome)不可能起步周期穷举。
举个不必定很恰当的例子,订单取消订单原由类型,能够无限扩充,仅用于放在看板上分析,不需要每次新增原由类型,就修改后端服务。
当然了,运用Prometheus采集metrics咱们应该避免出现高基数问题。
开箱即用的Meter
SpringBoot供给了非常多开箱即用的Meter,举几个初期版本不必定有的例子。
ThreadPoolTaskExecutor/ThreadPoolTaskScheduler
这两个类都是Spring供给的,包装了普通的jdk的线程池,供给了非常多在Spring里的加强功能。
针对这两类bean,SpringBoot会暴露线程池的关联meter,对业务亦比较有帮忙。
除了executor_completed是Counter之外,其他都是Guage。
DefaultMeterObservationHandler
在springboot初期版本,运用自己写的WebMvcMetricsFilter来记录http.server.requests指标。
在新版本中,运用micrometer供给了DefaultMeterObservationHandler。
区别在于DefaultMeterObservationHandler不仅采集普通Timer的,还采集了LongTaskTimer,即能够看到正在处理(in-flight)的请求metrics。
总结
Mircometer的核心模型就两个: Meter:包括一组瞬时Measurement度量,用一个独一标识Id(name+Tags)来标识这组度量;MeterRegistry:存储Meter的地区,getOrCreateMeter办法掌控同一个Id的Meter只注册一个Meter实例;注册Meter通常有两种方式: 实现MeterBinder接口,由ioc容器回调bindTo办法,注册Meter;运行时构造Meter实例,经过MeterRegistry#register API注册Meter;五种Meter模型: Counter:只包括一个只增不减的double类型度量,支持Exemplar;Gauge:只包括一个可增可减的double类型度量,不支持Exemplar;Timer:用于跟踪海量短期运行事件的计时器,包括三个度量count总数、totalTime总耗时、max最长耗时。其中max默认统计的是3分钟内的最大值,基于环形数组实现,数组容量bufferLength=3,每一个元素表率时长expiry=1分钟。支持Histogram,依托Histogram支持Exemplar;LongTaskTimer:用于度量正在执行(in-flight)的任务,包括active_count正在执行任务数、duration正在执行任务总时长、max正在执行任务的最大时长,支持Histogram,依托Histogram支持Exemplar;DistributionSummary:和Timer的实现几乎一致,区别在于DistributionSummary用于统计非时间的任意值,支持Histogram,依托Histogram支持Exemplar;关于Histogram: 桶计数:percentileHistogram=true开启桶计数,桶数量由minimumExpectedValue和maximumExpectedValue确定。在Prometheus场景下,桶计数需要累计全部进程生命周期中的所有计数,底层TimeWindowFixedBoundaryHistogram守护了一个永远不滚动的1容量的环形数组;分位数值:percentiles非空开启分位数值记录,分位数值数量由percentiles确定。收集时会将当前时间窗口内的计数,灌入累积Histogram,底层运用三方工具HdrHistogram实现;支持Exemplar:对于每一个桶,支持记录Exemplar;
|