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