分类:架构与高并发 · 作者:管理员 · 发布于 2026-10-18 · 阅读 0 · 标签:Spring Boot / 可观测性 / Micrometer / Prometheus / Grafana

一、痛点:没有指标的系统,排查靠「感觉」

很多 Java 后端项目的可观测性停留在「有日志」阶段:接口慢了看日志、内存涨了重启、消息积压了靠人工盯。但日志只能回答「发生了什么」,回答不了「趋势如何」——内存是缓慢泄漏还是突发暴涨?下单耗时是从哪次发版后开始劣化的?队列积压是否每逢大促都会规律性抬升?这些问题需要指标(Metrics)来回答:按时序采集的数值,既能看实时曲线,又能回溯历史。

本文将用一个电商中台服务为例,完整落地一套轻量可观测性方案:Micrometer(埋点 API)+ Prometheus(时序存储与抓取)+ Grafana(可视化与告警)。不引入重量级 APM,三个组件全是社区标配,一个下午就能跑起来。

二、整体架构与三件套分工

  • Micrometer:应用内的指标门面。Spring Boot 2.x/3.x 已内置,只需加一个 prometheus 注册表依赖,它就能把 JVM、线程池、HTTP 请求等指标统一暴露成 Prometheus 格式。
  • Prometheus:定时来应用拉取指标(/actuator/prometheus),存为时序数据,并提供 PromQL 查询。
  • Grafana:连接 Prometheus 数据源,画看板、配告警,出问题第一时间推到群。

三、接入:三行依赖 + 一段配置

第一步,pom.xml 加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

第二步,application.yml 只暴露需要的端点,别把 env、beans 这些内部结构漏出去:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      show-details: never
  metrics:
    tags:
      application: order-center   # 全局标签,多服务共用一套 Prometheus 时靠它区分

此时访问 /actuator/prometheus 就能看到一大片指标:jvm_memory_used_bytes、http_server_requests_seconds、process_cpu_usage……零手写代码,JVM 与 Web 层核心指标已经全了,这就是 Micrometer 的价值。

四、自定义业务指标:把系统「状态」变成数字

框架指标只覆盖通用维度,业务系统的关键水位必须自己埋。Micrometer 有四种核心类型:

  • Counter:只增不减的计数器,如「下单成功数」「支付回调失败数」。
  • Gauge:可增可减的当前值,如「待处理消息积压数」「连接池活跃连接数」。
  • Timer:耗时与次数的统计,如「下单接口耗时」。
  • DistributionSummary:数值分布统计,如「订单金额分布」。

实战一:统计订单创建量(Counter):

@Service
public class OrderMetricService {
    private final Counter orderCreateCounter;

    public OrderMetricService(MeterRegistry registry) {
        this.orderCreateCounter = Counter.builder("order.create.total")
                .description("累计创建订单数")
                .register(registry);
    }

    public void afterCreate() {
        orderCreateCounter.increment();
    }
}

实战二:监控 MQ 消费者积压(Gauge)。这里有个关键点:Gauge 要绑定一个可查询的「当前值来源」,而不是自己维护数字:

@Component
public class MqBacklogMonitor {
    public MqBacklogMonitor(MeterRegistry registry,
                            RabbitAdmin rabbitAdmin,
                            @Value("${mq.queue.order}") String queue) {
        Gauge.builder("mq.queue.backlog", rabbitAdmin,
                admin -> {
                    var props = admin.getQueueProperties(queue);
                    if (props == null) return 0;
                    return ((Number) props.get("QUEUE_MESSAGE_COUNT")).doubleValue();
                })
            .description("订单队列积压消息数")
            .register(registry);
    }
}

实战三:下单耗时(Timer),用 @Timed 注解最省事:

@Timed(value = "order.create.cost", percentiles = {0.5, 0.9, 0.99})
@PostMapping("/orders")
public Result<OrderVO> create(@RequestBody @Valid OrderRequest req) {
    // 下单主流程
}

自定义线程池也要纳入监控。手动 new 的 ThreadPoolExecutor 是监控盲区,用 ExecutorServiceMetrics 包装即可:

ExecutorService executor = Executors.newFixedThreadPool(8);
ExecutorService monitored = ExecutorServiceMetrics.monitor(registry, executor, "business-pool");

再补充一个高频场景:用指标做容量评估。大促前很多人拍脑袋加机器,其实看三张图就够:过去 30 天请求量峰值、单实例 CPU 与连接池水位、队列积压拐点。把这些指标按周同比放到同一张看板,容量规划就从「感觉」变成「数据」。

五、五个生产级踩坑记录

坑 0(先补一个):@Timed 注解不生效。很多人写完 @Timed 发现指标根本没出现,因为注解靠 AOP 织入,需要额外引入 aspect 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

且 @Timed 要标在 Bean 方法上(Controller 方法也行,但要注意代理生效:类不能被 final)。验证方式很简单:调用一次接口,去 /actuator/prometheus 搜指标名,搜不到就是没织入成功。

坑 1:标签基数爆炸(最致命)。标签(Tag)每多一种取值就多一条时间序列。有人把 userId、orderId 加进标签想按用户拆分,大促时每秒几十万条序列,直接把 Prometheus 内存打爆。经验法则:标签只放低基数维度(channel、region、status),高基数信息请走日志或数据库。

坑 2:Timer 的单位混淆。Micrometer 的 Timer 基于秒(seconds),而业务习惯算毫秒。查平均耗时直接用 rate(order_create_cost_seconds_sum[5m]) / rate(order_create_cost_seconds_count[5m]),别自己手动换算导致数值差三个数量级。

坑 3:percentiles 与 histogram 的取舍。@Timed 里 percentiles 是客户端算好的摘要,适合少量分位数;需要跨实例聚合分位数(如全局 P99)就用 histogram = true,配合 Prometheus 的 histogram_quantile。两者混用会重复采集,白费存储。

坑 4:自己 new 的线程池是监控盲区。Spring 的 @Async 线程池会被自动监控,但业务里手动 new 的 ThreadPoolExecutor 看不到。统一走 ThreadPoolTaskExecutor Bean,或用 ExecutorServiceMetrics 显式包装(见上文),否则线程耗尽时你连曲线都看不到。

坑 5:Actuator 端点裸奔。/actuator/prometheus 建议只用内网网段访问,或用 Spring Security 加白名单;Grafana 看板和告警规则要纳入版本管理,否则改一次仪表盘就不可追溯。

六、Prometheus 抓取与 Grafana 告警

Prometheus 侧,一个抓取任务即可:

scrape_configs:
  - job_name: order-center
    metrics_path: /actuator/prometheus
    static_configs:
      - targets: ['order-center:8080']

抓取频率默认 15s,接口量级不大时不必调太密,太密只是多烧存储;反之,storage.tsdb.retention.time 建议按业务保留 15~30 天,太短的保留期会让「回溯上周同一时段对比」变成空谈。服务实例多了以后,尽量用 service discovery 而不是 static_configs,否则每次扩容都要改配置文件重载,早晚漏掉一台。

Grafana 里新建告警规则,用 PromQL 表达式,例如「订单队列积压超过 1 万且持续 5 分钟」:

max(mq_queue_backlog{application="order-center"}) > 10000

配合 Alertmanager 可以推到企业微信群/钉钉,实现「指标异常主动找上门」,而不是等用户投诉。Alertmanager 的 route 与 receiver 按团队分流,运维组收服务可用性告警,业务组收业务指标告警,避免告警轰炸:

route:
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - receiver: ops-webhook
      matchers: ['severity="critical"']

常用 PromQL 查询,建议直接存成 Grafana 变量方便复用:

# 5 分钟请求量
sum(rate(http_server_requests_seconds_count[5m]))
# 接口 P99 耗时(按 path 分组)
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))
# 堆内存使用率
jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}

七、最佳实践清单

  1. 先盘点框架自带指标,再决定自定义埋点,避免重复造轮子。
  2. 核心业务链路(下单、支付、消息消费)必须有 Counter/Timer 埋点,这是容量评估的依据。
  3. 指标命名遵循 domain.action.result 风格,单位写进 description。
  4. 告警规则宁少勿滥:只对「影响用户或无法自愈」的指标告警,阈值结合历史数据定,先观察两周再启用。
  5. 看板分层:红色 = 立即可用(请求量、错误率、耗时),黄色 = 需要时看(JVM、连接池),绿色 = 专项排查(GC 明细、线程状态)。

可观测性建设是投资回报率最高的工程投入之一。当线上出问题时,一张曲线图往往比翻一小时日志更快定位根因。这套方案同样适用于单体应用——先把你最核心的服务接进来,再逐步推广到全链路。