
做了这么多年后端开发我越来越确定一件事监控系统不是上线以后再补的而是写代码的时候就要想清楚的。过去我经历过不少次线上事故CPU、内存、磁盘这些基础设施指标看着都正常但业务就是不对——订单量悄悄掉了没人发现某个接口延迟越来越高但系统资源却毫无波澜。后来我把Spring Boot应用接入了Prometheus和Grafana用自定义指标把业务状态彻底透明化才真正解决了资源正常但业务出问题这种最磨人的故障。这篇文章就围绕Spring Boot整合PrometheusGrafana做自定义指标监控从选型思路、环境搭建、指标原理到实际埋点、面板展示和告警配置完整走一遍。我用的都是实际踩过坑之后落定的方案代码可以直接抄配置直接复制就能跑起来。1. 为什么业务监控不能只靠系统资源指标1.1 一次深夜事故给我的教训先讲一个真实发生在我项目里的案例。某个核心订单服务的实例当时故障表现是接口不报错但订单创建成功率直线下降。我收到告警时先看CPU、内存——CPU 30%内存用了不到60%磁盘IO也很平稳一切看起来都很健康。可实际上前端反馈用户下单总失败。我排查了将近一小时最后发现是某个下游服务的连接池被占满导致新请求全部卡在等待连接的状态接口RT飙升但服务本身资源占用并不高。如果当时我监控了订单创建耗时这个业务指标第一眼就能看到RT异常再顺着链路去找原因整个过程用不了五分钟。这次事故之后我的监控理念彻底变了系统资源指标是底层保障业务指标才是真实效果。你上线一个订单服务最终好不好用不是看CPU多少而是看订单能不能成功创建、耗时多少、积压了多少。1.2 基础设施指标和自定义业务指标的本质区别系统资源指标通常指CPU、内存、磁盘、网络等它们描述的是机器状态。而自定义业务指标描述的是业务健康度两者分层明显维度系统资源指标自定义业务指标关注对象服务器、容器、中间件业务流程、用户行为、服务质量代表指标CPU使用率、内存使用率、磁盘IO订单成功率、下单耗时、积压消息数故障表现机器过载时才异常业务出问题时可能资源一切正常告警时机资源告警往往滞后业务指标告警更接近真实故障时刻说白一点系统资源指标是消防车业务自定义指标是火灾探测器。你当然不能没有消防车但真正帮你第一时间发现火灾的还是探测器。1.3 什么场景适合用自定义指标监控经验来看下面几类场景特别值得做自定义指标核心业务链路订单创建、支付回调、消息消费这些是命脉指标必须实时盯。外部依赖稳定性下游接口调用量、调用耗时、失败次数能快速判断是谁拖慢了整体链路。队列与异步任务消息积压数量、消费速率、重试次数是排查削峰填谷问题的高频切入点。特殊业务时段比如秒杀活动、抢购场景瞬时流量和峰值吞吐需要更细粒度的指标来观测。这些场景光靠Spring Boot默认暴露的health、info这些端点远远不够必须自己定义指标让Prometheus采集到有业务语义的数据。2. 技术选型为什么是PrometheusGrafana这套组合2.1 主流监控方案横评市面上可用的监控方案不少但真正适合Spring Boot应用自定义业务指标这个场景的我筛下来也就几个方案优点缺点适合场景Prometheus Grafana云原生标准、Pull模型、社区生态强、告警灵活不支持复杂长时间存储需要额外组件做持久化微服务、K8s、自定义指标丰富的场景Zabbix部署简单、模板丰富指标模型偏基础设施自定义业务指标比较笨重传统IT设备监控SkyWalking分布式链路追踪强自带拓扑图指标监控能力相对弱重链路不重度量APM专项场景开源时序库自研灵活度高投入大、维护成本高超大团队才会考虑我最终选择PrometheusGrafana的核心原因有三个Pull模型更契合微服务架构。每个Spring Boot应用暴露一个/actuator/prometheus端点Prometheus定期拉取服务和监控解耦新服务注册其实就是加一行配置。指标模型强大。标签Label体系让同一个指标可以按各种维度接口、方法、状态码、区域切片分析。Grafana的生态太成熟了。面板社区里有大量开箱即用的模板截图汇报、告警渲染、多数据源联动都很顺手。2.2 Pull模型和Push模型的哲学差异很多刚接触监控的读者容易疑惑为什么Prometheus不去收集数据而是让应用暴露数据等它来拉打个比方Push模型就像快递员上门取件每个应用主动把数据送到监控中心Pull模型则是监控中心定时到每个应用家门口取件。Pull模型的好处是——监控系统不会成为消息堆积的牺牲品也不会因为某个应用突发巨量数据而被冲垮。另外Pull模型天然能检测应用是否活着拉不到数据就说明应用挂了这本身就是一种指标。当然Pull模型也有缺点如果应用和Prometheus网络隔离或者应用是短生命周期任务批处理作业Pull就不方便。这种场景可以用Pushgateway做补充但常规Spring Boot服务基本都是长驻进程Pull完全够用。2.3 时间序列模型带来的统计计算便利Prometheus底层存储的是时间序列数据每个序列由指标名标签集唯一标识值随时间不断累加。这种模型的强大之处在于做统计分析不需要原始明细数据直接在时间序列上就能完成计算每秒订单创建速率直接用rate(order_created_total[5m])计算接口95分位延迟直接用histogram_quantile(0.95, sum(rate(order_process_seconds_bucket[5m])) by (le))计算积压订单变化趋势直接用delta(order_backlog[10m])这些函数在PromQL里都是内置的不用我们写任何代码。对比传统的SQL聚合或者自己写统计逻辑效率差距不是一星半点。3. 环境搭建Spring Boot应用接入Actuator和Micrometer3.1 pom.xml依赖引入现在Spring Boot和Prometheus的整合已经非常顺滑核心角色是Micrometer——它是JVM生态的指标门面框架类似SLF4J之于日志。Micrometer定义了一套统一指标模型Prometheus只是它的一个后端实现。我这边用的是Spring Boot 2.x做过好几个版本适配最稳定的依赖组合如下dependencies !-- Spring Boot Actuator端点暴露 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency !-- Micrometer Prometheus 注册器 -- dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency /dependencies不用手动指定Micrometer的版本号Spring Boot的dependency management会自动选一个兼容的版本。如果你用的是Spring Boot 3.x引入方式一模一样只是底层换成了Micrometer 1.x以上的版本API基本兼容。3.2 核心配置项与暴露策略引入依赖之后要在application.yml里把Prometheus的端点暴露出来。这里有一个新手经常掉的坑Actuator默认只暴露health和info两个端点你如果不显式配置/actuator/prometheus会一直返回404。management: endpoints: web: exposure: include: health,info,prometheus,metrics endpoint: health: show-details: always metrics: export: prometheus: enabled: true tags: application: order-service几个比较关键的点include: health,info,prometheus,metrics这里面prometheus是必须的metrics方便你调试查看JVM等默认指标。management.metrics.tags.application会给所有指标统一加上applicationorder-service这个标签。这是我在多服务混部场景下强烈建议加的否则Grafana面板里多个服务的指标会纠缠在一起无法按服务过滤。如果你用的是Spring Boot 3.x部分配置路径可能略有调整但整体结构一致。3.3 验证Spring Boot应用是否成功暴露指标启动应用后先访问http://localhost:8080/actuator/prometheus浏览器里如果能看到一大片以# HELP和# TYPE开头的文本说明指标暴露已经成功了。比如# HELP jvm_memory_used_bytes The amount of used memory # TYPE jvm_memory_used_bytes gauge jvm_memory_used_bytes{areaheap,applicationorder-service,idG1 Survivor Space,} 524288.0这里面除了JVM的指标还有HikariCP连接池的指标、Tomcat线程池指标、Spring MVC的请求耗时指标等等。Micrometer做了大量自动埋点工作你什么都不写就已经获得了几十上百个指标。不过这些自动指标只能反映框架层面的情况真正体现业务状态的指标还是得自己动手埋。4. 自定义指标的核心原理Micrometer指标模型与四种类型4.1 指标是怎么从业务代码流到Prometheus的理解了整条链路后面写代码就不会心存疑虑业务代码调用Micrometer API给某个指标名赋一个值或执行一次计数。Micrometer把这个指标写入内存中的寄存器MeterRegistry。Prometheus通过HTTP调用/actuator/prometheus端点Micrometer把当前内存里的指标快照按Prometheus的文本格式暴露出去。Prometheus按配置的时间间隔抓取存入自己的时序数据库。Grafana通过PromQL查询并渲染成图表。所以Micrometer是我们业务代码唯一需要接触的API你不需要直接跟Prometheus的格式打交道。4.2 Counter、Gauge、Timer、Histogram四种指标类型Micrometer提供的主要指标类型有四种理解它们的语义差别是设计好自定义指标的前提。Counter计数器只能单调递增适合统计发生了多少次和总量累计。比如订单创建总数、支付成功总数、请求失败次数。Counter orderCreatedCounter Counter.builder(order.created.total) .description(累计创建订单数) .tag(channel, app) .register(meterRegistry);Gauge仪表盘可增可减适合表示当前状态值比如当前积压订单数、当前在线用户数、线程池活跃线程数。Gauge不需要手动设置新值而是由Micrometer在采集时拉取你提供的Supplier。AtomicInteger backlogCount new AtomicInteger(); Gauge.builder(order.backlog, backlogCount, AtomicInteger::get) .description(当前积压订单数) .register(meterRegistry);Timer计时器专门统计耗时分布。内部会记录调用次数、总耗时、最大耗时并配合Histogram直方图计算耗时分布。比如下单处理耗时用Timer最合适。Timer orderProcessTimer Timer.builder(order.process.time) .description(下单处理耗时) .publishPercentileHistogram() .register(meterRegistry); long start System.nanoTime(); try { // 业务逻辑 } finally { orderProcessTimer.record(Duration.ofNanos(System.nanoTime() - start)); }Histogram直方图更精确地观测数据分布比如订单金额分布、请求大小分布。它按你预设的桶bucket记录落入每个区间的次数。DistributionSummary orderAmountSummary DistributionSummary.builder(order.amount) .description(订单金额分布) .baseUnit(yuan) .minimumExpectedValue(1.0) .maximumExpectedValue(10000.0) .publishPercentileHistogram() .register(meterRegistry);这里面的关键是Counter和Gauge解决多少的问题Timer和Histogram解决多慢和分布如何的问题。一个完整的业务监控系统这四类都会用到。4.3 标签Tag是自定义指标的灵魂标签是Prometheus最有价值的设计之一。比如同样是订单创建总数你可以打上channel标签区分App、小程序、H5打上region标签区分华东、华北打上status标签区分成功、失败。我的经验是标签要克制尽量控制在3~5个以内。因为Prometheus里每个标签值都是一个独立的时间序列标签维度爆炸会让存储压力和查询性能都显著上升。比如一张订单表有几十个状态字段你不可能全做成标签只挑你最经常做分组统计的维度做标签其他信息留在日志里即可。5. 实战给订单服务制作一套完整的业务监控指标5.1 定义一个专门封装指标的服务类我习惯的做法是建一个专门的OrderMetricsService把跟监控指标相关的代码全部收敛起来。这样做的好处是埋点代码不会散落在业务代码各处后续要删要改都方便。Component public class OrderMetricsService { private final MeterRegistry meterRegistry; private final Counter orderCreatedTotal; private final Counter orderFailedTotal; private final AtomicInteger orderBacklog; private final Timer orderProcessTimer; private final DistributionSummary orderAmountSummary; public OrderMetricsService(MeterRegistry meterRegistry) { this.meterRegistry meterRegistry; this.orderCreatedTotal Counter.builder(order.created.total) .description(累计创建订单数) .tag(type, normal) .register(meterRegistry); this.orderFailedTotal Counter.builder(order.failed.total) .description(累计创建失败订单数) .tag(type, normal) .register(meterRegistry); this.orderBacklog new AtomicInteger(0); Gauge.builder(order.backlog, orderBacklog, AtomicInteger::get) .description(当前积压待处理订单数) .register(meterRegistry); this.orderProcessTimer Timer.builder(order.process.time) .description(订单处理耗时) .publishPercentileHistogram() .serviceLevelObjectives(Duration.ofMillis(100), Duration.ofMillis(300), Duration.ofMillis(500)) .register(meterRegistry); this.orderAmountSummary DistributionSummary.builder(order.amount) .description(订单金额分布) .baseUnit(yuan) .minimumExpectedValue(10.0) .maximumExpectedValue(5000.0) .publishPercentileHistogram() .register(meterRegistry); } public void recordOrderCreated() { orderCreatedTotal.increment(); } public void recordOrderFailed() { orderFailedTotal.increment(); } public void incrementBacklog() { orderBacklog.incrementAndGet(); } public void decrementBacklog() { orderBacklog.decrementAndGet(); } public void recordProcessTime(Duration duration) { orderProcessTimer.record(duration); } public void recordOrderAmount(double amount) { orderAmountSummary.record(amount); } }我特别说一下serviceLevelObjectives这个配置。它让你自定义Timer的桶范围比如100ms、300ms、500ms。不配置也可以Micrometer默认会生成很多桶但那种桶的数量很多存储开销更大。根据自己的业务耗时预期定制3~5个有业务意义的桶比默认可视化效果好得多。5.2 在业务代码里埋点有了专门封装的服务类埋点就变得很清爽。在订单创建的主流程里加上统计逻辑Service public class OrderService { private final OrderMetricsService orderMetricsService; // 构造器注入省略 Transactional public Order createOrder(CreateOrderRequest request) { long start System.nanoTime(); try { Order order new Order(); order.setUserId(request.getUserId()); order.setAmount(request.getAmount()); // 其他业务逻辑 orderRepository.save(order); orderMetricsService.recordOrderCreated(); orderMetricsService.recordOrderAmount(request.getAmount()); orderMetricsService.recordProcessTime(Duration.ofNanos(System.nanoTime() - start)); // 订单创建后积压待处理数增加 orderMetricsService.incrementBacklog(); return order; } catch (Exception e) { orderMetricsService.recordOrderFailed(); throw e; } } public void processOrder(Long orderId) { try { // 处理订单逻辑 Thread.sleep(200); } finally { // 处理完成积压数减少 orderMetricsService.decrementBacklog(); } } }有几个复用点值得说明recordProcessTime放在finally或者在正常分支里执行都没问题但如果方法里有多个return路径最好用try-finally包住时间统计否则容易漏记某些路径的耗时。积压数用AtomicInteger配合Gauge天然适合并发环境下的增减。失败次数Counter放在catch块里既统计了失败总数又不会影响正常流程。5.3 看板核心指标一步步验证埋完代码重启应用再用浏览器访问/actuator/prometheus你应该能看到类似下面的内容# HELP order_created_total 累计创建订单数 # TYPE order_created_total counter order_created_total{applicationorder-service,typenormal,} 12.0 # HELP order_process_time_seconds 订单处理耗时 # TYPE order_process_time_seconds histogram order_process_time_seconds_bucket{applicationorder-service,le0.1,} 5.0 order_process_time_seconds_bucket{applicationorder-service,le0.3,} 8.0 order_process_time_seconds_bucket{applicationorder-service,le0.5,} 9.0 order_process_time_seconds_bucket{applicationorder-service,leInf,} 12.0 order_process_time_seconds_count{applicationorder-service,} 12.0 order_process_time_seconds_sum{applicationorder-service,} 2.347注意看Timer在Prometheus结构中是一组指标_bucket、_count、_sum。这也是为什么Prometheus能直接算P95、P99等分位数——利用histogram_quantile函数配合_bucket数据就能计算。我自己在验证时有个技巧先用浏览器直接访问端点确认格式无误再配置Prometheus抓取。如果直接跳去配Prometheus一旦抓不到数据你是无法判断是Spring Boot端的问题还是Prometheus配置问题的。6. 配置Prometheus抓取Spring Boot指标6.1 下载与基础启动Prometheus是一个独立的进程它跟Spring Boot应用没有代码耦合。我日常有两种部署方式生产环境用Docker Compose编排方便扩展。本地开发直接下载二进制包写一个prometheus.yml然后./prometheus --config.fileprometheus.yml启动。Prometheus默认监听9090端口启动后访问http://localhost:9090就能看到自带的UI可以做PromQL查询、查看Targets状态。6.2 prometheus.yml完整配置我整理了一份能直接用的配置里面已经注释了关键点global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: spring-boot-app metrics_path: /actuator/prometheus static_configs: - targets: [192.168.1.100:8080] labels: instance: order-service需要解释几个配置项scrape_interval抓取间隔。15秒对大部分业务指标都够用如果要做秒级监控可以调到5秒但要注意Prometheus存储压力会随之增加。metrics_path默认是/metricsSpring Boot Actuator的Prometheus端点是/actuator/prometheus所以这里必须显式指定。targets填Spring Boot应用的IP和端口。如果Prometheus和应用在同一个Docker网络里可以直接填服务名。evaluation_interval告警规则评估间隔和抓取间隔保持一致最省心。6.3 Targets面板确认采集状态启动Prometheus后进入Status - Targets页面如果配置正确spring-boot-app这个job的State应该是UP。如果显示DOWN80%的情况是metrics_path不对或者网络不通。我当时踩过一个比较隐蔽的坑Spring Boot应用开启了服务发现注解但没配端口映射Prometheus从宿主机抓不到容器内部的8080。排查方法很简单在Prometheus所在的主机上用curl http://应用IP:8080/actuator/prometheus先验证通不通通不过再去查网络层。7. Grafana监控面板的搭建与核心图表绘制7.1 添加Prometheus数据源Grafana安装完成之后默认端口3000第一次登录默认账号admin/admin之后会要求改密码。数据源配置很简单打开左侧齿轮图标进入Configuration - Data sources。点击Add data source选择类型Prometheus。在URL字段填Prometheus的地址比如http://192.168.1.200:9090。点击Save Test看到绿色提示说明连接成功。这里有一个常见问题Grafana是容器部署、Prometheus也是容器部署时URL不能写localhost:9090而要写Prometheus容器在Docker网络里的服务名或者宿主机IP。否则Save Test会一直报错。7.2 从零创建业务面板我比较推荐先自己手动建一两个面板搞清楚图表的基本逻辑之后再去社区里找模板导入。创建面板的具体步骤左侧加号 -Create - Dashboard - Add visualization。选择数据源Prometheus。在查询编辑器Query Editor里输入PromQL。比如最核心的订单创建速率面板PromQL写sum(rate(order_created_total[5m])) by (instance)画面板配置几个我自己常用的设置Legend设为{{instance}}让每条曲线以实例名区分。Unit选为ops/s数值以每秒次数展示。Min step设为15s与抓取间隔保持一致避免图表出现锯齿。另外订单处理耗时P95也是我最依赖的面板之一PromQL如下histogram_quantile(0.95, sum(rate(order_process_time_seconds_bucket[5m])) by (le))配合order_process_time_seconds_bucket的直方图数据这条查询能算出过去5分钟下单耗时的95分位值。看P95而不是看平均值能极大程度避免平均耗时正常但长尾请求很慢的失真情况。7.3 常见图表类型的选择心得Grafana图表类型不少但真正在业务监控里高频使用的就几种Time series时间序列图最常用适合展示指标随时间的变化趋势比如订单创建速率、处理耗时。Stat统计值只显示当前值或最近值适合展示当前积压订单数今日订单总量这种一眼要看到结果的指标。Bar chart柱状图适合对比不同标签的值比如各渠道订单量对比。Table表格适合展示明细数据比如各实例各接口的存活状态。一个面板页面里面混用这几种图表就足够覆盖90%的监控展示需求了。8. 告警规则配置与实践中的重点注意事项8.1 用Prometheus Rule设置告警Grafana本身也有告警能力但我更习惯用Prometheus的Alertmanager来做告警规则管理因为它不依赖Grafana运行更贴近基础监控的定位。在prometheus.yml所在目录建一个规则文件比如rules.ymlgroups: - name: order-service-alerts rules: - alert: OrderCreatedRateDrop expr: | sum(rate(order_created_total[10m])) by (instance) 50 for: 5m labels: severity: warning annotations: summary: 订单创建速率异常下降 description: 实例 {{ $labels.instance }} 订单创建速率最近10分钟低于50次/分钟请检查业务链路 - alert: OrderProcessTimeHigh expr: | histogram_quantile(0.95, sum(rate(order_process_time_seconds_bucket[5m])) by (le, instance)) 1.0 for: 3m labels: severity: critical annotations: summary: 订单处理耗时过高 description: 实例 {{ $labels.instance }} 的P95处理耗时超过1秒持续3分钟然后在prometheus.yml里引入这个规则文件rule_files: - rules.yml告警的核心是表达式要贴近业务感受。比如订单创建速率下降低于某个阈值时触发处理耗时超过1秒持续3分钟触发。这个阈值需要根据业务数据慢慢调切忌一开始就定一个绝对值最好先观察一周左右的实际分布再做规则。8.2 实践中总结的几个坑以下是我在Spring Boot整合Prometheus的过程中真正遇到过的坑每一个都花了不少时间才解决。第一坑指标名被加了后缀Micrometer会自动统一指标命名。比如我写order.process.time暴露到Prometheus里会变成order_process_time_seconds。这是Micrometer的命名规范——Timer追加时间单位后缀DistributionSummary追加基础单位后缀。如果你在Grafana里按原始名字查询查不到千万别慌去/actuator/prometheus看一眼实际暴露的名字再填PromQL。第二坑Counter重启归零Counter依赖内存状态应用一重启所有Counter归零趋势图上会出现断崖式下跌。如果你需要跨重启统计总量应该用sum聚合的同时配合increase函数观察增量而不是看原始总量。另外Prometheus本身对Counter的处理也会考虑重启重置场景rate和increase内部会自动修正。第三坑指标基数爆炸前文说过标签要克制。我有一次给每个请求都打上了userId标签结果Prometheus采集端开始超时——时间序列数量暴涨最后不得不推倒这部分埋点重来。标签值必须是有限的、离散的集合用户ID这种高基数标签绝对不能用。第四坑时钟漂移Prometheus对时间很敏感如果运行Prometheus的主机和运行Spring Boot应用的主机时间不一致会导致数据时间戳错位出现图表延迟甚至看不到最新数据。部署时就打个补丁所有服务器统一用NTP同步时间这是最基础也最容易被忽略的监控基建。8.3 一套可复制的完整监控流程总结下来我目前在每个Spring Boot服务上落地的监控体系是这样一个闭环接入阶段引入Actuator和Micrometer暴露prometheus端点加上application标签。埋点阶段抽取核心业务场景定义Counter、Gauge、Timer、Histogram四类指标落入专门的Metrics服务类。采集阶段Prometheus配置/actuator/prometheus抓取15秒间隔规则文件独立管理。展示阶段Grafana建好业务面板包含速率、耗时分布、积压数值、失败计数等核心图表。告警阶段从业务影响出发写PromQL告警表达式配合Alertmanager推送到即时通讯工具。复盘阶段每次故障结束后复盘告警是否在第一时间指出了真实问题可以反向推动指标和告警优化。我现在的习惯是每写一个新的业务接口顺手把跟它相关的业务指标一并埋好绝不允许上线以后再说。等真出了线上问题再去补监控那种被迫盲排的滋味体验过一次就够了。这套流程跑通之后订单链路即使再出问题我也能在两三个面板之间快速定位到真正的瓶颈而不是像最开始那样在系统资源指标里瞎转悠。