Micrometer 与可观测性
指标、链路追踪、与 Actuator 配合。
Micrometer 与可观测性
这一课你将能做到
- 说清 Metrics / Logs / Traces 三支柱各自回答什么问题,以及 Micrometer 在其中的位置
- 用 Actuator 暴露 Prometheus 格式指标,并给判题服务加上 Counter、Timer、Gauge 三类自定义指标
- 接入 Micrometer Tracing 让 traceId 跨服务传递,一次请求出问题能按 traceId 串起所有服务的日志
为什么要学
单体时代出问题:登录服务器 tail -f app.log 就完事。微服务时代一次请求要穿过 api-gateway → user-service → problem-service,日志散在三个进程、三台机器(容器化后还随时漂移)。「用户说下单慢」——你连日志在哪个文件里都要先找一圈。
可观测性(Observability)三支柱各回答一个问题:
| 支柱 | 回答的问题 | 载体 | 类比 |
|---|
| Metrics 指标 | 系统现在健康吗?(QPS、延迟、错误率) | 时序数值,便宜可聚合 | 体检报告 |
| Logs 日志 | 刚才具体发生了什么? | 文本/结构化事件,贵而详细 | 病历记录 |
| Traces 链路 | 这一次请求的时间都花在哪一跳? | traceId 串联的调用树 | 全程行车记录 |
标准排查姿势:指标报警 → 发现 P99 毛刺 → 拿一个慢请求的 traceId → 在各服务日志里串出完整链路 → 定位到 problem-service 的慢 SQL。三支柱缺一不可。
Micrometer 是指标门面(类似 SLF4J 之于日志):代码面向 MeterRegistry 打点,底层可对接 Prometheus、Datadog、InfluxDB 等。Spring Boot 3.x 的 Actuator 底层就是 Micrometer——你之前看到的 health 检查,和本课的 metrics 同属一套体系。
先建立图像
judge-service (:8083)
┌───────────────────────────────────────────────┐
│ 业务代码 ── registry.counter("judge.submit") │ ← 打点(内存累加)
│ 业务代码 ── Timer 记录判题耗时 │
│ │ │
│ Actuator /actuator/prometheus ← 抓取端点 │ ← 暴露(文本格式)
└───────┬───────────────────────────────────────┘
│ Prometheus 每隔 15s 主动来抓(Pull 模型)
┌───────▼────────┐ ┌─────────────┐
│ Prometheus │ ───▶ │ Grafana │ ← 查询 + 看板 + 告警
│ 时序数据库 │ └─────────────┘
└────────────────┘
一次跨服务请求的 traceId 传播:
客户端 → api-gateway(8080) → judge-service(8083) → problem-service(8082)
│生成 traceparent header│ 原样透传 │ 原样透传
▼ ▼ ▼
traceId=abc123 同一个 abc123 同一个 abc123
spanId=s1 spanId=s2 spanId=s3
每个服务的日志行都带 [abc123/s2] —— grep 一次,全链路日志到手
跟着做
步骤 1:judge-service 暴露 Prometheus 指标
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: ${spring.application.name} # 所有指标带上服务名 tag,多服务聚合时区分用
启动后访问 http://localhost:8083/actuator/prometheus,能看到 Spring Boot 自动注册的指标(JVM 内存、GC、线程池、HTTP、连接池全都有):
# HELP http_server_requests_seconds Duration of HTTP request handling
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="POST",uri="/judge/submit",status="200"} 42
http_server_requests_seconds_sum{method="POST",uri="/judge/submit",status="200"} 25.31
http_server_requests_seconds_max{method="POST",uri="/judge/submit",status="200"} 3.2
count 是调用次数、sum 是总耗时——两者相除即平均延迟,Prometheus 会帮你持续计算。生产再加 Prometheus 服务器(定期抓取这个端点存成时序数据)和 Grafana(查询绘图),就完成了指标链路。
步骤 2:自定义业务指标——Counter(判题次数)
package com.example.judge.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class JudgeMetrics {
private final Counter submitCounter;
public JudgeMetrics(MeterRegistry registry) {
this.submitCounter = Counter.builder("judge.submit.total")
.description("判题提交次数")
.tag("language", "java") // tag 是维度,查询时可拆分
.register(registry);
}
public void recordSubmit(String language) {
submitCounter.increment();
}
}
在判题入口调用 recordSubmit("java") 后,/actuator/prometheus 里出现 judge_submit_total_total{language="java",application="judge-service"} 1。Counter 只增不减,适合统计次数。
步骤 3:Timer 记录判题耗时(看 P95/P99)
平均延迟会骗人:10 个请求 9 个 10ms、1 个 10s,平均 1s 但用户体验极差。看分位数才真实:
package com.example.judge.service;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class JudgeTimerService {
private final Timer judgeTimer;
public JudgeTimerService(MeterRegistry registry) {
this.judgeTimer = Timer.builder("judge.duration")
.description("判题耗时")
.publishPercentiles(0.95, 0.99) // 在应用侧预聚合 P95/P99
.register(registry);
}
public void judge() {
Timer.Sample sample = Timer.start();
try {
doRealJudge(); // 真正的判题逻辑
} finally {
sample.stop(judgeTimer); // 记录本次耗时
}
}
private void doRealJudge() {
// ...沙箱执行与比对
}
}
也可以更简单:registry.timer("judge.duration").record(() -> doRealJudge())。输出里会出现 judge_duration_seconds{quantile="0.95"}。
步骤 4:Gauge 观察队列长度
判题请求提交进队列、沙箱慢慢消费,队列堆积是判题服务最重要的健康信号:
package com.example.judge.config;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.BlockingQueue;
@Configuration
public class QueueMetricsConfig {
public QueueMetricsConfig(MeterRegistry registry, BlockingQueue<Integer> judgeQueue) {
Gauge.builder("judge.queue.size", judgeQueue, BlockingQueue::size)
.description("待判题任务数")
.register(registry);
}
}
Gauge 是「瞬时值」,抓取时读一次当前队列长度——堆积曲线一旦持续上涨,说明消费能力跟不上了。
步骤 5:接入分布式链路 Tracing
pom.xml(Spring Boot 3.x 用 Micrometer Tracing,Sleuth 已停更):
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
management:
tracing:
sampling:
probability: 1.0 # 生产流量大建议 0.1(采 10%),学习环境全采
observations:
key-values:
application: judge-service
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]" # 日志行带上 traceId/spanId
2026-10-06 21:00:01.123 INFO [judge-service,6b2f2e0a1c9d4e5f,8a1b2c3d4e5f6a7b] 提交判题:problemId=1
6b2f2e0a1c9d4e5f 就是 traceId。Micrometer Tracing 默认遵循 W3C Trace Context 规范,HTTP 调用时自动携带并解析 traceparent 请求头,Feign、RestClient、网关的 WebClient 都被自动埋点——你一行传播代码都不用写。
本地把 span 发到 Zipkin 看可视化调用树:
docker run -d -p 9411:9411 openzipkin/zipkin
再访问 http://localhost:9411,按 traceId 或服务名搜,能看到 gateway → judge → problem 每一跳的耗时瀑布图。
步骤 6:端到端验证
- 依次启动 eureka → gateway → judge-service → problem-service;
- 从网关发一次判题请求;
- 在 judge-service 日志复制 traceId,去 problem-service 日志里搜——同一个 traceId,说明传播成功;
- 打开 Zipkin 搜索该 trace,观察每一跳耗时。
讲清楚要点
- 三支柱分工(背下来):Metrics 告诉你「有问题」(聚合、便宜、适合告警);Logs 告诉你「发生了什么细节」(贵、详尽、适合排查);Traces 告诉你「时间花在哪一跳」(串联跨服务调用树)。联动方式:指标告警 → 日志定位 → traceId 串链。
- Micrometer 是指标门面:代码只依赖
MeterRegistry 和 Counter/Timer/Gauge 三种核心量器,换后端(Prometheus → Datadog)不改业务代码;Actuator 的 health/metrics 端点底层就是它。
- tag 基数爆炸是大坑:tag 用于拆分维度,每个「tag 名+值」组合都是一条独立时间线。
userId 有几万种取值 → Prometheus 内存爆掉、查询变慢。原则:tag 值必须是有限小集合(语言、难度、状态码、结果状态),用户 ID、订单号这种高基数信息去日志里查,不要进指标。
- 分位数怎么看:Timer 配
publishPercentiles(0.95, 0.99) 输出 P95/P99——95% 的请求快于该值。SLA 常用 P99 而不是平均值,因为平均值掩盖长尾;「QPS 正常但用户投诉慢」几乎都是 P99 毛刺问题。
- traceId 传播机制(面试高频):入口(网关或第一个服务)生成 traceId → W3C 规范写入
traceparent: 00-{traceId}-{spanId}-{flags} 请求头 → Feign/RestClient/RestTemplate 自动透传 → 下游解析后继续向下传;每个服务内每跨一次边界产生新 spanId。日志里带上 traceId/spanId,就能跨服务 grep 出一次请求的完整轨迹。
- 采样率:
management.tracing.sampling.probability 控制上报比例,1.0 全采仅适合开发和低流量场景;生产 0.1 起步,错误请求通常强制全采(tail-based sampling,进阶)。
你可能会懵的地方
-
Prometheus 是 Pull 模型,服务缩容到 0 或抓取间隔内崩溃怎么办?
Pull 会丢掉抓取间隙的数据,但指标本来就是聚合视角,丢秒级数据可接受;对「必须不丢」的事件用日志或事件流。另外 Pushgateway 用于批处理任务的指标上报,属于补充手段。
-
traceId 和 spanId 到底什么关系?
traceId 标识一次完整请求(跨所有服务不变);spanId 标识一次「服务内的一个工作单元」(每次跨服务调用、每处理一步都产生新 span)。同一个 traceId 下多个 span 组成树,Zipkin/Jaeger 渲染出耗时瀑布图——先看总 span 找最慢的分支,再逐层下钻。
-
生产环境 sampling.probability 设多少?
流量 1k QPS 以下可 0.1~1.0;大流量 0.01~0.1。原则:采样率只影响「看得见的请求比例」,不影响传播本身(traceId 依然全量穿透到日志);只要日志带 traceId,即使该 trace 未上报 Zipkin,也能靠日志串链排查。
-
为什么打了 Counter 却看不到指标?
①没加 micrometer-registry-prometheus(指标只存在内存,没有暴露格式);②management.endpoints.web.exposure.include 没含 prometheus;③Counter 从未 increment()(Micrometer 默认只在第一次使用后才出现在输出里)。
-
指标、日志、链路都用什么开源组合落地?
最常见组合:Prometheus(存指标)+ Grafana(看板告警)+ Loki 或 ELK(日志)+ Zipkin/Jaeger/Tempo(链路)。国内云厂商常用阿里云 ARMS / SLS 一体化方案,概念完全一致,只是托管化了。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 指标 tag 里放 userId / orderId | 基数爆炸,监控系统内存与查询双双崩塌 | tag 只用低基数字段,明细查日志 |
| 只看平均耗时不看分位数 | 平均值掩盖长尾,用户慢但指标正常 | Timer 配 publishPercentiles 看 P95/P99 |
| 每个服务日志格式随意、无 traceId | 跨服务排错靠人肉对时间戳 | logging.pattern.level 统一带 traceId/spanId |
| sampling.probability: 1.0 直接上生产 | 全量上报打爆链路后端存储 | 生产 0.1 起步,按流量调 |
| 只打业务指标不配告警 | 出问题依旧靠用户反馈 | 核心指标配 Prometheus 告警规则通知值班 |
卡点急救
/actuator/prometheus 返回 404:三查——actuator 依赖在不在;micrometer-registry-prometheus 依赖在不在;exposure.include 里有没有 prometheus。
- 日志里 traceId 是空的:确认加了
micrometer-tracing-bridge-brave 且改了 logging.pattern.level;确认请求经过了自动埋点的客户端(Feign/RestClient),自己 new 的 HttpURLConnection 不会被埋点。
- Zipkin 界面查不到任何 trace:确认 zipkin-reporter-brave 依赖在、
management.tracing.sampling.probability 大于 0、Zipkin 容器映射了 9411 端口;先用日志里的 traceId 证实链路是通的,再排查上报。
面试常问
-
可观测性三支柱分别解决什么问题?怎么配合?
Metrics:聚合的时序数值(QPS、延迟分位、错误率),成本低,用于告警和大盘;Logs:离散的详细事件,用于还原细节;Traces:以 traceId 串联跨服务调用树,用于定位慢在哪一跳。配合流程:指标告警发现异常 → 按时间与维度过滤日志找到可疑请求 → 拿 traceId 在链路系统还原完整调用树 → 定位根因。
-
traceId 是怎么跨服务传播的?(必背)
入口服务(或网关)为请求生成 traceId,遵循 W3C Trace Context 规范写入 traceparent 请求头;下游服务由自动埋点(Micrometer Tracing + Brave/OTel)解析请求头并延续同一个 traceId,每跨一次边界新建 spanId,向下继续透传。Spring Cloud 下 Feign、RestClient、WebClient、网关路由均已自动埋点,业务代码零侵入;日志 pattern 加入 %X{traceId} 后即可跨服务串日志。
-
QPS 很高但用户反馈很慢,怎么排查?
先看指标:平均延迟正常而 P99 飙高,说明是长尾问题而非容量问题。用 Timer 的分位数据确认毛刺时段,取一个慢请求的 traceId 看 Zipkin 瀑布图,定位最慢一跳(常见:慢 SQL、下游 Feign 串行调用过多、GC 停顿、连接池耗尽)。再下钻该服务日志与 JVM 指标(GC 次数/线程池队列)锁定根因。答案要点:不用平均值用分位数、用 traceId 缩小范围、按跳逐层下钻。
自测三问
- 三支柱里,「哪一跳最慢」由谁回答?「系统现在健康吗」由谁回答?
- 为什么不能把 userId 当指标 tag?有什么替代方案?
- W3C Trace Context 的请求头叫什么?里面包含哪几段信息?
小结一句话
指标告诉你要不要慌、日志告诉你发生了什么、链路告诉你在哪一跳——Micrometer 统一打点,Actuator 暴露,Prometheus/Grafana/Zipkin 负责存、看、查。
延伸