Micrometer 系列【4】入门案例

文章目录

  • 前言
  • 1. 环境准备
    • 1.1 开发环境
    • 1.2 依赖配置
  • 2 基础知识:埋点
    • 2.1 核心定义
    • 2.2 分类(多角度)
    • 2.3 手动埋点:定制业务逻辑监控
      • 2.3.1 编程式
      • 2.3.2 注解式
    • 2.4 自动埋点:开箱即用
  • 3. 代码实现
    • 3.1 创建注册表
    • 3.2 自动埋点
    • 3.3 手动埋点
    • 3.4 输出指标
    • 3.5 输出示例

前言

需求说明:

  1. 不依赖Spring/SpringBoot
  2. 不接入PrometheusGrafana等监控服务;
  3. 仅使用Micrometer核心包,内存内完成埋点、读取指标;
  4. 死循环模拟业务操作(生产/消费任务),同时绑定JVM内置指标;
  5. 采用内置SimpleMeterRegistry(内存注册表,仅本地存储,不对外推送)。

1. 环境准备

1.1 开发环境

工具版本
JDK17+
Maven3.6+
Micrometer1.17.0

本示例仅依赖micrometer-core,无需SpringPrometheus或任何外部服务。


1.2 依赖配置

创建一个普通Maven工程,只引入micrometer核心基础包,无任何web、监控导出依赖:

<dependencyManagement><dependencies><dependency><groupId>io.micrometer</groupId><artifactId>micrometer-bom</artifactId><version>1.17.0</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>io.micrometer</groupId><artifactId>micrometer-core</artifactId></dependency></dependencies>

micrometer-core包:

micrometer-core严格遵循最小依赖

  • 编译期仅依赖JSpecify,无其他第三方强制依赖;

  • 停顿检测功能需要运行时依赖LatencyUtils(默认关闭,故不默认引入);

  • 客户端百分位统计需要运行时依赖HdrHistogram,不用可直接排除。


2 基础知识:埋点

2.1 核心定义

可观测性中的埋点(英文通常叫instrumentationtelemetry/data collection),是指在系统内植入采集逻辑,收集指标、日志、链路追踪等遥测数据,以此还原系统内部运行状态的技术手段,是可观测性体系数据采集的核心基础。

简单来说,就是提前在系统各处布下 “信息采集传感器”,传感器持续产出指标、调用链路、运行日志三类信息。有了这些数据,我们才能看清分布式系统内部运行状况,做到快速排查故障、分析性能问题。

典型应用场景:

  • 用户行为分析:页面访问、按钮点击、转化漏斗
  • 性能监控:接口耗时(RT)、QPS、错误率
  • 业务指标:订单量、支付金额、活跃用户数
  • 故障排查:异常堆栈、链路追踪

2.2 分类(多角度)

实现方式可分为:

类型做法特点
自动埋点SDK 自动采集通用信号省事,覆盖不到业务细节
手动埋点手写采集代码精准,但要维护
混合埋点两者结合最优实践

数据类型可分为:

类型采什么例子比喻
指标埋点数值统计(数量、耗时)“今天订单量 1000、平均耗时 200ms”仪表盘
日志埋点事件明细记录“用户 A 在 10:23 登录失败”行车记录仪
链路埋点调用路径追踪“请求→网关→订单服务→支付服务”导航轨迹

位置层次可分为:

类型位置典型采集内容
前端埋点浏览器 / H5页面浏览、点击、停留时长、转化漏斗
移动端埋点App / 小程序启动、崩溃、手势、页面跳转
后端埋点服务端接口耗时、QPS、错误率、依赖调用
基础设施埋点主机 / 容器 / 中间件CPU、内存、磁盘、网络、JVM、DB 连接池

业务目的可分为:

类型目的典型场景
业务埋点分析用户行为、衡量业务效果注册转化、下单漏斗、功能使用率
性能埋点监控系统性能表现接口 RT、页面加载时间、慢查询
异常埋点捕获和追踪错误接口报错、崩溃、超时、限流触发
安全埋点审计合规、风控登录尝试、越权操作、敏感数据访问

指标类型可分为:

类型采集形态例子对应 API
计数型只增不减的累计值订单总数、错误次数Counter
计时型事件的耗时分布接口响应时间Timer
分布型数据的分布情况一批请求耗时的 P99DistributionSummary
状态型瞬时高低起伏的值当前连接数、内存占用Gauge
增量型可增可减的累计值队列长度、积分余额Gauge(或自增自减)

采集时机可分为:

类型触发方式例子
事件触发型用户/系统动作发生时记录点击按钮、发起请求
状态变更型某个状态发生变化时记录订单状态从"待支付"变"已支付"
周期采集型定时轮询采集当前状态每隔 10 秒抓一次 CPU 使用率
异常触发型发生异常时才记录接口超时、抛异常时上报堆栈

同一个埋点,换个角度就属于不同类别:

比如"记录订单接口的耗时"├── 角度一(怎么埋):手动埋点 ├── 角度二(埋什么):指标埋点 ├── 角度三(埋在哪):后端埋点 ├── 角度四(为什么):性能埋点 ├── 角度五(什么形态):计时型 └── 角度六(什么时候):事件触发型

2.3 手动埋点:定制业务逻辑监控

当需要监控特定业务流程(如订单支付成功率、特定方法耗时)时,自动埋点无法满足需求,此时需通过代码进行手动埋点。

手动埋点又分两类:编程式(直接调API)和注解式AOP拦截)。

2.3.1 编程式

直接用MeterRegistry创建和调用,核心分为三步获取创建调用

简单示例:

// 1. 获取 MeterRegistry(通常通过依赖注入)@AutowiredprivateMeterRegistryregistry;// 2. 创建或获取 meter(同名同 tag 只创建一次)Counterorders=registry.counter("orders.created","channel","app");TimerapiTimer=registry.timer("api.request.duration","endpoint","/order");// 3. 在关键位置"埋点"调用orders.increment();// Counter: +1apiTimer.record(()->doSomething());// Timer: 自动统计耗时registry.gauge("queue.size",queue,Queue::size);// Gauge: 实时反映队列大小

或者用全局静态入口:

Metrics.counter("orders.created").increment();

2.3.2 注解式

使用@Timed等注解标记在方法或类上,Micrometer会通过AOP自动拦截并记录该方法执行耗时。这种方式代码侵入性小,适合快速统计接口性能,但无法像编程式那样灵活记录业务状态。‌‌

使用示例:

@Timed(value="order.create",extraTags={"service","order"},percentiles={0.5,0.95})publicOrdercreateOrder(@MeterTag(key="userType")StringuserId){// 方法执行时间自动记录到 Timer// 异常时自动打 exception="XxxException" 标签returndoCreate(userId);}@Counted(value="payment.count",recordFailuresOnly=true)publicvoidpay(StringorderId){// 仅失败时计数,标签 result="failure" + exception="..."doPay(orderId);}

2.4 自动埋点:开箱即用

Micrometer提供了多种框架、中间件、数据库等指标集成,会自动采集大量基础指标,无需编写额外代码。这种方式适用于监控JVM状态、HTTP请求、数据库连接池等通用场景。

Spring Boot使用场景中,只需引入spring-boot-starter-actuator和对应的注册表依赖(如micrometer-registry-prometheus),并通过配置management.endpoints.web.exposure.include暴露监控端点即可 。‌‌

如果只使用Micrometer核心依赖,需要手动创建注册表,并绑定指标器,即可自动实现埋点采集指标。

3. 代码实现

完整代码如下:

publicclassJvmMetricsDemoApplication{publicstaticvoidmain(String[]args)throwsInterruptedException{SimpleMeterRegistryregistry=newSimpleMeterRegistry();Metrics.addRegistry(registry);// ========== 1. 注册 JVM 指标 ==========newJvmMemoryMetrics().bindTo(registry);newJvmGcMetrics().bindTo(registry);newJvmThreadMetrics().bindTo(registry);newProcessorMetrics().bindTo(registry);// ========== 2. 业务对象 + 业务指标 ==========OrderServiceorderService=newOrderService();TimerorderTimer=registry.timer("biz.order.create");// ========== 3. 死循环调用业务 ==========System.out.println("=== JVM + 业务指标监控 (每5秒输出一次,Ctrl+C 停止) ===\n");longlastPrint=System.currentTimeMillis();while(true){// 调用业务方法,自动记录耗时orderTimer.record(orderService::createOrder);// 每 5 秒输出全部指标longnow=System.currentTimeMillis();if(now-lastPrint>=5000){lastPrint=now;printAllMetrics(registry);}TimeUnit.MILLISECONDS.sleep(200);}}privatestaticvoidprintAllMetrics(SimpleMeterRegistryregistry){System.out.println("───── "+LocalTime.now()+" ─────");registry.getMeters().forEach(meter->meter.measure().forEach(ms->{if(ms.getValue()>0){System.out.printf("%-45s %-15s %.2f%n",meter.getId().getName(),ms.getStatistic(),ms.getValue());}}));System.out.println();}/** * 业务对象 */staticclassOrderService{voidcreateOrder(){try{// 模拟业务逻辑耗时 20~100msTimeUnit.MILLISECONDS.sleep((long)(Math.random()*80+20));}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}}

3.1 创建注册表

SimpleMeterRegistryMicrometer内置的内存级注册表,所有指标数据仅保存在JVM堆内存中,不对外推送、不落盘,适合开发调试和单元测试场景。

// SimpleMeterRegistry:内存注册表,不依赖任何外部监控系统MeterRegistryregistry=newSimpleMeterRegistry();

通常,一个应用只有一个MeterRegistry入口,可以使用CompositeMeterRegistry包含多个子Registry,实现一次埋点、到处上报

3.2 自动埋点

使用bindTo(registry)JVM内部状态(内存、GC、线程、CPU)自动绑定为Micrometer指标,之后每次读取时实时获取当前值,无需手动采样。

// 绑定 JVM 内置指标newJvmMemoryMetrics().bindTo(registry);// 堆内存、非堆内存newJvmGcMetrics().bindTo(registry);// GC 次数、耗时newJvmThreadMetrics().bindTo(registry);// 线程数、状态分布newProcessorMetrics().bindTo(registry);// CPU 使用率、CPU 时间

3.3 手动埋点

业务对象是一个简单的内部类,createOrder()模拟20~100ms的业务处理耗时:

staticclassOrderService{voidcreateOrder(){// 模拟业务逻辑耗时 20~100msTimeUnit.MILLISECONDS.sleep((long)(Math.random()*80+20));}}

创建一个普通业务对象,再用Timer来统计其方法调用耗时。Timer是最常用的业务埋点方式。

// 创建业务对象OrderServiceorderService=newOrderService();// 注册 Timer,统计下单耗时TimerorderTimer=registry.timer("biz.order.create");

死循环调用业务,Timer自动记录每次调用的耗时。

longlastPrint=System.currentTimeMillis();while(true){// 调用业务方法,Timer 自动记录耗时orderTimer.record(orderService::createOrder);// 每 5 秒输出全部指标longnow=System.currentTimeMillis();if(now-lastPrint>=5000){lastPrint=now;printAllMetrics(registry);}TimeUnit.MILLISECONDS.sleep(200);// 避免 CPU 空转}

3.4 输出指标

遍历注册表中所有Meter,打印每个测量值。过滤零值以减少输出噪音。

privatestaticvoidprintAllMetrics(SimpleMeterRegistryregistry){System.out.println("───── "+LocalTime.now()+" ─────");registry.getMeters().forEach(meter->meter.measure().forEach(ms->{if(ms.getValue()>0){System.out.printf("%-45s %-15s %.2f%n",meter.getId().getName(),ms.getStatistic(),ms.getValue());}}));System.out.println();}

3.5 输出示例

───── 13:51:45 ───── jvm.memory.used VALUE 16800000.00 ← 堆内存已用 jvm.memory.max VALUE 1073741824.00 ← 堆内存上限 jvm.memory.committed VALUE 163577856.00 ← 堆内存已提交 jvm.threads.live VALUE 11.00 ← 活跃线程 jvm.threads.daemon VALUE 9.00 ← 守护线程 jvm.threads.peak VALUE 11.00 ← 线程峰值 jvm.threads.states VALUE 5.00 ← RUNNABLE 线程 jvm.threads.started COUNT 11.00 ← 累计启动线程 jvm.gc.max.data.size VALUE 4255121408.00 ← GC 数据区上限 process.cpu.usage VALUE 0.08 ← CPU 使用率 process.cpu.time COUNT 4062500000.00 ← CPU 时间(ns) system.cpu.count VALUE 12.00 ← CPU 核心数 biz.order.create COUNT 25.00 ← 下单调用次数 biz.order.create TOTAL_TIME 1.35 ← 下单总耗时(秒) biz.order.create MAX 0.09 ← 下单最大耗时(秒)