
1. 为什么我的SpringBoot服务“看起来活着”却总是半夜报警先说个真实的场景。上个月我们一个订单服务在凌晨3点CPU飙到95%接口平均响应时间从80ms涨到了3秒但服务本身没有宕机健康检查也一直返回UP。如果不是夜里值班同学盯着Grafana面板发现了曲线异常这个问题可能要等到早上用户大规模投诉才暴露。这就是典型的“活得好好的但活得很难受”的状态。传统的方式是出了问题看日志但日志只能告诉你“某个时刻发生了什么”很难告诉你“这个趋势是怎么演变的”。而SpringBoot集成Prometheus本质上是给应用装上一套持续记录生命体征的监测仪让你随时知道服务的吞吐量、响应延迟、JVM内存、线程池状态这些关键指标到底在发生什么变化。这篇文章我会从零开始把一个普通的SpringBoot服务接入Prometheus再配合Grafana出面板最后讲讲指标定制和告警配置的实战经验。整个过程不需要你有运维背景只要你手上有一个能跑起来的SpringBoot项目就行。内容偏实践我尽量把每一步背后的原因也讲清楚这样你遇到问题的时候不是只会抄配置而是能自己判断哪里出了岔子。2. 核心链路拆解从SpringBoot到Prometheus的数据流转过程很多初学者第一次接触监控体系容易把Prometheus和SpringBoot之间的关系搞混。其实这条链路上有三个角色各司其职理清楚之后后面的配置都不会迷路。2.1 链路中的三个角色各自负责什么SpringBoot应用通过Micrometer收集自身运行状态JVM、HTTP请求、线程池等并把指标数据以特定格式暴露成一个HTTP端点默认是/actuator/prometheus。Prometheus服务端定期默认15秒主动去拉取Pull上面那个HTTP端点的数据存到自己的时序数据库里并提供PromQL查询语言让你检索指标。Grafana从Prometheus查询数据用图表方式展示出来是可视化层。这里面最关键的一个设计思想是拉取模式Pull而不是应用主动上报Push。Prometheus定时来“问”应用要数据这种模式的优点是应用不需要知道监控系统的地址Prometheus挂了也不影响应用本身而且你随时可以用PromQL手动查历史数据不用依赖应用侧做任何事。2.2 Micrometer在这条链路里扮演的角色Micrometer是SpringBoot 2.x之后官方推荐的指标采集门面可以理解为SLF4J在日志领域的地位——它定义了一套统一的指标API底层可以对接多种监控系统。你在代码里只需要使用Micrometer的API记录指标至于数据最终是给Prometheus、InfluxDB还是New Relic完全由配置决定业务代码不需要改动。我见过有的老项目直接用Prometheus官方Client库然后在每个业务方法里手动埋点代码里全是Metrics的调用。这种方式不是不行但耦合太重而且对SpringBoot的自动配置支持远不如Micrometer。SpringBoot Actuator已经把Micrometer和Prometheus的集成做了大量自动配置我们只需要引入依赖大部分基础指标就自动有了。提示如果你用的还是SpringBoot 1.x情况会麻烦不少Actuator的配置方式和端点路径完全不同。这篇文章按SpringBoot 2.x/3.x来写也是目前的主流版本。2.3 指标数据类型和SpringBoot默认暴露了哪些指标Prometheus的数据模型是带标签label的时间序列。比如http_server_requests_seconds_count{methodGET,status200,uri/api/order}这条数据包含指标名、标签集合、时间戳和样本值。理解这个模型很重要因为后面写PromQL查询、配置告警规则都是在和标签打交道。SpringBoot通过Actuator Micrometer默认暴露的指标非常丰富常用的几类JVM相关jvm_memory_used_bytes堆内存用量、jvm_gc_pause_secondsGC暂停时间、jvm_threads_live_threads存活线程数系统相关system_cpu_usage整机CPU使用率、process_cpu_usage进程CPU使用率HTTP相关http_server_requests_seconds请求耗时直方图及计数、http_server_requests_seconds_count请求总数线程池相关tomcat_threads_busy_threads、tomcat_threads_current_threads数据库连接池hikaricp_connections_active、hikaricp_connections_waiting这些指标不需要你写一行代码只要Actuator配置正确Prometheus拉过来就有。这也意味着你可以在接入的第一个小时就看到一套相当完整的服务画像后面再根据自己的业务逐步增加定制指标。3. 动手实操SpringBoot接入Prometheus的完整步骤接下来是硬核部分。我会从依赖、配置、验证到启动Prometheus把每一步都过一遍。我自己踩过的坑会单独标注出来这些内容在官方文档里一般不会写。3.1 引入依赖这三个依赖缺一不可在pom.xml里加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency第一个是Actuator提供端点暴露能力第二个是Micrometer的Prometheus注册器负责把指标数据格式化成Prometheus能识别的文本格式。有同学会问为什么不需要单独引入Micrometer核心库因为spring-boot-starter-actuator里面已经传递依赖了micrometer-core我们只需要加注册器即可。这里有个小知识点Prometheus注册器本身不包含在Actuator里因为Actuator是通用监控门面不知道你最终用哪家监控系统。3.2 application.yml配置暴露端点而不是全部开放management: endpoints: web: exposure: include: health,info,prometheus metrics: tags: application: ${spring.application.name:unknown}这里有个细节值得展开说。management.endpoints.web.exposure.include这一项必须显式列出prometheus因为SpringBoot默认只暴露health和info两个端点。有些人图省事直接写include: *把全部端点都暴露出去这在开发环境可以生产环境建议不要这么干。每一个暴露的端点都是一次攻击面扩展特别是shutdown这类危险端点万一漏到公网后果很严重。增加management.metrics.tags.application是为了给所有指标打上一个统一的标签标识数据来自哪个应用。这个在多个服务接入同一个Prometheus时极其重要不然你在Grafana里根本分不清哪个指标来自哪个服务。3.3 验证SpringBoot侧是否就绪启动应用后先请求一下Actuator的Prometheus端点curl http://localhost:8080/actuator/prometheus如果一切正常你会看到一堆以# HELP和# TYPE开头后面跟具体指标数据的文本内容。这段文本就是Prometheus实际拉取的数据格式。看到这个输出说明SpringBoot侧已经准备就绪。注意如果你的服务加了server.servlet.context-path那么Actuator端点的路径也会带上这个前缀比如http://localhost:8080/myapp/actuator/prometheus。还有配置了management.server.base-path的话端点路径还会被二次改变。这两个配置同时存在时的最终路径很多人搞混我的经验是优先看server.servlet.context-path它是最高层次的前缀。3.4 安装并启动Prometheus服务端SpringBoot侧搞定之后需要一个Prometheus服务端来拉取数据。我是在一台Linux服务器上用Docker部署的整个启动过程两分钟搞定前提是你已经装了Docker。先写一个prometheus.yml配置文件global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: order-service metrics_path: /actuator/prometheus static_configs: - targets: [192.168.1.100:8080] labels: instance: order-service-prod然后启动容器docker run -d \ --name prometheus \ -p 9090:9090 \ -v /opt/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml \ -v /opt/monitoring/prometheus-data:/prometheus \ prom/prometheus:v2.53.0启动之后访问http://你的服务器IP:9090/targets正常情况下能看到order-service这个job的状态是UP并显示上次抓取的时间。这一步如果显示DOWN最常见的原因是网络不通或者端口没放行先telnet 192.168.1.100 8080试试。3.5 常见接入问题排查我在这里卡过三次的经验问题一MetricsPath配置错了如果你在prometheus.yml里写了metrics_path: /prometheus会404。因为SpringBoot Actuator默认端点路径是/actuator/prometheus不是/prometheus。这是个很小但特别容易忽略的问题我见过不止一个同事在这里卡住。问题二标签冲突导致数据被覆盖在prometheus.yml的static_configs里给不同的服务配了同一个instance标签值会导致两个服务的指标在Prometheus里互相覆盖表现为数据曲线断断续续、跳来跳去。我的习惯是instance标签直接用服务名加环境后缀确保全局唯一。问题三防火墙拦截云服务器上默认的安全组可能没有放行Prometheus抓取源IP所在网段的8080端口。这个查起来最隐蔽因为应用自己是好的本机curl也正常就是Prometheus拉不到。后来我用tcpdump在应用服务器上抓包才发现数据包根本没到。经验是接入任何采集系统之前先确认一下网络路径应用 - Prometheus服务端 - 应用双向都要通。4. 指标定制基础指标远远不够你的业务需要自己的监控语义默认指标能覆盖“服务是否健康”这个层面但业务层面的监控才是最有价值的。举几个实际场景支付系统的回调延迟第三方支付回调到我们系统的耗时分布订单服务的取消率单位时间内取消订单数占总订单数的比例库存服务的超卖拦截次数并发扣减库存时被业务规则拒绝的次数这些指标直接反映业务健康状况默认的JVM指标看不到这些。好在Micrometer的API足够简洁定制指标并不是什么大工程。4.1 Counter只增不减适合统计次数RestController RequestMapping(/api/order) public class OrderController { private final MeterRegistry meterRegistry; private final Counter orderCancelCounter; public OrderController(MeterRegistry meterRegistry) { this.meterRegistry meterRegistry; this.orderCancelCounter Counter.builder(order.cancel.count) .description(Total number of cancelled orders) .register(meterRegistry); } PostMapping(/cancel) public Result cancel(RequestBody CancelRequest request) { // 业务逻辑 orderCancelCounter.increment(); return Result.success(); } }Counter通常配合rate()函数使用因为在Prometheus里直接看Counter本身的数值意义不大因为它是累积的看不到趋势。正确姿势是查它的速率rate(order_cancel_count_total[5m])表示最近5分钟内每秒取消的订单数。注意一个命名细节Micrometer的Counter在导出到Prometheus后指标名会被自动加上_total后缀。所以代码里叫order.cancel.count在Prometheus里查的时候要写order_cancel_count_total。4.2 Gauge可增可减适合记录当前值Component public class StockMetrics { private final Gauge stockLevelGauge; public StockMetrics(MeterRegistry meterRegistry) { this.stockLevelGauge Gauge.builder(stock.level, this, StockMetrics::getCurrentStockLevel) .description(Current stock level) .register(meterRegistry); } private int getCurrentStockLevel() { // 从缓存或数据库获取当前库存 return stockService.getCurrentStock(); } }Gauge适合表示某个时刻的瞬时值比如当前库存量、当前在线人数、队列当前长度。和Counter不同Gauge不需要increment()它的值是从你提供的函数里实时计算的每次Prometheus来抓取的时候都会调用一次。4.3 Timer耗时统计生产环境必须关注百分位private final Timer orderProcessTimer; public OrderService(MeterRegistry meterRegistry) { this.orderProcessTimer Timer.builder(order.process.time) .description(Time taken to process an order) .publishPercentileHistogram(true) .register(meterRegistry); } public Order createOrder(OrderRequest request) { return orderProcessTimer.record(() - { // 业务处理逻辑 return orderRepository.save(requestToEntity(request)); }); }Timer比单纯记录“平均耗时”要强大得多。平均耗时会被极端值拉偏——比如某一次GC导致某个请求耗时10秒平均时间立刻被拉高但“绝大多数用户感受到的延迟”其实没变。Timer默认会生成_seconds_count、_seconds_sum、_seconds_max配合publishPercentileHistogram(true)还会生成直方图可以在Grafana里算P95、P99这些真正反映用户体验的百分位指标。注意publishPercentileHistogram会带来额外的内存开销因为Prometheus的直方图是有桶bucket的每个标签组合都会有一堆桶的数据。如果你的指标很关键而且请求量极大需要评估一下开销。我的实践是核心接口开启低频接口不开启。4.4 指标命名规范和Tag的使用原则Micrometer的指标名建议用小写点分格式比如order.create.success、payment.callback.delay。标签Tag用来区分同一个指标的不同维度比如order.create带上statussuccess和statusfailed两个标签值就可以按状态分组查询。但标签不是越多越好。标签的基数cardinality直接影响内存占用。想象一下每个指标带userId标签10万个用户就有10万个时间序列Prometheus的内存会爆掉。标签应该选择低基数的维度比如接口路径、状态码、机房、应用名千万不要把用户ID、订单ID这种高基数内容放到标签里。5. PromQL查询与告警规则让数据真正发挥价值现在Prometheus已经能拉到数据了但光有数据没人看等于白搭。要让监控真正发挥作用至少要把查询和告警跑起来。5.1 几个必须掌握的PromQL查询模式PromQLPrometheus Query Language是查询数据的核心语言掌握它才能让监控体系从“能看到数据”升级为“能发现问题”。看几个最常用的查询表达式JVM堆内存使用量jvm_memory_used_bytes{areaheap}HTTP接口P99响应耗时按接口维度分组histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))这个查询拆解开来是rate(...[5m])计算每个bucket在5分钟内的速率sum(...) by (le, uri)按响应桶上界和接口路径聚合最后histogram_quantile(0.99, ...)从直方图估算出P99值。P99的含义是99%的请求耗时低于这个值比平均耗时更能反映真实用户体验。CPU使用率rate(process_cpu_usage[5m]) * 100错误率超过5%的接口sum(rate(http_server_requests_seconds_count{status~5..}[5m])) by (uri) / sum(rate(http_server_requests_seconds_count[5m])) by (uri) 0.05最后一个是我在实际告警里经常用的模式两个查询结果做除法得到每个接口的5xx错误率占比再和阈值0.05比较。那一眼语法有点绕但逻辑其实很简单——先算分子错误请求数速率再算分母总请求数速率两者相除就是错误率。5.2 告警规则配置别让告警变成“狼来了”告警规则写在Prometheus的配置里格式如下groups: - name: order-service-alerts rules: - alert: HighErrorRate expr: | sum(rate(http_server_requests_seconds_count{status~5..}[5m])) by (uri) / sum(rate(http_server_requests_seconds_count[5m])) by (uri) 0.05 for: 5m labels: severity: critical annotations: summary: High error rate on {{ $labels.uri }} description: Error rate is above 5% for the last 5 minutes.这里有个参数for: 5m非常关键。它的意思是条件持续满足5分钟才触发告警。这可以有效避免偶发抖动导致的告警轰炸——比如一次网络抖动导致某个瞬间错误率飙升但立刻恢复了这种情况其实不需要通知值班人员。告警规则配置好之后记住修改Prometheus配置后需要重载docker exec prometheus kill -HUP 1kill -HUP 1是让Prometheus平滑重载配置的方式不用重启容器。我早期的做法是直接重启容器后来发现这会导致短暂的数据抓取中断虽然只有几秒但如果正好在告警恢复的窗口期内可能错过通知。5.3 告警发到钉钉/企微/飞书的常见做法Prometheus本身不做消息分发告警需要经过Alertmanager组件来路由到不同渠道。我的做法是用Alertmanager把告警转发到钉钉的Webhook地址。Alertmanager的配置文件大概长这样route: group_by: [alertname] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: dingtalk receivers: - name: dingtalk webhook_configs: - url: https://oapi.dingtalk.com/robot/send?access_token你的token send_resolved: true几个参数的实际体验repeat_interval是告警重复通知的间隔设成4小时比较合适。不然每5分钟发一条消息值班同学的手机很快就会被刷爆然后他就会把告警静音之后真正的严重故障反而被忽视了。group_wait是同类告警聚拢的等待时间比如同一时刻有10个接口都错误率超阈值等30秒把10条合并成一条发出去减少打扰。send_resolved: true表示问题恢复后也通知一声值班同学看到“已恢复”的消息才会真正安心。这条链路部署起来需要额外启动一个Alertmanager容器配置不算复杂但能把告警从“Prometheus里的一行红字”变成“手机上的一条推送”价值完全不一样。6. 接入Grafana面板内行看曲线外行看大板子数据有了告警有了还需要一块好看的屏幕让团队和领导都能直观看到服务状态。Grafana在这里承担的就是可视化工作。6.1 快速部署并连接Prometheus数据源Grafana部署同样用Docker一条命令的事docker run -d \ --name grafana \ -p 3000:3000 \ -v /opt/monitoring/grafana-data:/var/lib/grafana \ grafana/grafana:latest启动后访问http://你的服务器IP:3000默认账号密码都是admin首次登录会要求修改密码。接下来在首页找到“Add your first data source”选择Prometheus填上Prometheus的地址比如http://192.168.1.100:9090点击“Save test”提示成功即可。6.2 从JVM到HTTP一张看的舒服的面板需要哪些要素我的经验是一张好的监控面板应该遵循“从宏观到微观”的层次第一行服务可用性。请求QPS、错误率、平均响应时间。这几个指标最能快速判断服务是否处于健康状态。第二行资源消耗。JVM堆内存、GC频率和耗时、CPU使用率、线程数。对应排查性能瓶颈时最常查看的指标。第三行中间件状态。数据库连接池活跃连接数、等待连接数这是高并发场景最容易出问题的地方。创建面板时注意Grafana的变量功能非常实用。比如把application标签做成一个下拉列表变量然后所有图表都用$application变量来过滤数据这样一套面板可以复用到所有接入监控的服务上不用每个服务单独建一块板子。6.3 别把错误率千分位当成小数点错误率这是我见过最多人犯错的地方。Prometheus的指标值很多是小数形式的比率比如http_server_requests_seconds_count的rate值通常在0到几十之间但process_cpu_usage的取值是0到1的小数表示CPU使用率是0.5代表50%。在Grafana面板里配置单位的时候process_cpu_usage如果直接选“percent0-100”会显示成50正确但有些面板模板里选的是“percentunit0-1”那显示的就是0.5看起来像是CPU一直在50%而不是100%。我经常看到有人拿着告警截图说CPU只有0.2%其实是没把单位换算看对。同样的情况也出现在一些自定义指标上。我建议在代码里记录指标时就想清楚这个值的语义和范围并在Grafana面板的description里写明单位不然过了一个月再看面板自己都会忘记这个指标到底是0.0-1.0还是0-100。7. 常见坑和进阶思路这些经验花了不止一个晚上7.1 三个反复出现的接入坑运行一段时间之后我总结出几个出现频率极高的坑坑一重复采集导致指标翻倍如果Prometheus同时配置了两个job指向同一个SpringBoot实例比如一个通过服务发现自动发现一个通过static_configs手动指定那么数据会被采集两次查询结果翻倍。排查方法很直接看Prometheus的/targets页面里是否同一个实例出现多行。坑二标签不一致导致查询结果永远为空Prometheus的标签必须完全匹配才可能查到数据。比如你在prometheus.yml里给job加了自定义标签envprod但你在Grafana面板里写查询条件{envproduction}死活查不到数据因为标签值对不上。这种问题排查起来特别憋屈因为日志和配置都没报错。坑三健康检查指标被误用为性能指标有人拿health端点是否UP来判断服务性能这完全不对。health只反映“能不能提供基本服务”应用可能已经开始响应缓慢、错误率飙升但health仍然返回UP。真正反映性能的是QPS、P99、错误率这些指标它们和健康检查是两套体系。7.2 进阶从“有监控”到“告警语义正确”接入工作做完之后真正考验团队的是告警语义的设计。我见过很多团队的告警规则看起来没问题但实际效果很差告警阈值拍脑袋定的比如错误率超过5%就告警实际业务平时错误率就在3%-4%徘徊于是系统每天都在发无意义的告警。告警规则没有区分白天和夜间白天高峰期CPU 70%正常夜间低峰期CPU 70%可能就是有异常任务在跑。告警恢复条件没有单独设置导致问题恢复后短时间内反复告警。我的建议是先收集两周的基线数据看看正常运行时各个指标的分布范围再把告警阈值设置在基线之上留足余量。告警规则上线后前两周要重点观察误报率及时调整。这点真的非常重要因为一个误报率太高的监控系统到最后一定会变成“狼来了”——没人会真正关注告警通知。7.3 扩展思路这套方案能怎么继续延伸SpringBoot集成Prometheus只是服务监控的第一步。方向跑通了之后还可以继续扩展接入多个实例把同一个服务的多副本实例都加入Prometheus抓取根据instance标签做聚合查询从单机视角升级到集群视角。服务自动发现在Kubernetes环境里使用K8s的服务发现机制Pod滚动更新时Prometheus能自动找到新的目标不用手动维护targets列表。业务指标监控不只是技术指标把订单量、支付成功量、用户注册量这些业务核心指标也接入监控让技术团队和业务团队用同一块屏幕对齐视角。聚合多个服务把网关、订单、支付、库存等微服务统一接入Prometheus用application标签区分服务做跨服务维度的联调排查。我个人在实际操作中的体会是监控系统的初期建设最怕贪多求全先把一个服务的核心链路完整跑通再横向扩展复制比一开始就铺到所有服务要可控得多。等到整套链路稳定了再有节奏地推广到其他团队这样每个接入方得到的都是一套被验证过的方案而不是一堆半成品配置。最后补充一个小技巧我刚接入完的时候习惯每天在Grafana上看看几个核心接口的P99曲线连续看了一周对服务的“正常形态”有了直观印象。后来线上出过一次数据库连接池耗尽的问题我扫一眼曲线就发现hikaricp_connections_waiting的峰值和响应时间飙升的时间点完全吻合排查方向一下就锁定了。这种对数据和指标的感觉比写再多的告警规则都管用。