微服务监控体系:Micrometer+Prometheus+Grafana的完整搭建
微服务监控体系:Micrometer+Prometheus+Grafana的完整搭建
适读人群:有微服务实战经验的后端工程师 | 阅读时长:约26分钟 | Spring Boot 3.2 / Prometheus 2.50
开篇故事
某次大促前夕,凌晨两点我接到电话,说核心服务响应变慢了。我登上服务器,翻日志,翻GC日志,凭感觉定位,折腾了一个小时才发现是某个数据库连接池耗尽了。
那一刻我深刻感受到:没有监控体系,出了问题只能"盲猜",排查效率极低,心理压力极大。
第二天我就开始搭建监控体系。Micrometer采集指标,Prometheus拉取并存储,Grafana展示,AlertManager发告警。搭起来之后,有一次服务响应时间P99开始上升,AlertManager在2分钟内给我发了告警,我打开Grafana一眼就看到是连接池的活跃连接数在涨,5分钟就定位到了原因——是一个慢查询没走索引。
有监控和没监控,处理问题的效率差了何止10倍。今天把这套监控体系从头到尾完整讲一遍。
一、核心问题分析
Spring Boot微服务的监控体系需要覆盖四个维度:
JVM指标:堆内存使用率、GC频率和耗时、线程数。JVM指标异常通常是GC问题或内存泄漏的前兆。
应用层指标:HTTP接口的请求量(QPS)、响应时间(P50/P95/P99)、错误率。这是最直接反映服务健康状况的指标。
中间件指标:数据库连接池状态(活跃连接数、等待数)、Redis连接数、Kafka消费Lag。中间件指标往往是性能问题的根因。
业务指标:下单成功率、支付转化率等业务关键指标。这些指标需要在代码里埋点。
Micrometer是Spring Boot默认的指标框架,通过Actuator的/actuator/prometheus端点暴露Prometheus格式的指标,Prometheus定时拉取,Grafana读取Prometheus数据并展示。
二、原理深度解析
2.1 监控数据采集链路
2.2 Histogram指标(响应时间分布)
2.3 Prometheus拉取架构
三、完整代码实现
3.1 项目依赖
<dependencies>
<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>
<!-- AOP支持(@Timed注解需要) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>3.2 Actuator配置
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
show-details: when-authorized
probes:
enabled: true
metrics:
export:
prometheus:
enabled: true
# 给所有指标添加公共标签
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active:unknown}
version: ${app.version:unknown}
# 配置histogram百分位(P50, P95, P99)
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5, 0.95, 0.99
slo:
http.server.requests: 100ms, 500ms, 1s, 2s # SLO阈值
server:
port: 8081 # 把Actuator放在独立端口,与业务端口分离3.3 自定义业务指标
package com.laozhang.monitor.metrics;
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.binder.MeterBinder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
/**
* 业务指标注册器
* 实现MeterBinder,在应用启动时自动注册所有业务指标
*/
@Slf4j
@Component
public class BusinessMetricsBinder implements MeterBinder {
private final AtomicInteger pendingOrders = new AtomicInteger(0);
private final AtomicLong totalOrderAmount = new AtomicLong(0);
@Override
public void bindTo(MeterRegistry registry) {
// 1. Gauge(当前值):当前待处理订单数
Gauge.builder("order.pending.count", pendingOrders, AtomicInteger::get)
.description("待处理订单数量")
.tag("type", "pending")
.register(registry);
// 2. Counter(累计值):订单创建总数
Counter orderCreateCounter = Counter.builder("order.created.total")
.description("订单创建总数")
.register(registry);
// 3. Timer(耗时分布):订单处理时间
Timer orderProcessTimer = Timer.builder("order.process.duration")
.description("订单处理耗时")
.publishPercentiles(0.5, 0.95, 0.99)
.publishPercentileHistogram()
.sla(
java.time.Duration.ofMillis(100),
java.time.Duration.ofMillis(500),
java.time.Duration.ofSeconds(1)
)
.register(registry);
// 4. DistributionSummary(值分布):订单金额分布
DistributionSummary orderAmountSummary = DistributionSummary.builder("order.amount")
.description("订单金额分布(元)")
.baseUnit("yuan")
.publishPercentiles(0.5, 0.9, 0.99)
.register(registry);
log.info("业务指标注册完成");
}
// 提供给业务代码调用的方法
public void incrementPendingOrders() { pendingOrders.incrementAndGet(); }
public void decrementPendingOrders() { pendingOrders.decrementAndGet(); }
}package com.laozhang.monitor.service;
import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderMetricsService {
private final MeterRegistry meterRegistry;
public OrderMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
/**
* 使用@Timed注解自动记录方法执行时间
* 需要启用TimedAspect Bean
*/
@Timed(
value = "order.create.duration",
description = "创建订单耗时",
percentiles = {0.5, 0.95, 0.99},
histogram = true
)
public OrderResult createOrder(CreateOrderRequest request) {
// 业务逻辑
OrderResult result = doCreateOrder(request);
// 记录成功/失败指标
Counter.builder("order.create.result")
.tag("status", result.isSuccess() ? "success" : "failure")
.tag("channel", request.getChannel())
.register(meterRegistry)
.increment();
// 记录订单金额
meterRegistry.summary("order.amount",
"channel", request.getChannel()
).record(request.getAmount().doubleValue());
return result;
}
private OrderResult doCreateOrder(CreateOrderRequest request) {
return new OrderResult();
}
}package com.laozhang.monitor.config;
import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MicrometerConfig {
/**
* 开启@Timed注解支持
*/
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}3.4 数据库连接池监控
HikariCP连接池的指标Micrometer会自动采集,但需要确保配置了名字:
spring:
datasource:
hikari:
# 连接池名称,用于区分不同数据源的指标
pool-name: order-db-pool
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000HikariCP会自动暴露以下指标:
hikaricp.connections.active:活跃连接数hikaricp.connections.idle:空闲连接数hikaricp.connections.pending:等待获取连接的线程数hikaricp.connections.timeout:获取连接超时次数
3.5 Prometheus配置
# prometheus.yml
global:
scrape_interval: 15s # 拉取间隔
evaluation_interval: 15s # 规则评估间隔
scrape_configs:
# Spring Boot应用(K8s环境)
- job_name: 'spring-boot-apps'
kubernetes_sd_configs:
- role: pod
namespaces:
names:
- production
relabel_configs:
# 只抓取有prometheus.io/scrape: "true" annotation的Pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: 'true'
# 使用Pod annotation里的path
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
# 使用Actuator端口(8081)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
# 添加应用名label
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: application3.6 告警规则配置
# alert_rules.yml
groups:
- name: spring-boot-alerts
rules:
# HTTP错误率告警
- 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: 2m
labels:
severity: critical
annotations:
summary: "{{ $labels.application }} 错误率过高"
description: "应用 {{ $labels.application }} 5分钟错误率为 {{ $value | humanizePercentage }}"
# P99响应时间告警
- alert: HighP99Latency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket[5m])) by (application, le)
) > 2
for: 3m
labels:
severity: warning
annotations:
summary: "{{ $labels.application }} P99响应时间过高"
description: "P99 = {{ $value }}s,超过2秒阈值"
# 连接池耗尽告警
- alert: DbConnectionPoolExhausted
expr: |
hikaricp_connections_pending{application!=""} > 5
for: 1m
labels:
severity: critical
annotations:
summary: "{{ $labels.application }} 数据库连接池等待队列过长"
description: "等待连接的线程数 = {{ $value }}"
# JVM堆内存告警
- alert: JvmHeapMemoryHigh
expr: |
jvm_memory_used_bytes{area="heap"}
/
jvm_memory_max_bytes{area="heap"}
> 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.application }} JVM堆内存使用率过高"
description: "堆内存使用率 {{ $value | humanizePercentage }}"3.7 Grafana Dashboard配置(关键面板)
// JVM总览面板配置片段
{
"title": "JVM Overview",
"panels": [
{
"title": "堆内存使用率",
"type": "gauge",
"expr": "jvm_memory_used_bytes{area='heap',application='$application'} / jvm_memory_max_bytes{area='heap',application='$application'} * 100",
"thresholds": [
{"value": 70, "color": "yellow"},
{"value": 85, "color": "red"}
]
},
{
"title": "HTTP请求P99延迟",
"type": "graph",
"expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{application='$application'}[5m])) by (le))"
},
{
"title": "DB连接池状态",
"type": "graph",
"exprs": [
"hikaricp_connections_active{application='$application'}",
"hikaricp_connections_idle{application='$application'}",
"hikaricp_connections_pending{application='$application'}"
]
}
]
}四、生产配置与调优
4.1 Prometheus存储优化
# Prometheus存储配置
storage:
tsdb:
retention.time: 15d # 数据保留15天
retention.size: 50GB # 最大存储50GB
# 对于长期存储,接入Thanos或VictoriaMetrics4.2 指标采样率控制
management:
metrics:
distribution:
# 只对慢请求(>100ms)记录histogram,减少存储压力
percentiles-histogram:
http.server.requests: false # 全量histogram太大
# 只记录percentiles,不记录histogram桶
percentiles:
http.server.requests: 0.5, 0.95, 0.99五、踩坑实录
坑一:/actuator/prometheus端口暴露在公网,被恶意扫描刷取指标。
actuator端口没有做鉴权,被暴露到公网,每秒有大量扫描请求来拉取/actuator/prometheus,消耗了不少CPU。解决方案是把Actuator单独放在内部端口(8081),K8s的NetworkPolicy只允许Prometheus服务访问这个端口,不对外暴露。
坑二:指标太多导致Prometheus存储爆炸,磁盘用完。
有个同事在每个接口的每个参数上都打了Tag,比如tag("orderId", orderId),这导致Prometheus的时间序列数量爆炸(每个不同的orderId都是一条独立的时间序列)。Prometheus存储了几千万条时间序列,磁盘很快就满了。
规则:Tag的取值必须是有限的、低基数的(比如状态码、服务名、方法名),绝对不能用高基数的值做Tag(用户ID、订单ID、TraceId等)。
坑三:@Timed注解加在private方法上,没有生效。
@Timed是通过AOP实现的,AOP只能拦截Spring管理的Bean的public方法。私有方法、final方法、非Spring Bean的方法上的@Timed都不会生效,也不报错,非常难发现。
改成代码埋点:Timer.Sample sample = Timer.start(registry); ... sample.stop(timer);,或者把方法改成public。
坑四:Grafana Dashboard没有分环境,测试环境的指标污染了生产Dashboard。
使用$environment变量在Dashboard层面过滤,确保生产Dashboard只展示生产环境的指标。给所有指标打上environment标签是前提。
六、总结
监控体系的核心是:Micrometer埋点(自动+手动),Actuator暴露指标,Prometheus拉取存储,Grafana可视化,AlertManager告警。关键注意点:Tag不能是高基数值(避免时间序列爆炸),Actuator端口不能对公网暴露,@Timed只对public方法有效,连接池名称要配置好才能区分多数据源。
有了完整的监控体系,出了问题从"盲猜"变成"看图说话",排查效率提升10倍以上。
