一、痛点:没有指标的系统,排查靠「感觉」
很多 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"}
七、最佳实践清单
- 先盘点框架自带指标,再决定自定义埋点,避免重复造轮子。
- 核心业务链路(下单、支付、消息消费)必须有 Counter/Timer 埋点,这是容量评估的依据。
- 指标命名遵循 domain.action.result 风格,单位写进 description。
- 告警规则宁少勿滥:只对「影响用户或无法自愈」的指标告警,阈值结合历史数据定,先观察两周再启用。
- 看板分层:红色 = 立即可用(请求量、错误率、耗时),黄色 = 需要时看(JVM、连接池),绿色 = 专项排查(GC 明细、线程状态)。
可观测性建设是投资回报率最高的工程投入之一。当线上出问题时,一张曲线图往往比翻一小时日志更快定位根因。这套方案同样适用于单体应用——先把你最核心的服务接进来,再逐步推广到全链路。