☰
Spring Boot集成Prometheus监控:从指标埋点到告警避坑指南
2026/10/15 5:38:28 网站建设 项目流程

去年有段时间,我负责的一套订单查询服务频繁收到“变慢”的反馈,接口的响应时间时快时慢,数据库看不出明显异常,日志里偶尔有几条慢SQL,修完之后也确实好了几天,可过阵子又复发。真正让我下定决心把Spring Boot的监控全面切换到Prometheus的,是这样的偶发问题根本没有一个直观手段看到全貌——日志只能告诉我“发生了什么”,却无法回答“系统此刻的水位还剩多少”“连接池什么时候先扛不住”。

Prometheus这套组合解决的不只是“接口慢”这一个场景,它把系统的运行时状态变成了可以随时回放、可以横向对比、可以预测趋势的指标流。这篇文章我不打算复述一遍官方文档,而是把Spring Boot集成Prometheus的完整路径拆开讲:版本怎么选、端点怎么暴露、业务指标怎么埋、上线之后又会踩哪些坑。适合正在给Spring Boot项目做监控选型,或者已经集成了但总觉得不对劲的开发者参考。

1. 为什么是Prometheus:它解决了其他监控工具解决不了的问题

1.1 日志、数据库监控、APM各自的盲区

先说那段排查经历。我的项目里当时并不缺监控手段,甚至可以说比较齐全:日志系统、数据库监控、APM链路追踪都有。可真要定位这类偶发性能问题的时候,三样工具各有各的局限。

  • 日志系统:能把异常和慢SQL抓出来,但看不出“整体水位”。问题发生的那几秒里,系统CPU、内存、线程池、数据库连接池到底是什么状态,日志基本给不出来。
  • 数据库监控:能定位慢SQL,可SQL往往是结果而不是根因。连接在等待什么、资源在哪个环节耗尽,还得另外去数据库端找线索。
  • APM工具:链路耗时看得比较清楚,但免费版本保留窗口短,而且对“自定义业务指标”的支持非常弱,想给某个核心接口单独加上“下单次数”“失败次数”这类计数器,操作起来很别扭。

这三样工具的共同问题是:它们都偏向于“记录事件”,而不是“持续测量状态”。就像体检报告和急诊病历的区别——APM和日志更像急诊病历,哪里出事了记哪里;而Prometheus更像体检报告,每15秒给系统量一次体温、血压、心率,数据连续可对比。

1.2 拉取模型带来的结构性优势

Prometheus在取数方式上跟传统监控有一个本质差异——它是“拉”不是“推”。Prometheus Server每隔一段时间,主动访问应用暴露出的HTTP端点,比如/actuator/prometheus,把当时的指标快照抓走,存进自己的时序数据库。

这个“拉取模型”带来几个非常实际的好处:

  • 应用侧不需要关心指标往哪里推、推送失败要不要重试、消息积压怎么办。它只需要把一个HTTP端口暴露在外面就够了。
  • 监控端可以从任何一个实例拉取数据,天然适合多个副本同时运行的环境。哪个节点健康就抓哪个节点。
  • 只要服务遵循Prometheus的指标暴露规范,就能被直接纳管,不需要在每台机器上额外装Agent程序。

这种设计对Java生态还有一个额外加分项:Spring Boot的Actuator模块本身就在HTTP端口上暴露监控端点,于是在Java世界里大家约定俗成,指标格式统一交给一个叫Micrometer的门面处理,Prometheus只是Micrometer支持的一种输出格式。

1.3 Spring Boot项目目前的监控真实现状

Spring Boot从1.x时代就有Actuator,但它的主要作用是暴露健康检查、配置信息这些运维端点。真要给业务打点,提供的原生能力很粗。后来社区统一走向了Micrometer方案:业务代码里只操作MeterRegistry接口,底层实际输出成什么格式,由引入的具体注册表实现决定。

micrometer-registry-prometheus就是那个把Micrometer指标翻译成Prometheus文本格式的注册表实现。业务埋点时你完全不需要接触Prometheus的客户端库,这层解耦在未来切换监控后端时会非常值钱——比如某天你想从Prometheus切到InfluxDB,业务代码一行都不用动。

2. 版本与依赖的匹配:见过太多在这里翻车

2.1 Micrometer的定位:Java世界的监控门面

Micrometer本质上是一个门面库,跟SLF4J在日志系统里的定位很像。SLF4J让你用统一的Logger接口打日志,底层想接Logback还是Log4j2随时换;Micrometer让你用统一的MeterRegistry接口做计量,底层想输出到Prometheus还是Datadog随你便。

集成Spring Boot项目时,我们通常引入两个依赖:spring-boot-starter-actuator负责把监控端点暴露到HTTP端口上,micrometer-registry-prometheus负责把指标翻译成Prometheus格式。Actuator负责“通道”,Prometheus Registry负责“翻译”,职责非常清晰。

2.2 Spring Boot 2.x和3.x的版本差异

不同版本的Spring Boot对应的Micrometer大版本是不一样的要求,这点如果搞错,后面会跑出一堆莫名其妙的错误。

Spring Boot版本推荐的Micrometer版本依赖写法差异注意事项
2.x1.9.x / 1.10.xio.micrometer:micrometer-registry-prometheus与spring-boot-starter-actuator配合,版本由Boot BOM统一管理
3.x1.10.x及以上io.micrometer:micrometer-registry-prometheus底层基于Jakarta EE,与旧版API不兼容

Spring Boot项目里一般不需要手动指定Micrometer的版本号,直接用Boot自己的BOM管理即可。只有当项目没有使用spring-boot-dependencies做统一依赖管理,或者手动覆盖了版本时,才需要认真核对这三者的版本兼容性。

2.3 最小可运行依赖清单

以一个标准的Maven项目为例,集成Prometheus监控需要的最小依赖是这个样子:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <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-starter-web不一定必须有,如果你的项目不是Web项目,Actuator同样可以通过其他端口暴露端点,但绝大多数Spring Boot服务都是Web应用,所以这个依赖通常都会带上。

2.4 版本不匹配的典型症状

如果在老旧的Spring Boot 2.2版本上强行引入新版Micrometer 1.13,启动时会直接抛出NoSuchMethodError,报错位置往往是在PrometheusMeterRegistry的初始化方法里。这类错误跟业务代码无关,定位方法很简单:先把micrometer-registry-prometheus的版本改为跟随Boot BOM,或者显式把Boot升级到能配套该Micrometer的版本。

还有一个常见的反模式是:业务代码里直接依赖了io.prometheus:simpleclient这个Prometheus客户端库。理论上这也不是不行,但会让埋点代码同时维护两套API,而且一旦未来切换监控后端,这些直接依赖就会变成清理不干净的脏账。统一走MeterRegistry接口才是省心路径。

3. 最简集成路径:端点暴露与抓取配置

3.1 第一步:让Actuator把Prometheus指标暴露出来

Spring Boot的Actuator默认只暴露health和info端点,prometheus端点默认是关闭的,需要手动开启。在application.yml里加这样一段配置:

management: endpoints: web: exposure: include: health,info,prometheus metrics: tags: application: ${spring.application.name}

这段配置做了两件事:第一,把prometheus端点加进Web暴露列表;第二,给所有指标加上一个application标签,标签值取应用名。第二点极其重要,当你有多个服务写入同一个Prometheus时,没有这个标签,你会在Grafana里分不清哪个指标来自哪个服务。

如果你的项目设置了server.servlet.context-path,比如全局API前缀是/api,还要注意Actuator端点默认不会跟随这个前缀。如果希望端点统一带前缀,需要额外加:

management: server: base-path: /api

但一般的做法是让Actuator端点保持独立路径,避免跟业务接口混在一套鉴权逻辑里。实测下来,监控端点单独管理更清爽。

3.2 第二步:配置Prometheus抓取任务

Prometheus服务端的抓取配置文件prometheus.yml里,新增一个scrape_config任务即可:

global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'springboot-app' metrics_path: '/actuator/prometheus' static_configs: - targets: - '192.168.1.10:8080'

scrape_interval默认15秒足够用,不需要设得太频繁。Prometheus的抓取本身有一定的性能开销,尤其是指标数量上去之后,每15秒拉一次几MB的文本是很正常的量。metrics_path一定不要写成带http://前缀的完整URL,这里只填路径。

targets里的地址要填Prometheus容器或服务器能访问到的地址。如果你把Prometheus也跑在Docker里,就会遇到一个新手极其常见的坑:在配置里填localhost:8080,Prometheus容器内部解析localhost指向的是容器自己,永远抓不到宿主机上的Spring Boot服务。这种情况要么用宿主机IP,要么用host.docker.internal这样的特殊域名。

3.3 验证链路是否打通的快捷方式

配置完成后怎么确认链路通了?三步走:

首先,浏览器或curl直接访问应用的指标端点:

curl http://localhost:8080/actuator/prometheus | head -20

正常会看到一堆以# HELP和# TYPE开头的文本,紧接着是具体指标,例如:

# 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",} 1.23456789E8

如果这里都看不到内容,说明应用侧的暴露配置有问题,先去排查Actuator端点是否真的开启了。第二步,访问Prometheus自带的查询页面,在标签栏里搜jvm_memory_used_bytes,如果能搜到数据,说明抓取成功。第三步,在Prometheus查询框里输入rate(jvm_memory_used_bytes[1m])这类PromQL,确认数据能正常计算。

这三步走完,一次最基本的集成就算彻底打通了。

4. 从“系统指标”到“业务指标”:Counter、Timer、Gauge的使用边界

Actuator和Micrometer默认给我们带了一大堆JVM指标:内存、GC、线程池、HTTP请求。但真正让监控价值翻倍的,是给自己的业务逻辑埋点。Micrometer提供了几种核心计量器,每一种都有严格的使用场景,用错了指标语义就乱了。

4.1 Counter:只增不减的计数器

Counter是最直观的计量器,用来记录单调递增的次数。简单说,它就是一个“只加不减”的计数器。

适用场景很清晰:请求总量、下单成功数、支付失败数、限流拦截次数。我的代码里通常是这么用的:

@Service public class OrderService { private final Counter orderCreateCounter; private final Counter orderFailCounter; public OrderService(MeterRegistry registry) { this.orderCreateCounter = Counter.builder("business_order_create_total") .description("订单创建成功次数") .tag("channel", "app") .register(registry); this.orderFailCounter = Counter.builder("business_order_fail_total") .description("订单创建失败次数") .tag("channel", "app") .register(registry); } public void createOrder(OrderRequest request) { try { // 业务逻辑 orderCreateCounter.increment(); } catch (Exception e) { orderFailCounter.increment(); throw e; } } }

这里有一个关键点:计数器只有在Prometheus里用rate()或increase()函数处理后才有意义。你看到business_order_create_total的值一直在涨,这不是问题,这正是Counter的设计——PromQL里rate(business_order_create_total[1m])就能算出每分钟的订单创建速率。如果你把Counter当成当前值来读,那语义就错了。

4.2 Timer:耗时测量的关键

Timer是另一个高频使用的计量器,它同时记录两件事:调用次数和每次调用的耗时。在Prometheus输出里,它会生成_count、_sum、_max这三个基础指标,其中_sum / _count就是平均耗时,_max是窗口内的最大耗时。

实测下来,给核心接口加上Timer最有价值,它能直接告诉你P99延迟到底是多少,而不用再去日志里翻那些零零散散的慢请求:

@RestController public class OrderQueryController { private final Timer orderQueryTimer; public OrderQueryController(MeterRegistry registry) { this.orderQueryTimer = Timer.builder("business_order_query_seconds") .description("订单查询耗时") .publishPercentileHistogram() .register(registry); } @GetMapping("/order/{id}") public Order queryOrder(@PathVariable Long id) { Timer.Sample sample = Timer.start(registry); try { // 业务逻辑 return orderService.query(id); } finally { sample.stop(orderQueryTimer); } } }

Timer.start(registry)这一行一定要写在业务逻辑开始之前,sample.stop()要放在finally里,保证无论业务是成功还是抛异常,耗时都会被记录。如果你把Timer.start放在某几行代码之后,那测量到的就是片段时间,数据会偏差很大。

4.3 Gauge:实时水位的观察窗

Gauge与Counter完全相反,它的值可以上下波动,是用来观察“当前水位”的。连接池活跃连接数、队列当前长度、内存使用率、在线用户数,这些都是典型的Gauge场景。

@Component public class PoolMonitor { public PoolMonitor(MeterRegistry registry, ThreadPoolTaskExecutor executor) { Gauge.builder("business_thread_pool_active", executor, ThreadPoolTaskExecutor::getActiveCount) .description("业务线程池活跃线程数") .register(registry); } }

Gauge在Micrometer里的平均值含义与Counter不同,它不需要rate()处理,直接查询就是在当前抓取时刻的值。这符合“水位表”的语义——你想看的就是“此刻池子里有多少水”。

4.4 标签设计:监控数据值不值钱就看这一关

我觉得埋点里最有技术含量、也最容易被人忽略的是标签设计。标签是Prometheus的维度体系,设计得好,一个指标可以拆出几十条时间序列;设计得差,一个指标直接拖垮监控端的存储。

几条长期实践下来的准则:

  • 标签维度宁少勿多。每个标签都会增加维度的组合数,组合是指数级的。核心接口按uri、method、status三个维度拆分,已经足够覆盖绝大多数分析需求。
  • 不要把用户ID、订单ID写进标签。这类值基数极高,会生成几十万条时间序列,Prometheus内存和存储直接被撑爆。
  • 标签的值必须来自有限集合。比如状态值只有success、fail、timeout,渠道来源只有几个固定的枚举值,这样维度组合可控。
  • 同一次注册的标签要稳定。同一个指标的标签key集合必须一致,否则Micrometer会在运行期抛异常,这一点在下一节会专门展开讲。

5. 上线之后踩过的坑:几个问题的完整排查链路

5.1 坑一:/actuator/prometheus一直404,Prometheus抓不到数据

现象:配置全部按照文档写了,浏览器访问http://localhost:8080/actuator/prometheus,返回404。

排查链路:我先检查了application.yml里management.endpoints.web.exposure.include是不是漏了prometheus。后来发现没漏。又检查了依赖里是否有micrometer-registry-prometheus,也没漏。最后用curl http://localhost:8080/actuator把所有端点列出来,发现prometheus端点压根没注册。

问题的真正原因:项目里手动覆盖了management.endpoints.web.path-mapping,把prometheus重命名成了别的路径。这种配置本身没问题,但会导致Prometheus默认抓/actuator/prometheus时404。解决办法是在scrape_configs里的metrics_path改成被重命名后的路径,或者干脆去掉这个重映射,保持默认路径与Prometheus配置一致。

5.2 坑二:指标名称相同但标签key不一致,运行期直接异常

现象:服务启动时报错,日志里出现一行非常关键的提示:

Prometheus requires that all meters with the same name have the same set of tag keys. There is already an existing meter named 'business_order_seconds' containing tag keys [uri]. The meter you are trying to register has the same name but different tag keys [uri, method].

排查链路:这个报错直接说明问题出在我的代码里:同一个指标名business_order_seconds被注册了两次,一次只带uri标签,另一次带了uri和method两个标签。Prometheus不允许同一个名称的指标拥有不同的标签集合,因为它会把它们当成同一条时间序列的不同维度来存储,这在逻辑上本身就是矛盾。

问题根源:我在两个不同的位置用Timer.builder("business_order_seconds")创建了指标,一处漏了method标签,一处加了。这类问题特别容易在公共埋点工具类里出现,尤其是当多个调用方传入动态标签数组时。

解决方案也很清晰:指标注册应该集中在某个专门的MetricsConfig类里,由构造函数统一负责注册,业务代码只负责引用已注册的计量器,不再自行用Timer.builder()创建。同时,如果确实需要动态标签,应该用相同的标签key集合,只是在不同调用中传入不同的值。

5.3 坑三:P99数字和用户真实感受对不上

现象:Grafana里显示的P99延迟只有几百毫秒,但业务同学反馈说用户卡到了几秒才出结果,数字与体验完全对不上。

排查链路:我先怀疑埋点位置不正,后来把Timer.start移到了Controller入口的最前面。再看指标,P99确实下降了,但还是对不上。接着查PromQL查询方式,发现用的是histogram_quantile(0.99, sum(rate(..._bucket[5m])) by (le)),这是标准写法,没问题。

最后把问题定位到了Micrometer的Timer实现:如果只是调用Timer.builder("xxx").register(registry),默认生成的分位数是一个窗口期内的近似值,特别是对偶发性慢请求的捕捉会严重低估。要拿到相对准确的分位数,必须显式开启publishPercentileHistogram(),这样才能生成适合histogram_quantile聚合计算的分桶直方图。

这是我踩过最深的一个坑。解决方案是给核心接口的Timer都加上publishPercentileHistogram(),同时在PromQL查询里使用5分钟窗口的rate做聚合,这样得到的P99才基本贴合真实体验。

5.4 坑四:把用户ID写进标签,内存直接起飞

现象:一个“给每个用户统计在线状态”的需求,我在Gauge里给每个用户加了一个userId标签。上线半小时后,Prometheus容器的内存持续攀升,最终OOM重启。

排查链路:Prometheus查询页面里按userId分组统计指标数量,发现时间序列数量是用户数的十倍还不止。因为每个用户ID都变成了一条独立的时间序列,值再乘以指标字段数量,几十万用户就是几十万条序列,对Prometheus的存储和内存都是毁灭性的。

正确做法是放弃用独立的userId标签追踪每个用户,改成按区间统计:比如Gauge的值表示“在线用户总数”,再按城市、版本等有限维度拆分,这样时间序列数量始终可控。这也是标签设计“基数要低”这条铁律最鲜血淋漓的验证。

6. 让Prometheus真正产生价值:Grafana看板与告警规则

6.1 数据源接入与第一张看板

指标被抓进Prometheus之后,大多数人都会配一个Grafana来做可视化。接入的步骤很简单:登录Grafana,进入Configuration,选择Data Sources,类型选Prometheus,URL填Prometheus服务的地址,比如http://prometheus:9090,点Save & Test即可。

Grafana的Dashboards市场里有很多现成的Spring Boot监控模板,可以直接搜“Spring Boot”导入。导入模板之后,JVM内存、GC次数、HTTP请求量这些基础指标就有了现成的看板。我自己通常还会在此基础上新建一个业务看板,把核心接口的请求量、平均耗时、P99、错误率放进去。

6.2 几个高频率的PromQL查询

看业务看板时,下面几个PromQL是我日常使用频率最高的:

每分钟请求量,按接口拆分:

sum(rate(http_server_requests_seconds_count[1m])) by (uri)

平均耗时:

sum(rate(http_server_requests_seconds_sum[1m])) by (uri) / sum(rate(http_server_requests_seconds_count[1m])) by (uri)

P99延迟:

histogram_quantile(0.99, sum(rate(business_order_query_seconds_bucket[5m])) by (le) )

错误率:

sum(rate(business_order_fail_total[5m])) / sum(rate(business_order_create_total[5m]) + rate(business_order_fail_total[5m]))

这些查询都很基础,但在实际定位问题时价值巨大。尤其是把“请求量”和“P99”放在同一张图里,能非常清楚地看到系统是在量涨之后开始劣化的,还是请求量平稳但延迟凭空升高——两种情况的排查方向完全不同。

6.3 告警规则与SLO的设计思路

指标采集到位后,告警规则是下一个重点。Prometheus的告警规则写在独立的规则文件里:

groups: - name: springboot-app rules: - alert: HighErrorRate expr: | sum(rate(business_order_fail_total[5m])) / sum(rate(business_order_create_total[5m]) + rate(business_order_fail_total[5m])) > 0.05 for: 5m labels: severity: warning annotations: summary: "订单创建错误率超过5%"

规则里的for: 5m表示错误率持续5分钟超过阈值才触发告警,这能有效过滤掉瞬时抖动造成的误报。在设计告警时要注意,不要把服务的SLO全部寄托在单一指标上,建议同时监控可用性、延迟和错误率三个维度。我也比较推荐按接口重要程度分级:核心交易链路用更严格的阈值,边缘接口可以放宽。

6.4 监控的最后一公里:从“有监控”到“用监控”

很多人把Prometheus和Grafana搭起来之后就觉得大功告成了,其实这只是开始。指标上线之后,更重要的是形成一套观察习惯——每周固定翻一翻趋势图,看看各项指标的基线是不是在缓慢爬升;每次发版后对比新旧版本的指标曲线;每次告警触发之后,把关联的指标面板截图归档到故障复盘里。只有真的把监控数据用起来,才能体现出前面那些埋点工作真正的价值。

我个人的体会是,Prometheus这套体系的学习曲线不算陡,但从“接口通了、能抓到指标”到“指标能帮我们定位问题、提前预警”,中间需要靠真实的故障去磨合。特别是前面提到的埋点设计规范和标签基数控制,几乎每个团队都会至少踩一次坑。建议大家在自己项目里落地时,先把这套组合完整跑起来,再逐步补充业务埋点和告警规则,慢慢把监控从被动响应变成主动感知。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询