ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

Micrometer 系列【35】Spring Boot Micrometer Metrics 数据采集、指标查询解析

Micrometer 系列【35】Spring Boot Micrometer Metrics 数据采集、指标查询解析 文章目录1. 前言2. 指标查询2.1 MetricsEndpointActuator 指标端点2.1.1 核心成员2.1.2 listNames()全量指标名称查询2.1.3 metric() 单指标详情查询2.2 全量指标名称查询接口2.3 指标详情接口3. 数据采集3.1 采集时触发3.2 内部状态维护3.2.1 自动配置3.2.2 请求开始3.2.3 请求结束3.2.4 查询数据1. 前言在之前的几篇文档中我们已经了解了在Micrometer原生框架中指标的整个生命周期包括实例化、注册、采集、销毁。原生MeterRegistry提供register、remove、close等底层API手动完成指标的增删与资源回收。那么在Spring Boot环境下一切不完全等同原生编码。Spring Boot自动装配、自动注入MeterRegistry、MeterBinder、自动构建内置指标、上下文生命周期钩子已经做到了开箱即用。通过之前Spring Boot Micrometer Metrics自动配置章节我们已经了解到根据classpath上的依赖创建对应的MeterRegistryBean整个Spring 应用只存在一个MeterRegistry实例。业务代码直接Autowired MeterRegistry registry注入使用。MeterBinder自动绑定内置指标Spring容器启动完成后收集全部MeterBinderBean并自动注册到全局registry。那么在Spring Boot场景中是如何实现数据采集、指标查询的呢2. 指标查询2.1 MetricsEndpointActuator 指标端点核心作用提供Spring Boot Actuator内置/actuator/metrics接口用于人工调试查看应用所有指标、实时数值、标签信息仅做只读查询不参与指标采集与生命周期管理。支持接口GET /actuator/metrics获取应用所有已注册指标名称列表GET /actuator/metrics/{metricName}?tagk1:v1根据指标名 标签过滤查询指标实时测量值、可用标签2.1.1 核心成员端点核心依赖MeterRegistrySpring Boot自动注入组合注册中心Endpoint(idmetrics)publicclassMetricsEndpoint{privatefinalMeterRegistryregistry;// ...}在引入了micrometer-registry-prometheus后注入的是PrometheusMeterRegistrycollectNames(names, this.registry)递归收集指标名privatevoidcollectNames(SetStringnames,MeterRegistryregistry){if(registryinstanceofCompositeMeterRegistrycompositeMeterRegistry){// 复合注册中心本身不存储meter遍历全部子registry递归调用compositeMeterRegistry.getRegistries().forEach((member)-collectNames(names,member));}else{// 真实底层registry(Prometheus/SimpleMeterRegistry)// getMeters() 获取meterMap全部meter快照registry.getMeters().stream().map(this::getName)// meter.getId().getName()只拿指标名字丢弃tag.forEach(names::add);}}2.1.2 listNames()全量指标名称查询核心逻辑递归遍历MeterRegistry所有指标器收集所有Meter的名称去重排序。关键特性仅读取指标元数据名称不执行measure()、不采集实时数值性能极高。ReadOperationpublicMetricNamesDescriptorlistNames(){SetStringnamesnewTreeSet();collectNames(names,this.registry);returnnewMetricNamesDescriptor(names);}2.1.3 metric() 单指标详情查询完整执行步骤解析标签参数parseTags()解析URLtagkey:value参数格式错误抛出参数异常匹配指标实例findFirstMatchingMeters()根据指标名标签匹配容器内注册的Meter空值处理无匹配指标直接返回nullHTTP响应404采集合并数值getSamples()调用meter.measure()获取测量数据合并多实例指标值过滤可用标签getAvailableTags()收集剩余可过滤标签移除 URL 已传入的过滤标签组装返回数据封装指标名、单位、测量值、可用标签等信息。ReadOperationpublicNullableMetricDescriptormetric(SelectorStringrequiredMetricName,NullableListStringtag){ListTagtagsparseTags(tag);CollectionMetermetersfindFirstMatchingMeters(this.registry,requiredMetricName,tags);if(meters.isEmpty()){returnnull;}MapStatistic,DoublesamplesgetSamples(meters);MapString,SetStringavailableTagsgetAvailableTags(meters);tags.forEach((t)-availableTags.remove(t.getKey()));Meter.IdmeterIdmeters.iterator().next().getId();returnnewMetricDescriptor(requiredMetricName,meterId.getDescription(),meterId.getBaseUnit(),asList(samples,Sample::new),asList(availableTags,AvailableTag::new));}解析tag参数parseTags(tag)将url的tagkey:value字符串列表转换MicrometerTag对象列表格式错误抛出异常。没有传tag返回空集合。findFirstMatchingMeters()在MeterRegistry查找匹配meterprivateCollectionMeterfindFirstMatchingMeters(MeterRegistryregistry,Stringname,IterableTagtags){if(registryinstanceofCompositeMeterRegistrycompositeMeterRegistry){returnfindFirstMatchingMeters(compositeMeterRegistry,name,tags);}returnregistry.find(name).tags(tags).meters();}getSamples()mergeMeasurements()采集并合并测量值meters.forEach((meter)-mergeMeasurements(samples,meter));meter.measure()// 实时执行采集返回一组Measurement{Statistic, value}合并规则Statistic.MAX取各个meter的最大值Double::max其余所有统计项执行求和Double::sum注意不是求平均值是sum/max合并。getAvailableTags()收集可用tag集合已经在url请求参数中指定的tag key从availableTags移除代表该维度已经被过滤不能再下钻。tags.forEach((t)-availableTags.remove(t.getKey()));最后组装MetricDescriptor DTO返回。2.2 全量指标名称查询接口需要开启暴露metrics端点management.endpoints.web.exposure.includemetrics访问路径GET /actuator/metrics请求参数无任何请求参数不需要path变量、不需要query参数。HTTP MethodGET对应注解ReadOperation只读操作。对比GET /actuator/metrics/{metricName}是另外一个方法metric()带Selector路径变量。响应DTOMetricNamesDescriptor implements OperationResponseBody响应示例{names:[application.ready.time,http.server.requests,jvm.memory.used,jvm.threads.live,system.cpu.usage]}2.3 指标详情接口访问路径GET /actuator/metrics/{metricName}请求参数路径变量参数说明metricName必填指标名称如jvm.memory.usedQuery参数参数说明示例tag可选可多次传用于按tag过滤meter格式必须key:valuetagstatus:200tagmethod:GET请求示例1不带tag过滤GET /actuator/metrics/jvm.memory.used请求示例2带tag过滤GET /actuator/metrics/http.server.requests?tagstatus:200tagmethod:GET返回DTOMetricDescriptor implements OperationResponseBody响应示例{name:jvm.memory.used,description:The amount of used memory,baseUnit:bytes,measurements:[{statistic:VALUE,value:186421344}],availableTags:[{tag:area,values:[heap,nonheap]}]}字段说明字段说明name指标名称入参传入的metricNamedescription指标描述来自MeterId可为nullbaseUnit基础单位(bytes/seconds等)可为nullmeasurements采集统计结果数组‑statistic统计类型VALUE/COUNT/TOTAL_TIME/MAX等‑value采集出来的数值实时调用meter.measure()得到availableTags还可以继续筛选的tag维度url上已经指定过的tag会被剔除‑tagtag key‑values该key下全部可选tag值集合3. 数据采集3.1 采集时触发我们以/actuator/metrics/jvm.memory.used为例MetricsEndpoint#findFirstMatchingMeters匹配到6个Gauge实例调用meter.measure()获取每个指标器中的所有度量值Measurement调用JDK Map原生merge方法如果key不存在直接put (key,value)如果key已经存在执行第三个参数的合并函数旧值、新值传入函数函数返回值作为新的value存入map。调用measurement.getValue()时触发真正采集指标数值samples.merge(measurement.getStatistic(),measurement.getValue()jvm.memory.used的Statistic类型是Value则会对所有Gauge实例的值进行求和返回进行标签过滤后直接返回响应对象使用Actuator内部的JSon序列化后返回到浏览器{availableTags:[{tag:area,values:[heap,nonheap]},{tag:id,values:[G1 Survivor Space,Compressed Class Space,Metaspace,CodeCache,G1 Old Gen,G1 Eden Space]}],baseUnit:bytes,description:The amount of used memory,measurements:[{statistic:VALUE,value:109319464}],name:jvm.memory.used}3.2 内部状态维护类型Timer计时器指标记录http服务请求耗时、请求次数。http.server.requests指标在Spring Boot 3.x以上的版本都基于Micrometer Observation模式进行统一观测了这里我们简单了解下。3.2.1 自动配置WebMvcObservationAutoConfiguration自动注册过滤器ServerHttpObservationFilter进行观测埋点。DefaultMeterObservationHandler把Observation事件转成Micrometer Meter指标3.2.2 请求开始执行时机HTTP请求进入过滤器业务逻辑尚未执行请求开始时执行DefaultMeterObservationHandler#onStart方法若开启shouldCreateLongTaskTimer获取/创建{name}.active的LongTaskTimer调用.start()得到采样对象将LongTaskTimer.Sample存入Observation.Context调用Timer.start(meterRegistry)生成Timer.Sample仅记录开始时间戳将Timer.Sample存入上下文OverridepublicvoidonStart(Observation.Contextcontext){if(shouldCreateLongTaskTimer){LongTaskTimer.SamplelongTaskSamplemeterRegistry.more().longTaskTimer(context.getName().active,createTags(context)).start();context.put(LongTaskTimer.Sample.class,longTaskSample);}Timer.SamplesampleTimer.start(meterRegistry);context.put(Timer.Sample.class,sample);}3.2.3 请求结束执行时机请求完成正常返回 / 异常请求完成时执行DefaultMeterObservationHandler#onStop方法从上下文提取、组装tags追加error标签获取上下文保存的Timer.SamplemeterRegistry.timer(context.getName(), tags)nametags不存在新建Timer Meter注册进meterMap已存在直接复用已有Timersample.stop(timer)计算耗时执行timer.record()写入耗时统计若开启长任务计时器取出LongTaskTimer.Sample执行.stop()结束活跃任务统计Override// TODO decide what to do about context.getName being NullableSuppressWarnings(NullAway)publicvoidonStop(Observation.Contextcontext){ListTagtagscreateTags(context);tags.add(Tag.of(error,getErrorValue(context)));Timer.Samplesamplecontext.getRequired(Timer.Sample.class);sample.stop(this.meterRegistry.timer(context.getName(),tags));if(shouldCreateLongTaskTimer){LongTaskTimer.SamplelongTaskSamplecontext.getRequired(LongTaskTimer.Sample.class);longTaskSample.stop();}}3.2.4 查询数据访问actuator/metrics/http.server.requests和【采集时触发】流程一致唯一的区别就是数据已经在内存中了。返回示例{availableTags:[{tag:exception,values:[none,IOException]},{tag:method,values:[GET]},{tag:error,values:[none,IOException]},{tag:uri,values:[/actuator/metrics/{requiredMetricName},/actuator/metrics,/actuator/prometheus]},{tag:outcome,values:[SUCCESS]},{tag:status,values:[200]}],baseUnit:seconds,measurements:[{statistic:COUNT,value:291},{statistic:TOTAL_TIME,value:8785.79363672},{statistic:MAX,value:714.0757704}],name:http.server.requests
返回列表