很多团队在监控后端服务时,第一反应都是先看CPU、看内存,但Spring Boot应用真正需要关注的往往是JVM堆、GC停顿、接口延迟、连接池水位这些内部状态。SpringBoot集成普罗米修斯(Prometheus)这件事,本质是把应用内部这些状态通过统一标准暴露出来,Prometheus按间隔拉取,再由Grafana做可视化。以前线上出问题只能靠猜,或者等用户反馈了再去翻日志,接入之后,指标直接变成时序数据,趋势一目了然。这篇文章就来聊聊我在实际项目里从引入依赖、暴露指标、对接Prometheus,到自定义业务指标、踩坑排错的全过程。后端开发、SRE或者运维同学都可以直接参考,我会把配置、代码和为什么会这么做的底层逻辑一起讲清楚。
1. 整体设计:从应用指标到监控看板的链路到底怎么搭
1.1 核心链路拆解
先明确我们要实现什么。最基础的形态是这样一条数据流:
Spring Boot应用内部通过 Micrometer 收集指标,然后通过 Actuator 暴露一个 HTTP 端点/actuator/prometheus,Prometheus 服务器周期性地发起 HTTP 请求到这个端点,拉取到文本格式的指标数据并存入自己的时序数据库。之后你用 PromQL 查询这些数据,最终在 Grafana 上画成折线图、柱状图、仪表盘。
所以这里其实有三个角色:
- Spring Boot应用:被监控的对象,需要集成 Micrometer、Actuator。
- Prometheus服务器:负责定时抓取、存储、查询。
- Grafana:可选但强烈推荐,负责把数据变成能看的图。
关键在于“定时拉取”这个动作。Prometheus 用的是 pull 模型,不是 push 模型。也就是说,不是应用主动把数据报到监控中心,而是监控中心到应用这边来取。这样做的好处是:监控中心的配置变更不需要逐个应用通知;应用多了一个实例,只要配置好 target 就能自动抓取;如果应用挂了,Prometheus 抓不到数据,本身就是一个有效的故障信号。这一点在Kubernetes 环境里尤其好用,Pod 动态伸缩时,Prometheus 能自动感知新实例。
Micrometer 在这个链路里是一个抽象层。它定义了一套统一的指标 API,比如 Counter、Gauge、Timer、Summary,然后由 micrometer-registry-prometheus 把这些指标翻译成 Prometheus 使用的文本格式。如果没有 Micrometer,直接用 Prometheus 官方 Java Client 也是可以的,但是一旦将来想接其他监控系统,比如 InfluxDB、New Relic,就得改一版代码。用 Micrometer 之后,替换监控后端只是换一个依赖的事,业务代码完全不用动。
1.2 为什么是 Actuator 来暴露端点
Spring Boot Actuator 本身就是为“暴露应用运行时信息”设计的。它提供 health、info、metrics、beans、heapdump 等众多端点。我们只需要把 Prometheus 的 exporter 端点接进来,可以通过management.endpoints.web.exposure.include来控制哪些端点对外可见。通常出于安全考虑,不需要把所有端点都暴露到公网,但对于监控和运维来说,health、prometheus、metrics这三个端点几乎必开。
有一个容易被忽略的点:Actuator 的默认 base-path 是/actuator,所以最终指标端点默认是/actuator/prometheus。很多人配了management.endpoints.web.exposure.include=prometheus却忘了加 base-path 前缀,导致访问时多写少写路径都不对,这个后面会详细讲。
1.3 依赖选型和版本匹配
Spring Boot 2.x 和 3.x 在集成时,依赖坐标本身没有太大差别,还是这两个:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>但是注意 Spring Boot 3.0 开始要求 JDK 17,Micrometer 版本也升级到了 1.10 以上。如果项目还在用 Spring Boot 2.3 甚至更老,正好搭上了旧版 Micrometer,端点路径、命名字段可能有些差异,最好按你当前的 Boot 大版本找官方对应的文档。
我在一个项目里还遇到过依赖冲突:项目本身已经有micrometer-core,但版本太老,引入 registry-prometheus 之后类冲突。解决方式是先看依赖树mvn dependency:tree,统一 Micrometer 版本到 Boot 所管理的版本,不要自己单独指定。Spring Boot 的依赖管理已经把 Micrometer 各模块的版本锁好了,最简单的方式就是依赖 Boot BOM,别手动写 Micrometer 的版本号。
2. 从零到一:依赖引入、端点配置和首次验证
2.1 最小化依赖与启动配置
假设你使用的是 Maven,在pom.xml里加上上面两个依赖之后,修改application.yml:
management: endpoints: web: exposure: include: health,info,prometheus,metrics endpoint: health: show-details: always metrics: tags: application: ${spring.application.name}management.metrics.tags.application这行特别重要。它会往所有指标上强制打一个application="你的应用名"的标签。否则如果同一个 Prometheus 里抓了多个服务,你根本分不清哪个指标来自哪个应用。这个标签在压测、容量规划、告警分组时极其好用。
如果你的应用想单独用一个端口暴露监控信息,不跟业务端口混在一起,可以加上:
management: server: port: 9091这样/actuator/prometheus只在 9091 端口生效,业务仍然在 8080。对外防火墙也只需要放开 9091 给监控网络访问。这个做法很安全,我在生产环境默认就是这个配置。
2.2 启动应用并验证指标输出
应用启动起来之后,先看日志有没有报错,然后用 curl 验证:
curl http://localhost:8080/actuator/prometheus如果你给 management 配了单独端口,那就是:
curl http://localhost:9091/actuator/prometheus成功的返回是文本内容,开头差不多长这样:
# HELP jvm_memory_used_bytes The amount of used memory # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{area="heap",id="PS Eden Space",} 5.2345678E7这里有一个值得说清楚的点:prommetrics 文本格式中,# HELP和# TYPE不是多余的注释,Prometheus 服务端和 Grafana 都会依赖 TYPE 来区分指标类型,从而选择正确的查询函数。比如counter类型才适合用rate(),gauge就是瞬时值。你在看原始输出时别嫌弃这些“注释”,它们在语义上很重要。
2.3 在 Spring Boot 3.x 中的变化
Spring Boot 3 里 Actuator 默认端点跟 2.x 基本一致,但management.endpoints.web.exposure.include如果只写一个*,会暴露所有端点,包括 heapdump、logfile、shutdown 等,风险较大。我建议宁可多写几个端点,也别用通配符。另外 Spring Boot 3 默认的 Micrometer 命名规则与 Prometheus 的兼容性更好,比如 HTTP 指标名从http.server.requests变成了http_server_requests_seconds,这个命名改动在查询 PromQL 时需要留意。
还有一个容易被坑的点:Spring Boot 2.x 里只要引入micrometer-registry-prometheus,Prometheus 端点会自动注册;如果应用继承了 Spring Cloud Gateway 等特殊框架,会多一层 WebFlux/WebMVC 差异,导致指标端点被拦截。遇到这种情况,可以先看端点是否为 404,再根据框架类型调整配置。
3. 指标不能只靠“自带的”:默认指标和自定义业务指标
3.1 先看懂默认指标里都有什么
接入之后,Prometheus 默认抓到的指标数量通常有几百个。不要被吓到,大多数是 JVM、系统、Spring MVC 这三类。常见的有:
| 指标名称 | 类型 | 含义 |
|---|---|---|
jvm_memory_used_bytes | gauge | JVM 内存已使用量 |
jvm_gc_pause_seconds | summary | GC 停顿耗时 |
system_cpu_usage | gauge | 整个系统 CPU 使用率 |
process_cpu_usage | gauge | 当前进程 CPU 使用率 |
http_server_requests_seconds | summary/histogram | Spring MVC 请求耗时分布 |
hikaricp_connections_active | gauge | 数据库连接池活跃连接数 |
tomcat_threads_busy_threads | gauge | Tomcat 繁忙线程数 |
logback_events_total | counter | 按日志级别统计的日志条数 |
默认指标价值非常大,但前提是你知道怎么测。比如hikaricp_connections_active能直接告诉你连接池是否被打满;jvm_gc_pause_seconds的 p99 能从全局判断用户感知到的卡顿是否来自 GC。很多线上问题不是你主动写的业务指标暴露的,而是这些默认指标先出现异常。
3.2 自定义 Counter:订单数、错误数、点击量
默认指标再好,也不能完全覆盖业务。比如你在优化一个下单流程,想知道单位时间成功下单数和失败下单数,这只能靠自定义指标。Micrometer 的 Counter 是最简单的计数指标,单调递增,适合“次数”类的统计。
先注入MeterRegistry:
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Service; @Service public class OrderMetricService { private final Counter orderSuccessCounter; private final Counter orderFailCounter; public OrderMetricService(MeterRegistry registry) { this.orderSuccessCounter = registry.counter("order.success.total"); this.orderFailCounter = registry.counter("order.fail.total"); } public void markSuccess() { orderSuccessCounter.increment(); } public void markFail() { orderFailCounter.increment(); } }这里我把指标名写成了order.success.total。Micrometer 在转发给 Prometheus 时会自动把点号替换为下划线,最终变成order_success_total。如果你希望带业务维度,比如记录不同订单来源的成功数,可以加标签:
Counter.builder("order.success.total") .tag("source", "mini_program") .register(registry) .increment();标签是 Prometheus 里非常重要的概念,相当于给指标加维度。但是注意,标签值的基数要控制住。如果你把用户 ID 当标签值,比如.tag("userId", userId),后果就是每个用户都产生一条独立的时间序列,几千个用户就是几千条序列,Prometheus 的存储和查询都会被拖垮。
3.3 自定义 Gauge:队列长度、在线人数、当前水位
Gauge 适合表示可增可减的瞬时值,比如等待队列长度、当前活跃连接数、本地缓存的 key 数量。和 Counter 不同,Gauge 不需要调用 increment,它本质上是一个“当前数值”的读数。
可以用register方法传入一个 GaugeSupplier:
Gauge.builder("queue.waiting.size", queueService::getWaitingSize) .description("当前排队任务数") .register(registry);这里特别注意:Gauge 不要在一个请求里反复调用set()去维护,否则容易写乱。更好的方式是注册一个会“实时读取”的 Supplier,每次 Prometheus 抓取时,它才去取值。这样数值永远能反映当前状态,不用你主动更新。
3.4 用 Timer 和 DistributionSummary 测延迟和大小
HTTP 接口的耗时测量,Spring Boot 已经内置了http_server_requests_seconds指标。但如果你要测量某个业务方法内部某一段逻辑的耗时,可以用Timer:
Timer.Sample sample = Timer.start(registry); try { // 业务逻辑 } finally { sample.stop(registry.timer("order.process.time", "orderType", type)); }Timer不仅记录了耗时列表,还自动记录次数和总时间。Prometheus 会暴露_seconds_count和_seconds_sum,查询平均耗时就是rate(order_process_time_seconds_sum[5m]) / rate(order_process_time_seconds_count[5m])。这里还可以配publishPercentileHistogram=true,让 Micrometer 生成直方图,方便查 p99、p95:
Timer.builder("order.process.time") .publishPercentileHistogram(true) .register(registry);但是注意,publishPercentileHistogram 生成的 Prometheus 指标是 bucket 形式,数据量会增加,如果接口量很大,建议只在需要的接口上用,而不是全局默认开。
DistributionSummary和 Timer 类似,但测量的是“数据大小”或“件数”之类的分布。比如你在分析响应体大小,或者一次批量任务处理的行数,就用得上。使用方法基本一样。
3.5 注解方式和编程方式怎么选
Micrometer 还提供了注解方式。最简单的是@Timed:
@RestController public class DemoController { @Timed(value = "pay.request.time", percentiles = {0.5, 0.9, 0.99}) @PostMapping("/pay") public Result pay() { // ... } }前提是项目里引入了micrometer-core,且在配置类上启用@EnableTiming功能。除了@Timed,还有@Counted。注解方式的优点是侵入小,适合“懒人用法”。但是它的缺点是命名规则相对固定,而且无法动态绑定标签值。如果要在标签里把不同的渠道号、失败码加进去,我建议还是用编程方式,因为注解只适用于静态维度。
现实项目里我的习惯是:简单的耗时统计用注解,涉及多个动态标签时用编程 API,所有统计逻辑集中到一个 MetricService 中,避免业务代码散落指标逻辑。
4. 完整闭环:Prometheus 抓取配置与 Grafana 可视化
4.1 用 Docker 快速启动 Prometheus
本地验证阶段,Docker 是最快的。先准备一个prometheus.yml:
global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: "spring-boot-app" metrics_path: /actuator/prometheus static_configs: - targets: - "host.docker.internal:8080" labels: application: "demo-order-service"然后启动容器,把配置文件挂载进去:
docker run -d \ --name prometheus \ -p 9090:9090 \ -v /path/to/prometheus.yml:/etc/prometheus/prometheus.yml \ prom/prometheus这里用host.docker.internal是给 Docker for Mac/Windows 访问宿主机用的。如果你在用 Linux,容器默认网络模式下访问宿主机要换成宿主机 IP,或者直接用--network=host。填错 host 的结果就是 Prometheus 的控制台里 target 状态显示 DOWN。
启动完成后,打开http://localhost:9090/targets,能看到每个 job 的健康状态。如果是 UP,说明抓取链路已经通了。这里有一个细节:metrics_path里要带/actuator,因为默认 base-path 是/actuator,Prometheus 默认只请求/metrics,不配置的话会直接 404。
4.2 prometheus.yml 中容易被忽略的字段
除了上述最小配置,还有几个字段值得关注:
| 字段 | 作用 | 经验建议 |
|---|---|---|
scrape_interval | 抓取间隔 | 默认 15s 够用,别太频繁 |
scrape_timeout | 单次抓取超时 | 默认 10s,指标多时可调大到 20s |
honor_labels | 是否允许 target 里的标签覆盖全局标签 | 多环境共用时慎开 |
relabel_configs | 标签重写 | 常用于 k8s 环境 |
static_configs | 静态 target 列表 | 小规模直接写 IP 即可 |
比如你抓取到很多服务,可以用labels.application给每个 target 再打了一个标签,这在 Grafana 里可以按应用名筛选。注意区别management.metrics.tags.application和 scrape_configs 里的labels.application,前者是 Micrometer 打进指标内部,后者是 Prometheus 在抓取时附加的。如果两边有同名标签但值不一致,Prometheus 会优先保留 target 上的标签,可能导致你查询时值异常,所以同一套环境里最好保持一致。
4.3 从 Prometheus 到 Grafana
Prometheus 自带的 UI 适合调试,但报表能力太弱。我用 Grafana 比较多。在 Grafana 里加数据源时,类型选 Prometheus,URL 填http://prometheus:9090(如果 Grafana 也在容器中)或者http://localhost:9090(如果 Grafana 在宿主机)。
画面板时,最常用的 PromQL 有这么几个:
- 应用 JVM 堆内存趋势:
jvm_memory_used_bytes{area="heap", application="demo-order-service"}- 应用 QPS:
sum(rate(http_server_requests_seconds_count{application="demo-order-service"}[5m]))- 接口 p99 耗时:
histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket{application="demo-order-service"}[5m])) )- 错误请求占比:
sum by (status) (rate(http_server_requests_seconds_count{status=~"5.."}[5m]))这里http_server_requests_seconds_bucket是 Spring Boot 默认生成的直方图 bucket。histogram_quantile是用来计算分位数的,如果指标没开直方图,是算不出 p99 的。所以当你发现某个接口没有 p99 数据时,先去看原始指标里有没有_bucket。
Grafana 上可以直接导入社区面板,比如 Spring Boot 相关的仪表盘模板,里面有完整的 JVM、线程、Http、数据源监控。但我还是建议自己有选择地建几个关键面板,而不是盲目导一个大而全的。大而全面板看着热闹,实际告警和定位问题时反而容易让人迷失。
4.4 告警规则可以怎么配
如果部署的 Prometheus 具备告警能力,可以加一条规则到rules.yml,比如 JVM 堆内存超过 80% 时告警。在prometheus.yml的rule_files字段引入规则文件,然后在 Prometheus 里配置 Alertmanager 地址。一个例子:
groups: - name: spring-boot-app-alerts rules: - alert: HighJvmHeapUsage expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.8 for: 5m labels: severity: warning annotations: summary: "JVM 堆内存超过 80%"但我要提醒一句:告警阈值一定得基于真实负载去校准。JVM 堆使用率 90% 不一定是性能问题,可能只是堆被分配得比较小,并且 GC 正在正常处理。先看趋势,看是否持续增长,再决定阈值,不要机械地套用公式。
5. 踩坑实录:从端点 404 到指标基数爆炸的处理方案
5.1 访问 /actuator/prometheus 返回 404
这个问题非常常见,原因通常是三个:
management.endpoints.web.exposure.include里没有加prometheus。- 项目里有 Spring Security,没有放行这个端点。
- management 的 base-path 被改掉了,比如改成了
/manage,但抓取配置里还请求/actuator/prometheus。
处理方式分两类。如果是暴露配置问题,把 prometheus 加进去就行。如果是 Spring Security 问题,需要在你现有的SecurityFilterChain里放行该端点:
authorizeHttpRequests(auth -> auth .requestMatchers("/actuator/prometheus").permitAll() .anyRequest().authenticated());我个人建议把management.server.port设为独立端口,并在安全配置里只对这个端口放开管理端点。这样业务安全策略不会误伤监控端点,监控端点也不会因为业务接口需要复杂认证而无法访问。
5.2 Prometheus 页面显示 DOWN
target 显示 DOWN,原因基本出现在网络和路径上。最直接的排查方式是看 Prometheus 抓到的错误信息。常见有:
context deadline exceeded:说明应用响应太慢,或者指标太多,单次抓取超时。增大scrape_timeout。connection refused:端口不对。注意 management 端口和业务端口不要搞混。404 not found:metrics_path不对,或者是 base-path 被改了。- 如果是 Kubernetes 环境,
targets里的 IP 是 Pod IP,需要保证metrics_path在容器内也能访问。
我在一个老项目里还遇到过scrape_interval设成 5s,应用经常触发 Full GC,反而抓取超时。最后改成 15s,并同时调大 JVM 最大堆才解决。别为了“实时性”把抓取间隔设得太短,15秒对应用监控来说已经足够。
5.3 指标基数爆炸:标签值不能是用户ID
这是团队里最容易被忽视的坑。某个同学在自定义指标时写:
registry.counter("api.call.count", "userId", userId);一次推送就产生几千条序列,Prometheus 虽然在设计上是高基数时序数据库,但基数也有限。指标基数过大会导致:
- Prometheus 内存暴涨,查询明显变慢。
- 应用本身因为指标注册和定期扫描,CPU 占用升高。
- Grafana 图表刷新非常卡。
解决办法很简单:标签值应限制在可枚举的、低基数的维度内,比如状态码、渠道、机房、接口名。如果非要做用户维度的分析,就别放 Prometheus 了,放到链路追踪或者 OLAP 系统里去分析,工具场景要分清楚。
5.4 指标端点本身成了性能瓶颈
很多人不知道 Prometheus 的指标端点不是“免费”的。每次/actuator/prometheus被请求时,Micrometer 都要把已有的所有指标序列化成一整段文本。如果注册了几十万条指标,这个序列化的 CPU 开销是非常可观的,甚至会导致服务响应变慢。
针对这个问题,可以从几方面优化:
- 降低高基数标签,从源头减少指标数量。
- 不全局
management.endpoints.web.exposure.include=*,只暴露实际要用的端点。 - 把监控端点放到独立的 management 端口上,避免业务流量和抓取流量互相干扰。
- Prometheus 端增加抓取间隔,降低序列化频率。
- 如果指标量确实过大,可以在 Micrometer 里用
MeterFilter过滤掉不需要的指标名称或标签,避免它们被注册进来。
一个可参考的MeterFilter写法:
@Bean public MeterFilter customMeterFilter() { return MeterFilter.ignoreTags("thread") .and(MeterFilter.denyNameStartsWith("jvm.buffer")); }这样能从应用侧拦住一部分不想采集的指标。不过过滤逻辑要谨慎,先看清要保留什么,别把排查问题要用的关键指标也滤掉了。
5.5 版本合并导致的指标困惑
Spring Boot 2.x 默认生成的 HTTP 指标在不同版本之间有差异。有的版本叫http.server.requests,有的版本叫http_server_requests_seconds。如果你同时还在用 Tomcat 的tomcat.servlet.request.xxx,排查时容易把几个相似指标搞混。
我的经验是优先使用带有_seconds后缀的指标,因为它是 Micrometer 标准化的产物,Query 函数和单位的可读性更强。老项目如果因为升级 Boot 后指标名变了,最好直接在 Prometheus 查询里迁移到新指标,不要长期维护两条相似的查询。
5.6 常见 FAQ 速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| curl 指标端点返回 404 | exposure 未配置 / base-path 变化 | 检查 yml 的 include 与 base-path |
| 指标端点有数据但 Prometheus DOWN | metrics_path 或端口错误 | 检查 prometheus.yml 的 path、targets |
| 抓取超时 | 指标量太大或应用响应慢 | 增大 scrape_timeout / 降低指标基数 |
| 查询时不显示自定义指标 | 指标名被改写(点号转下划线) | 在 Prometheus 里搜_total后缀 |
| JVM 内存面积图异常 | area="heap"与area="nonheap"混用 | 查询时加 area 筛选 |
| 标签里出现以毫秒为单位的耗时 | Timer 默认单位是秒 | 看_seconds,乘 1000 得到毫秒 |
6. 一些实操中的长期建议
把这套体系在项目里跑起来不难,难的是让它稳定持续地发挥价值。我个人的习惯是,每个 Spring Boot 服务都建立一个独立的MetricService,里面集中注册所有业务指标,并且给每个指标写清注释:单位是什么、标签有哪些、统计的是哪个业务流程。这样做并不是为了给别人看,而是半年之后你自己回过来看时,不至于对着一个xxx_total发呆,还要翻 Git 历史。
另一个经验是:告警规则不要一开始就铺很多条,先挑三到五个真正影响服务可用性的指标,比如 JVM 堆使用率持续上涨、接口 5xx 比例超过阈值、HikariCP 活跃连接数打满。跑一段时间之后根据故障案例逐渐补充规则。监控这件事,规则越少越精准,越不容易被噪音淹没。
最后再提一个容易被忽略的点:Prometheus 只是把数据采回来、存下来,真正区分监控好坏的是“趋势”。我见过不少团队只看当前值,发现内存高了就立刻重启应用,重启之后指标归零,问题反复出现却从来没人分析增长曲线。集成完成之后,多花一点时间在 Grafana 上做几个“最近 24 小时趋势”的视图,再结合发布时间点对比,很多问题能提前至少半天被发现。这才是 SpringBoot 集成普罗米修斯这套组合最大的价值所在。