1. 引言:让 Spring Boot 应用"可观测"
一个应用能跑起来,和能在生产环境里被放心地跑,中间隔着一整套可观测能力。线上 CPU 突然飙高、接口 P99 从 80ms 涨到 2s、数据库连接池被打满、某个下游服务开始超时——这些问题如果只能靠用户投诉来发现,那排查成本会高得离谱。
Spring Boot 生态给出的答案是两层组合:
- Spring Boot Actuator:提供开箱即用的运维端点(健康检查、指标、环境信息、日志级别动态调整等),是"应用对外的运维接口"。
- Micrometer:一个与厂商无关的指标门面(SLF4J 之于日志,Micrometer 之于指标),把应用内的度量数据统一抽象成
Counter、Gauge、Timer等原语,再通过不同的 registry 输出到 Prometheus、Datadog、InfluxDB 等后端。
本文按"端点 → 自定义健康检查 → 指标原语 → Prometheus 抓取 → Grafana 面板 → 日志与链路关联 → 告警规则"的顺序,搭一套可以直接落地的生产级监控体系。
技术栈版本约定:Spring Boot 3.2.x、Java 17、Micrometer 1.12.x、Prometheus 2.50+、Grafana 10.x、AlertManager 0.26+。
先看整体架构,理解各组件之间的数据流向:
flowchart TD
App["Spring Boot 应用"] --> Act["Actuator 端点"]
App --> Mic["Micrometer 指标门面"]
Mic --> Reg["MeterRegistry"]
Reg --> Prom["/actuator/prometheus"]
Act --> Prom
Prom --> Scrape["Prometheus 抓取"]
Scrape --> Graf["Grafana 可视化"]
Scrape --> Alert["AlertManager 告警"]
App --> Log["日志 + traceId"]
Log --> Agg["Loki / ELK"]
Graf --> Agg
Alert --> Notify["PagerDuty / Slack / 邮件"]
2. Actuator 端点体系:用途与安全配置
2.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2.2 核心端点一览
| 端点 | 用途 | 生产建议 |
|---|---|---|
/actuator/health | 聚合健康状态,供 K8s 探针、负载均衡使用 | 对外暴露,但只返回 status |
/actuator/metrics | 查看单个指标的原始数据(调试用) | 仅内网暴露 |
/actuator/env | 查看配置项与环境变量 | 默认关闭,含敏感信息 |
/actuator/loggers | 查询与动态修改日志级别 | 仅内网 + 鉴权 |
/actuator/prometheus | Prometheus 格式的指标输出 | 仅监控网段可访问 |
/actuator/threaddump、/actuator/heapdump | 线程/堆转储 | 仅内网,慎开 |
2.3 配置示例
management:
endpoints:
web:
base-path: /actuator
exposure:
include: health,info,metrics,prometheus,loggers
exclude: env,beans,configprops,heapdump,threaddump
endpoint:
health:
show-details: when-authorized # 未授权时只返回 UP/DOWN
probes:
enabled: true # 开启 liveness / readiness 探针
loggers:
enabled: true
server:
port: 9090 # 管理端口与业务端口分离
关键实践:管理端口与业务端口分离。 把 Actuator 放到独立端口(如 9090),在 K8s 里只让监控系统访问该端口,业务端口完全不暴露运维接口,这是最省事也最有效的隔离手段。
端口分离后的访问路径如下:
flowchart LR
User["外部用户 / 业务流量"] --> Biz["业务端口 8080"]
Monitor["Prometheus / K8s 探针"] --> Mgmt["管理端口 9090"]
Biz --> App["Spring Boot 应用"]
Mgmt --> App
Mgmt -.->|"仅监控网段可达"| Sec["鉴权 + IP 白名单"]
2.4 安全加固
如果管理端口无法分离,至少要做鉴权:
@Configuration
public class ActuatorSecurityConfig {
@Bean
public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
.anyRequest().hasRole("MONITOR"))
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
再配合 Nginx / 网关做 IP 白名单,形成"网络层 + 应用层"双重防护。
3. 自定义健康检查指示器(HealthIndicator)
内置的 DataSourceHealthIndicator、RedisHealthIndicator 只能覆盖基础设施。业务层面的"健康"需要自己定义,比如:核心依赖的第三方接口是否可达、本地缓存是否预热完成、消息队列消费者是否存活。
3.1 实现 HealthIndicator
@Component("paymentGateway")
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentClient paymentClient;
public PaymentGatewayHealthIndicator(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
@Override
public Health health() {
try {
long start = System.nanoTime();
boolean ok = paymentClient.ping();
long costMs = (System.nanoTime() - start) / 1_000_000;
if (!ok) {
return Health.down()
.withDetail("reason", "ping returned false")
.build();
}
return Health.up()
.withDetail("latencyMs", costMs)
.withDetail("endpoint", paymentClient.endpoint())
.build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
访问 /actuator/health 时会看到:
{
"status": "UP",
"components": {
"paymentGateway": { "status": "UP", "details": { "latencyMs": 23 } },
"db": { "status": "UP" },
"diskSpace": { "status": "UP" }
}
}
3.2 用 HealthContributor 做分组
当健康检查项变多时,需要区分"影响流量的"和"仅告警的"。用 HealthContributor 分组,再配合 K8s 探针:
@Configuration
public class HealthGroupConfig {
@Bean
public HealthContributor readinessContributor(PaymentGatewayHealthIndicator payment,
DataSourceHealthIndicator db) {
Map<String, HealthContributor> map = new LinkedHashMap<>();
map.put("paymentGateway", payment);
map.put("db", db);
return CompositeHealthContributor.fromMap(map);
}
}
management:
endpoint:
health:
group:
readiness:
include: readinessContributor,ping
liveness:
include: ping
注意:liveness 只应包含"进程自身是否卡死"的判断,绝不能把数据库、下游服务放进去——否则下游抖动会导致 K8s 重启你的 Pod,引发雪崩。
健康检查分组与 K8s 探针的配合关系:
flowchart TD
K8s["Kubernetes"] --> Live["liveness 探针"]
K8s --> Ready["readiness 探针"]
Live --> LGroup["liveness 组:仅 ping"]
Ready --> RGroup["readiness 组:paymentGateway + db + ping"]
LGroup --> Decision1{"进程是否卡死"}
RGroup --> Decision2{"依赖是否可用"}
Decision1 -->|"否"| Restart["重启 Pod"]
Decision1 -->|"是"| Keep["保持运行"]
Decision2 -->|"是"| Traffic["接入流量"]
Decision2 -->|"否"| Remove["摘除流量"]
4. Micrometer 指标抽象层:四大原语
Micrometer 的核心价值是"写一次,输出到任意后端"。四个最常用的原语:
| 原语 | 语义 | 典型场景 |
|---|---|---|
Counter | 单调递增计数 | 请求数、错误数、消息消费条数 |
Gauge | 瞬时值快照 | 队列长度、连接池活跃数、缓存条目数 |
Timer | 耗时分布 + 计数 | 接口响应时间、外部调用耗时 |
DistributionSummary | 数值分布(非时间) | 请求体大小、批量处理条数 |
四类原语的选型可以按下面的思路判断:
flowchart TD
Start["需要记录一个度量"] --> Q1{"是否只增不减?"}
Q1 -->|"是"| Counter["Counter:累计计数"]
Q1 -->|"否"| Q2{"是否表示耗时?"}
Q2 -->|"是"| Timer["Timer:耗时分布 + 计数"]
Q2 -->|"否"| Q3{"是否需要观察瞬时值?"}
Q3 -->|"是"| Gauge["Gauge:瞬时快照"]
Q3 -->|"否"| Summary["DistributionSummary:数值分布"]
4.1 注入 MeterRegistry
@Service
public class OrderMetrics {
private final Counter orderCreated;
private final Counter orderFailed;
private final Timer orderProcessTimer;
private final DistributionSummary payloadSize;
public OrderMetrics(MeterRegistry registry) {
this.orderCreated = Counter.builder("order.created.total")
.description("成功创建的订单数")
.tag("channel", "app")
.register(registry);
this.orderFailed = Counter.builder("order.failed.total")
.description("创建失败的订单数")
.register(registry);
this.orderProcessTimer = Timer.builder("order.process.duration")
.description("订单处理耗时")
.publishPercentiles(0.5, 0.95, 0.99)
.publishPercentileHistogram()
.register(registry);
this.payloadSize = DistributionSummary.builder("order.payload.bytes")
.description("订单请求体大小")
.baseUnit("bytes")
.register(registry);
}
public void recordSuccess(int bytes) {
orderCreated.increment();
payloadSize.record(bytes);
}
public void recordFailure() {
orderFailed.increment();
}
public <T> T time(Supplier<T> supplier) {
return orderProcessTimer.record(supplier);
}
}
4.2 Gauge 的正确写法
Gauge 最容易踩坑:它持有的是对象的弱引用,如果被观测对象被 GC 回收,指标会变成 NaN。正确做法是绑定一个长期存活的对象:
@Component
public class QueueMetrics {
private final BlockingQueue<Task> taskQueue;
public QueueMetrics(MeterRegistry registry, BlockingQueue<Task> taskQueue) {
this.taskQueue = taskQueue;
Gauge.builder("task.queue.size", taskQueue, BlockingQueue::size)
.description("待处理任务队列长度")
.register(registry);
}
}
4.3 用注解简化埋点
@Timed(value = "api.order.create", percentiles = {0.5, 0.95, 0.99})
@PostMapping("/orders")
public OrderVO create(@RequestBody OrderDTO dto) {
// ...
}
需要开启 @Timed 的 AOP 支持:
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
4.4 标签(Tag)设计原则
标签是 Prometheus 的维度,但基数(cardinality)是杀手。以下写法必须避免:
// ❌ 错误:userId 有百万级取值,会撑爆 Prometheus
Counter.builder("request.total").tag("userId", userId).register(registry);
// ✅ 正确:只使用有限枚举值的维度
Counter.builder("request.total")
.tag("uri", "/api/orders")
.tag("status", "200")
.register(registry);
经验法则:单个指标的标签组合数控制在 1000 以内,任何可能无限增长的字段(用户 ID、订单号、时间戳、异常堆栈)都不能作为标签。
5. Prometheus 集成:端点暴露与抓取
5.1 引入 registry
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
引入后 /actuator/prometheus 自动可用,输出形如:
# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{method="GET",status="200",uri="/api/orders"} 1523.0
http_server_requests_seconds_sum{method="GET",status="200",uri="/api/orders"} 87.42
5.2 通用标签
management:
metrics:
tags:
application: ${spring.application.name}
env: ${ENV:prod}
instance: ${HOSTNAME:unknown}
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 100ms,300ms,1s
application 标签是 Grafana 面板按应用筛选的基础,务必配置。
5.3 Prometheus 抓取配置
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets:
- '10.0.1.11:9090'
- '10.0.1.12:9090'
labels:
env: 'prod'
app: 'order-service'
K8s 环境下用服务发现更合适:
- job_name: 'spring-boot-k8s'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: (.+)
replacement: ${1}:9090
5.4 验证抓取
在 Prometheus UI 的 Status → Targets 页面确认目标为 UP,然后在 Graph 页执行 http_server_requests_seconds_count{application="order-service"} 验证数据。
完整的指标采集链路如下:
sequenceDiagram
participant App as "Spring Boot 应用"
participant Act as "Actuator /prometheus"
participant Prom as "Prometheus"
participant Graf as "Grafana"
Prom->>Act: "每 15s 发起 scrape"
Act->>App: "读取 MeterRegistry 指标"
App-->>Act: "返回文本格式指标"
Act-->>Prom: "200 OK + metrics"
Prom->>Prom: "写入 TSDB"
Graf->>Prom: "PromQL 查询"
Prom-->>Graf: "返回时间序列"
6. Grafana 面板搭建
6.1 数据源与变量
添加 Prometheus 数据源后,在 Dashboard 设置中定义变量:
变量名:application
类型:Query
查询:label_values(http_server_requests_seconds_count, application)
这样一套面板可以复用到所有服务。
6.2 JVM 指标面板
| 面板 | PromQL |
|---|---|
| 堆内存使用 | sum(jvm_memory_used_bytes{area="heap",application="$application"}) by (instance) |
| 非堆内存 | sum(jvm_memory_used_bytes{area="nonheap",application="$application"}) by (instance) |
| GC 暂停时间(每秒) | rate(jvm_gc_pause_seconds_sum{application="$application"}[1m]) |
| 线程数 | jvm_threads_live_threads{application="$application"} |
| 类加载数 | jvm_classes_loaded_classes{application="$application"} |
6.3 HTTP 请求指标面板
# QPS(按 URI)
sum(rate(http_server_requests_seconds_count{application="$application"}[1m])) by (uri)
# P99 响应时间
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket{application="$application"}[5m])) by (le, uri))
# 错误率
sum(rate(http_server_requests_seconds_count{application="$application",status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{application="$application"}[5m]))
6.4 数据库连接池面板
HikariCP 的指标由 Micrometer 自动绑定,前缀为 hikaricp_:
# 活跃连接数
hikaricp_connections_active{application="$application"}
# 等待获取连接的线程数(>0 说明池子不够)
hikaricp_connections_pending{application="$application"}
# 连接获取耗时 P99
histogram_quantile(0.99,
sum(rate(hikaricp_connections_acquire_seconds_bucket{application="$application"}[5m])) by (le))
hikaricp_connections_pending 持续大于 0 是最典型的"连接池打满"信号,应直接配置告警。
6.5 面板布局建议
按"总览 → 资源 → 业务"三层组织:
- 第一行:QPS、错误率、P99 延迟、实例数(四个 Stat 面板,一眼看全局)
- 第二行:JVM 堆内存、GC 频率、线程数
- 第三行:HTTP 各 URI 的延迟热力图、状态码分布
- 第四行:连接池、缓存命中率、消息队列积压
7. 日志与链路追踪的关联:MDC + TraceId
指标告诉你"出问题了",日志告诉你"问题在哪"。两者之间需要一个共同的关联键——traceId。
7.1 引入链路追踪
<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: 0.1 # 生产环境采样 10%
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
7.2 把 traceId 写入 MDC
Micrometer Tracing 会自动把 traceId、spanId 放进 MDC,只需在日志格式中引用:
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n"
输出效果:
2026-11-01 10:41:41.203 [http-nio-8080-exec-3] INFO [a3f9c2b1e8d74f60,9b2c1d4e] c.e.order.OrderService - 订单创建成功 orderId=10086
7.3 异步线程的 MDC 传递
@Async、线程池、CompletableFuture 会丢失 MDC 上下文,需要显式包装:
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
if (context != null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
}
7.4 日志聚合侧关联
在 Loki / ELK 中,用 traceId 作为检索键:
{app="order-service"} |= "a3f9c2b1e8d74f60"
Grafana 中还可以配置 Exemplar,让指标图上的点直接跳转到对应的 trace,实现"指标 → 链路 → 日志"的三级下钻。
8. 告警规则设计:Prometheus + AlertManager
8.1 告警规则文件
# rules/spring-boot-alerts.yml
groups:
- name: spring-boot-app
interval: 30s
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
/
sum(rate(http_server_requests_seconds_count[5m])) by (application)
> 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.application }} 5xx 错误率超过 5%"
description: "当前错误率 {{ $value | humanizePercentage }},持续 5 分钟"
- alert: HighLatencyP99
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket[5m])) by (le, application)
) > 1
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.application }} P99 延迟超过 1s"
- alert: ConnectionPoolExhausted
expr: hikaricp_connections_pending > 0
for: 3m
labels:
severity: critical
annotations:
summary: "{{ $labels.application }} 连接池出现等待"
- alert: JvmHeapHigh
expr: |
sum(jvm_memory_used_bytes{area="heap"}) by (application, instance)
/
sum(jvm_memory_max_bytes{area="heap"}) by (application, instance)
> 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} 堆内存使用率超过 90%"
- alert: InstanceDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }} 抓取失败"
8.2 AlertManager 路由与抑制
route:
receiver: 'default'
group_by: ['alertname', 'application']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: ['severity="critical"']
receiver: 'pagerduty'
continue: true
- matchers: ['severity="warning"']
receiver: 'slack'
inhibit_rules:
# 实例宕机时,抑制该实例上的其他告警,避免告警风暴
- source_matchers: ['alertname="InstanceDown"']
target_matchers: ['severity=~"warning|critical"']
equal: ['instance']
8.3 告警设计的三条原则
- 每个告警都必须可行动。如果收到告警后不知道该做什么,这条规则就该删掉。
- 用
for抑制抖动。瞬时尖刺不告警,持续 N 分钟才告警。 - 分级 + 抑制。critical 走电话/值班,warning 走 IM;上游故障时抑制下游衍生告警。
9. 总结
把上面的内容串起来,一套完整的生产级监控体系是这样的:
flowchart LR
A["Spring Boot 应用"] --> B["Actuator 端点"]
A --> C["Micrometer 指标"]
C --> D["/actuator/prometheus"]
B --> D
D --> E["Prometheus 抓取"]
E --> F["Grafana 面板"]
E --> G["AlertManager 告警"]
A --> H["日志 + TraceId"]
H --> I["Loki / ELK"]
F --> I
G --> J["PagerDuty / Slack"]
落地清单:
- Actuator 管理端口与业务端口分离,
env/heapdump等敏感端点关闭 - 核心业务依赖实现
HealthIndicator,liveness 只含进程自身检查 - 指标标签基数受控,禁止把 ID 类字段作为 tag
- Prometheus 抓取配置
application标签,Grafana 用变量复用面板 - 日志格式包含
traceId,异步线程池配置TaskDecorator - 告警规则带
for持续时间,配置抑制规则防告警风暴
监控体系不是一次搭完就结束的工程,它需要随着业务演进持续调整阈值、补充指标、清理无效告警。先让"能看见"成为常态,再逐步追求"看得准"。