CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Spring Cloud 微服务入门/课时/Micrometer 与可观测性
RawBlame
课时目录10 节

Spring Cloud 微服务入门

  1. 01微服务与 Spring Cloud
  2. 02架构模式与术语
  3. 03服务注册与 Eureka
  4. 04Spring Cloud Gateway
  5. 05Cloud Config 配置中心
  6. 06OpenFeign 服务调用
  7. 07熔断与容错
  8. 08Spring Data 扩展
  9. 09Micrometer 与可观测性
  10. 10综合练习与部署直觉

Micrometer 与可观测性

指标、链路追踪、与 Actuator 配合。

30 分钟

Micrometer 与可观测性

这一课你将能做到

  1. 说清 Metrics / Logs / Traces 三支柱各自回答什么问题,以及 Micrometer 在其中的位置
  2. 用 Actuator 暴露 Prometheus 格式指标,并给判题服务加上 Counter、Timer、Gauge 三类自定义指标
  3. 接入 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 指标

pom.xml:

<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>

application.yml:

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>

application.yml:

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:端到端验证

  1. 依次启动 eureka → gateway → judge-service → problem-service;
  2. 从网关发一次判题请求;
  3. 在 judge-service 日志复制 traceId,去 problem-service 日志里搜——同一个 traceId,说明传播成功;
  4. 打开 Zipkin 搜索该 trace,观察每一跳耗时。

讲清楚要点

  1. 三支柱分工(背下来):Metrics 告诉你「有问题」(聚合、便宜、适合告警);Logs 告诉你「发生了什么细节」(贵、详尽、适合排查);Traces 告诉你「时间花在哪一跳」(串联跨服务调用树)。联动方式:指标告警 → 日志定位 → traceId 串链。
  2. Micrometer 是指标门面:代码只依赖 MeterRegistry 和 Counter/Timer/Gauge 三种核心量器,换后端(Prometheus → Datadog)不改业务代码;Actuator 的 health/metrics 端点底层就是它。
  3. tag 基数爆炸是大坑:tag 用于拆分维度,每个「tag 名+值」组合都是一条独立时间线。userId 有几万种取值 → Prometheus 内存爆掉、查询变慢。原则:tag 值必须是有限小集合(语言、难度、状态码、结果状态),用户 ID、订单号这种高基数信息去日志里查,不要进指标。
  4. 分位数怎么看:Timer 配 publishPercentiles(0.95, 0.99) 输出 P95/P99——95% 的请求快于该值。SLA 常用 P99 而不是平均值,因为平均值掩盖长尾;「QPS 正常但用户投诉慢」几乎都是 P99 毛刺问题。
  5. traceId 传播机制(面试高频):入口(网关或第一个服务)生成 traceId → W3C 规范写入 traceparent: 00-{traceId}-{spanId}-{flags} 请求头 → Feign/RestClient/RestTemplate 自动透传 → 下游解析后继续向下传;每个服务内每跨一次边界产生新 spanId。日志里带上 traceId/spanId,就能跨服务 grep 出一次请求的完整轨迹。
  6. 采样率: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 告警规则通知值班

卡点急救

  1. /actuator/prometheus 返回 404:三查——actuator 依赖在不在;micrometer-registry-prometheus 依赖在不在;exposure.include 里有没有 prometheus。
  2. 日志里 traceId 是空的:确认加了 micrometer-tracing-bridge-brave 且改了 logging.pattern.level;确认请求经过了自动埋点的客户端(Feign/RestClient),自己 new 的 HttpURLConnection 不会被埋点。
  3. 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 缩小范围、按跳逐层下钻。

自测三问

  1. 三支柱里,「哪一跳最慢」由谁回答?「系统现在健康吗」由谁回答?
  2. 为什么不能把 userId 当指标 tag?有什么替代方案?
  3. W3C Trace Context 的请求头叫什么?里面包含哪几段信息?

小结一句话

指标告诉你要不要慌、日志告诉你发生了什么、链路告诉你在哪一跳——Micrometer 统一打点,Actuator 暴露,Prometheus/Grafana/Zipkin 负责存、看、查。

延伸

  • Micrometer 官方文档(Concepts / Tracing)
  • Spring Boot Actuator:生产可用特性
  • 下一课:综合练习与部署直觉——把五个进程串成完整链路,并预览 Docker/K8s 的部署世界。
加载本节练习…
← 上一课Spring Data 扩展下一课 →综合练习与部署直觉