Spring Boot Actuator 生产级监控实战——让你的服务变得透明可观测
Spring Boot Actuator 生产级监控实战——让你的服务变得透明可观测
适读人群:需要对 Spring Boot 服务做生产级监控的工程师 | 阅读时长:约17分钟 | 核心价值:掌握 Actuator 核心端点配置、自定义健康指标、Prometheus 指标接入的完整实战方案
一、凌晨两点的故障告警
去年某次线上事故让我印象深刻。凌晨两点,某个服务的请求成功率从 99.8% 掉到 60%,告警响了,但运维登上去发现:服务进程还在,接口还在响应,JVM 堆内存看起来正常,日志里看不到明显异常。
折腾了将近一个小时,才排查出根本原因:线程池队列打满了,大量请求在队列里等待超时,返回了超时错误,但进程本身是健康的。
如果当时有细粒度的线程池监控指标——队列水位、活跃线程数、拒绝任务数——报警就能直接指向问题,排查时间从 1 小时变 5 分钟。
这件事之后,我在所有项目里把 Actuator + Prometheus + Grafana 这套监控方案推成了标准配置。这篇文章把这套方案完整讲清楚。
二、Actuator 基础配置
2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Prometheus 指标导出 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>2.2 核心配置
management:
# 指定管理端口(与业务端口分离,更安全)
server:
port: 8081
endpoints:
web:
# 暴露的端点列表(生产环境不要暴露 heapdump、threaddump 等敏感端点)
exposure:
include: health,info,metrics,prometheus,loggers,env
base-path: /actuator
endpoint:
health:
# 认证用户可以看详细健康信息
show-details: when_authorized
# 健康检查包含的组件
show-components: always
# Prometheus 指标配置
metrics:
export:
prometheus:
enabled: true
tags:
# 给所有指标加应用名标签,Grafana 里按服务名过滤
application: ${spring.application.name}
distribution:
# HTTP 请求延迟分位数(p50, p95, p99)
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5, 0.95, 0.99
info:
env:
enabled: true
java:
enabled: true
build:
enabled: true三、自定义健康检查指标
Actuator 内置了 DB、Redis、消息队列等健康检查,但业务层面的健康状态需要自定义。
场景:我们的服务依赖一个外部支付网关,需要把支付网关的连通性纳入健康检查。
package com.example.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.time.Instant;
/**
* 支付网关健康检查指标。
* 当 /actuator/health 被调用时,Spring Boot 会自动调用此 HealthIndicator。
* 如果任一 HealthIndicator 返回 DOWN,整体 health 状态变为 DOWN,
* 配合 Kubernetes liveness probe 可以触发容器重启。
*/
@Component("paymentGateway") // Bean 名称决定了健康检查项目名
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
public PaymentGatewayHealthIndicator(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public Health health() {
Instant start = Instant.now();
try {
// 调用支付网关的心跳接口
String response = restTemplate.getForObject(
"https://payment-gateway/ping", String.class);
long latencyMs = Duration.between(start, Instant.now()).toMillis();
if ("pong".equals(response)) {
return Health.up()
.withDetail("latencyMs", latencyMs)
.withDetail("status", "reachable")
.build();
} else {
return Health.down()
.withDetail("reason", "unexpected response: " + response)
.withDetail("latencyMs", latencyMs)
.build();
}
} catch (Exception e) {
long latencyMs = Duration.between(start, Instant.now()).toMillis();
return Health.down()
.withDetail("reason", e.getMessage())
.withDetail("latencyMs", latencyMs)
.withException(e)
.build();
}
}
}四、自定义业务指标(Micrometer)
内置的 HTTP 请求、JVM 等指标能满足基础监控需求,但业务指标需要手动埋点。
package com.example.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 订单服务,演示如何在业务代码里埋入 Micrometer 指标。
*/
@Service
public class OrderMetricsService {
// 订单创建计数器
private final Counter orderCreatedCounter;
// 订单创建失败计数器
private final Counter orderFailedCounter;
// 订单处理耗时 Timer
private final Timer orderProcessTimer;
// 当前待处理订单数(Gauge,用于监控积压)
private final AtomicInteger pendingOrderCount = new AtomicInteger(0);
public OrderMetricsService(MeterRegistry registry) {
// 注册 Counter:订单创建次数,按状态分 tag
this.orderCreatedCounter = Counter.builder("order.created")
.description("订单创建总次数")
.tag("type", "normal") // 可以加多个 tag 做多维统计
.register(registry);
this.orderFailedCounter = Counter.builder("order.failed")
.description("订单创建失败次数")
.register(registry);
// 注册 Timer:记录订单处理耗时分布
this.orderProcessTimer = Timer.builder("order.process.duration")
.description("订单处理耗时(毫秒)")
.publishPercentiles(0.5, 0.95, 0.99) // 输出 p50/p95/p99
.register(registry);
// 注册 Gauge:实时反映待处理订单积压数
Gauge.builder("order.pending.count", pendingOrderCount, AtomicInteger::get)
.description("当前待处理订单数")
.register(registry);
}
/**
* 创建订单,同时上报业务指标。
*/
public Long createOrder(OrderRequest request) {
return orderProcessTimer.record(() -> {
pendingOrderCount.incrementAndGet();
try {
// 模拟订单创建业务逻辑
Long orderId = doCreateOrder(request);
orderCreatedCounter.increment();
return orderId;
} catch (Exception e) {
orderFailedCounter.increment();
throw e;
} finally {
pendingOrderCount.decrementAndGet();
}
});
}
private Long doCreateOrder(OrderRequest request) {
// 实际业务逻辑
return System.currentTimeMillis();
}
// 占位类
public static class OrderRequest {}
}五、线程池监控指标接入
回到文章开头的事故场景——如何监控线程池?
package com.example.config;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class MonitoredThreadPoolConfig {
@Bean("monitoredAsyncExecutor")
public Executor monitoredAsyncExecutor(MeterRegistry registry) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("monitored-async-");
executor.initialize();
// 将线程池注册到 Micrometer,自动上报以下指标:
// executor.pool.size(线程池大小)
// executor.active(活跃线程数)
// executor.queued(队列中的任务数)
// executor.completed(已完成任务数)
ExecutorServiceMetrics.monitor(
registry,
executor.getThreadPoolExecutor(),
"async-default", // 指标名前缀
"async" // tag
);
return executor;
}
}注册之后,Prometheus 就能采集到 executor.queued 指标,当队列水位超过 70% 时触发告警,这正是文章开头那次故障中缺失的关键监控。
六、踩坑实录
坑1:health 端点暴露了敏感配置信息
现象:/actuator/env 接口可以看到数据库密码、Redis 密码等配置项的明文值。
原因:Actuator 端点没有做访问控制,任何人都能访问。
解法:这个坑非常严重,这个坑我在一个早期项目里真的看到过。解法有两个:一是把 management.server.port 设为独立端口,只对内网暴露;二是用 Spring Security 对 actuator 端点加认证。生产环境必须两个都做。
坑2:/actuator/health 响应慢导致 K8s 误判为不健康
现象:服务明明正常,K8s 频繁重启 Pod,日志显示 liveness probe 超时。
原因:健康检查里包含了一个对外部服务的 HTTP 调用,外部服务偶尔响应慢(超过 2 秒),健康检查超时,K8s 判定 unhealthy 并重启。
解法:将健康检查分为两个组:liveness(是否存活,只检查 JVM 和内部状态)和 readiness(是否就绪,检查外部依赖)。K8s liveness probe 只调 /actuator/health/liveness,readiness probe 调 /actuator/health/readiness。
management:
endpoint:
health:
group:
liveness:
include: ping # 只检查 JVM 存活
readiness:
include: ping,db,redis,paymentGateway # 检查所有依赖坑3:Prometheus 指标采集到大量高基数 tag 导致内存爆涨
现象:Prometheus server 内存从 2GB 涨到 10GB,最终 OOM。
原因:某个指标用了 userId 作为 tag 值,用户有数百万个,导致时序数量爆炸(高基数问题)。Prometheus 的存储模型里,每个唯一的 tag 组合都是一条时序,百万 userId 就是百万条时序。
解法:绝对不能把用户 ID、订单 ID 等高基数值作为 tag。只用低基数的分类信息做 tag,比如:状态(SUCCESS/FAIL)、业务类型(ORDER/REFUND)、接口路径(/api/v1/order)。
七、Grafana Dashboard 建议
集成 Prometheus + Grafana 之后,我建议每个服务至少配置以下面板:
- 服务基础健康:JVM 堆内存、GC 次数/时间、线程数
- HTTP 接口指标:请求量、成功率、p95/p99 延迟(按接口分 tag)
- 线程池监控:队列水位、活跃线程、拒绝任务数
- 数据库连接池:活跃连接数、等待连接数、连接创建时间
- 自定义业务指标:订单量、支付成功率等核心业务数据
这五类指标覆盖了 80% 的故障场景,能让你在问题发生的第一分钟就定位到方向。
