外链论坛

 找回密码
 立即注册
搜索
查看: 16|回复: 3

Micrometer源码分析

[复制链接]

3026

主题

155

回帖

9923万

积分

论坛元老

Rank: 8Rank: 8

积分
99239027
发表于 2024-10-10 17:08:30 | 显示全部楼层 |阅读模式

前言

好久没输出了,近期比较忙,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;
回复

使用道具 举报

3046

主题

2万

回帖

9909万

积分

论坛元老

Rank: 8Rank: 8

积分
99098928
发表于 2024-10-21 18:19:26 | 显示全部楼层
你的见解真是独到,让我受益良多。
回复

使用道具 举报

3095

主题

2万

回帖

9996万

积分

论坛元老

Rank: 8Rank: 8

积分
99968783
发表于 2024-10-24 18:19:15 | 显示全部楼层
对于这个问题,我有不同的看法...
回复

使用道具 举报

3005

主题

2万

回帖

9606万

积分

论坛元老

Rank: 8Rank: 8

积分
96066006
发表于 2024-10-26 20:33:33 | 显示全部楼层
期待你更多的精彩评论,一起交流学习。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

站点统计|Archiver|手机版|小黑屋|外链论坛 ( 非经营性网站 )|网站地图

GMT+8, 2024-11-9 09:24 , Processed in 0.078693 second(s), 19 queries .

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.