Spring Boot应用监控实战:Actuator + Micrometer + Prometheus + Grafana

📅 发布时间:2026/10/15 2:13:03
Spring Boot应用监控实战:Actuator + Micrometer + Prometheus + Grafana
做后端开发这些年我有个特别深的感受服务能跑起来和“知道它正在怎么跑”完全是两回事。接口返回 200 不代表没有隐患CPU 看似空闲不代表没有内存泄漏很多问题都是等用户先发现然后我们才开始手忙脚乱翻日志。所以这期按系列进度聊一聊应用监控而且是 Spring Boot 项目最顺手的一整套组合Actuator 做端点暴露Micrometer 做指标采集Prometheus 做存储与抓取Grafana 做可视化展示。这套方案上手成本不高收益却非常直接适合正在维护微服务、或者想把单体服务状态摸清楚的同学参考。先说个真实感受我以前也以为监控就是“加个依赖看几个数字”后来真正排过几次线上问题才明白监控系统要回答的本质问题是——服务现在处于什么状态状态是否在恶化如果出问题能不能快速定位到方向带着这几个问题去看 Actuator、指标、面板才不会把监控做成只是“好看的大屏”。这篇文章不会停留在“引入依赖、打开几个端点”的程度我会把端点的含义、指标的分类、生产环境下的安全与性能坑都过一遍尽量让你看完就能照着搭出一套能真正辅助定位问题的监控体系。1. 应用监控到底要解决什么问题1.1 从“服务还活着”到“服务状态可感知”新手阶段我们判断服务是否正常最常用的办法是 curl 一下接口能返回数据就认为没问题。这种方式属于典型的“黑盒监控”只关注外部表现不知道服务内部发生了什么。问题是接口能通可能只是 Web 容器线程还没被耗尽但数据库连接池已经快被占满可能 HTTP 请求处理正常但 GC 频繁导致停顿飙升只是还没达到超时阈值。Spring Boot Actuator 解决的就是“白盒监控”的问题。它把应用内部的状态通过标准化端点暴露出来比如健康状态、内存用量、线程状态、配置信息、日志级别等。我们可以通过这些信息感知到服务“活得怎么样”而不仅仅是“是不是活着”。这是应用监控的第一个价值把不可见的东西变成可观测的数据。我自己搭建监控体系时通常会先问自己一个问题如果这个服务明天凌晨三点挂了我希望监控系统在挂掉之前的几个小时能给我什么线索答案通常是内存是否持续增长、GC 是否异常频繁、线程数是否突破预期、连接池是否耗尽。而这些数据Actuator 基本都能提供基础指标Prometheus 负责记录趋势Grafana 负责呈现曲线。1.2 监控的三个层次健康检查、指标采集、链路追踪完整应用监控可以拆成三个层次理解这三个层次才能避免把监控做成“单点工具”。第一层是健康检查回答“服务活着吗”。这一层最基础通过定期探测 Actuator 的 /actuator/health 端点能知道进程是否存活、依赖的组件是否可用比如数据库、Redis、消息队列。很多容器编排平台比如 Kubernetes就是靠这个端点做存活和就绪探针的。第二层是指标采集回答“服务状态如何”。这就是 Micrometer Prometheus 的强项了。除 JVM 基础指标外还可以自定义业务指标比如订单创建速率、积压的任务数量、外部接口调用耗时分布。有了时序数据才能看到状态变化的趋势。第三层是链路追踪回答“一次请求经历了什么”。这个通常要引入分布式链路追踪系统比如 OpenTelemetry Zipkin/Jaeger 的组合能还原一次调用在各个服务之间的耗时和依赖关系。这不是本文的重点但你需要知道监控体系里它是独立且重要的一块。很多人做监控只盯着第二层把大量精力花在“指标多不多”上却忽视了第一层。我的建议是先把健康检查做扎实再逐步完善指标链路追踪按团队实际需要引入不要一上来就铺大摊子。2. Spring Boot Actuator 核心端点拆解2.1 引入依赖与最小配置在 Spring Boot 项目中使用 Actuator最直接的方式是添加 spring-boot-starter-actuator 依赖。以 Maven 为例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency如果你的项目是 Spring Boot 2.x 或 3.x只要 spring-boot-starter-parent 管理好了版本号这里不需要指定版本。需要注意的是光加依赖还不能访问所有端点因为 Actuator 默认只暴露 health 等少数几个端点。老版本曾经暴露得过宽后来官方把默认策略改成了只暴露 health这个改动非常正确也提醒我们暴露端点本身就是一种风险面能少暴露就少暴露。在 application.yml 里我习惯做这样一份最小配置management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: tags: application: ${spring.application.name}这里 include 配置了需要暴露的端点。如果用的是 Spring Boot 2.x需要额外引入 micrometer-registry-prometheus 才能让 prometheus 端点生效后面会讲。加 metrics 这个端点是为了开发时快速查看当前应用有哪些指标生产环境看情况建议还是在 Prometheus 里看。启动应用后访问 http://localhost:8080/actuator/health正常情况会返回类似下面这样{ status: UP, components: { diskSpace: { status: UP, details: { total: 500107264000, free: 322075525120, threshold: 10485760, exists: true } }, ping: { status: UP } } }注意这里的 status 不是指你的业务接口是否正常而是指 Spring Boot 内部所有健康指示器的综合结果。只要有任何一个关键依赖处于 DOWN整体状态就会变成 DOWN这个设计很合理。2.2 health 端点与自定义 HealthIndicatorActuator 的健康检查是分组件判定的。数据库连接、Redis、Elasticsearch、RabbitMQ 等只要 classpath 里有对应的客户端依赖Spring Boot 就会自动注册相应的 HealthIndicator。这也是为什么健康检查能覆盖到“数据库是否可用”的原因。但它只能检查“它认识的组件”如果我们的服务强依赖某个外部接口或本地文件目录就需要自己写健康检查逻辑。实现方式很简单实现 HealthIndicator 接口并在应用上下文中注册为 Bean。Component public class ExternalOrderServiceHealthIndicator implements HealthIndicator { private final RestTemplate restTemplate new RestTemplate(); Override public Health health() { try { ResponseEntityString response restTemplate.exchange( https://example-order-api/health, HttpMethod.GET, null, String.class ); if (response.getStatusCode().is2xxSuccessful()) { return Health.up() .withDetail(externalOrderService, available) .build(); } return Health.down() .withDetail(externalOrderService, unexpected status: response.getStatusCode()) .build(); } catch (Exception e) { return Health.down(e).build(); } } }这样在 /actuator/health 的返回结果里就会多出一个 externalOrderService 节点如果它挂了整体状态会变成 DOWN编排平台会据此重启实例或者摘除流量。这里有个很容易踩的坑健康检查里的网络调用必须设置超时时间否则一旦外部服务一直不响应健康检查端点本身就会卡住反而不健康了。RestTemplate 默认没有超时限制建议显式配置连接超时和读取超时。另外Spring Boot 2.3 支持按“可用性组”暴露不同的健康信息比如把 liveness 和 readiness 拆开这个在容器环境特别实用放到后面单独说。2.3 常用的其他端点除了 health实战场上我经常用的端点还有这么几个端点路径主要作用使用场景/actuator/info展示自定义的应用信息查看版本号、Git commit、构建时间/actuator/metrics查看应用已采集的所有指标名排查某个指标是否存在/actuator/metrics/{name}查看某个指标的详细数值查看具体 JVM 指标/actuator/loggers查看和修改日志级别线上临时把某个包调成 DEBUG/actuator/env查看环境配置确认某个配置项的生效值/actuator/beans列出所有 Bean排查 Bean 冲突、确认是否注册/actuator/threaddump导出线程快照排查线程阻塞、死锁问题/actuator/heapdump导出堆内存快照分析内存泄漏我最常用的是 loggers 和 threaddump。loggers 可以动态调整日志级别不需要重启应用排查问题时非常方便。相关用法是 POST 请求 /actuator/loggers/{package}请求体构造如下{ configuredLevel: DEBUG }threaddump 则适合在服务明显卡顿或 CPU 飙高时使用。导出的线程快照可以直接用分析工具查看各线程的状态和锁信息能快速看出是否有线程长时间卡在某个锁上。这里要提醒一点不要把 /actuator/env 和 /actuator/threaddump 这类信息敏感或存在性能消耗的端点直接暴露到公网。env 端点可能泄露配置项threaddump 短时间大量访问会带来额外开销。建议只在内网使用或者通过 Spring Security 做访问控制。3. 用 Micrometer 把指标做细3.1 Micrometer 是谁为什么选它Spring Boot 的指标采集并不是直接写给 Prometheus 的而是通过 Micrometer 这个门面层。打个比方Micrometer 像是日志领域的 SLF4J应用里只面向它写指标至于指标最终是给 Prometheus、InfluxDB 还是云端监控由具体的 registry 实现决定。这个设计带来的好处是你的业务代码不依赖某个具体的监控系统。以后如果团队从 Prometheus 换成其他时序数据库业务层的指标代码不用改只需要替换依赖和配置。Micrometer 定义了四种最核心的计量器MeterCounter 计数、Gauge 当前值、Timer 耗时分布、DistributionSummary 数值分布。理解这四种类型基本就能覆盖绝大多数业务监控需求。Counter 只增不减适合统计累计值比如订单总数、请求总数。Gauge 表示当前快照值可以上下波动比如当前线程数、当前队列大小。Timer 用来记录事件耗时同时统计次数和耗时分布比如接口响应时间。DistributionSummary 用来记录一组数值的分布比如订单金额分布。3.2 自定义业务指标一个电商下单场景理论讲再多不如写一段代码。假设我们有个简单的电商下单服务需要监控三个点下单总次数、当前排队处理的订单数、一次下单全流程的耗时。先注入 MeterRegistry然后在服务中使用Service public class OrderService { private final Counter orderCreateCounter; private final AtomicInteger pendingOrders new AtomicInteger(0); private final Timer orderCreateTimer; public OrderService(MeterRegistry registry) { this.orderCreateCounter Counter.builder(order.create.total) .description(Total number of created orders) .tag(channel, api) .register(registry); this.pendingOrders registry.gauge(order.pending, pendingOrders); this.orderCreateTimer Timer.builder(order.create.duration) .description(Time taken to create an order) .publishPercentileHistogram() .register(registry); } public void createOrder(Order order) { // 模拟排队处理 pendingOrders.incrementAndGet(); Timer.Sample sample Timer.start(); try { // 实际的创建订单逻辑 Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { sample.stop(orderCreateTimer); pendingOrders.decrementAndGet(); } orderCreateCounter.increment(); } }这里有个细节值得展开Gauge 的注册方式。我上面用的是registry.gauge(order.pending, pendingOrders)Micrometer 会直接通过反射拿到 AtomicInteger 的数值每次 Prometheus 来抓取时获取的是“当时”的值。Gauge 不应该在业务代码里手动 set因为它的语义就是“当前状态由外部对象决定”而不是由你每次累加决定。再来看 Timer。我使用了publishPercentileHistogram()这会生成基于直方图的百分位数指标Granafa 里可以用histogram_quantile计算出 P95、P99 耗时。如果不调用这个方法Timer 默认只记录 count 和 totalTime看不到耗时分布监控价值大大降低。启动应用并暴露 prometheus 端点后我们就能在抓取结果里看到类似这样的指标order_create_total{applicationorder-service,channelapi} 3.0 order_pending{applicationorder-service} 2.0 order_create_duration_seconds_count{applicationorder-service} 3.0 order_create_duration_seconds_sum{applicationorder-service} 0.187注意 Micrometer 会把驼峰命名的指标转换成下划线命名为了统一规范业务指标名最好从一开始就按下划线风格取避免出现 order.create.duration 被翻译成 order_create_duration 这种被迫转换的情况虽然能用但读起来没那么顺。3.3 命名与标签的艺术指标系统里有一句话“指标名是变量名标签是维度。”好的标签设计能让监控从“发现异常”升级到“定位异常”。举一个非常典型的反面案例有人做过一个“按用户维度统计订单量”的指标于是给 Counter 加了个 taguserId。看起来很有道理每个用户下单都能查到。但用户量有百万级每个用户都是一个唯一的标签组合这就形成了高基数high cardinality问题。Prometheus 这类时序数据库对标签组合的数量非常敏感高基数指标会把内存和磁盘占用抬高几个数量级最终查询变慢甚至集群不稳定。正确做法是把用户维度放到日志里或者单独存到业务库做统计而不是放进 Prometheus 指标。监控指标更适合聚合不适合精确到某个业务对象。另外一个常见问题是标签命名不一致。比如有的地方写app有的写application有的写service导致 Grafana 查询时经常写错标签名。我的习惯是在项目初期就约定一套公共标签application 表示服务名、instance 表示实例标识、env 表示环境dev/test/prod。通过配置统一注入management: metrics: tags: application: ${spring.application.name} env: ${spring.profiles.active:default}这样所有指标都会自带 application 和 env 标签在做跨服务对比时非常方便。4. 与 Prometheus 和 Grafana 集成4.1 接入 Prometheus 端点Actuator 默认并不能直接输出 Prometheus 格式的数据需要添加 Micrometer 的 Prometheus registry 依赖dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependencySpring Boot 检测到这个依赖后会自动注册一个类型为 PrometheusMeterRegistry 的 Bean并启用 /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,idPS Eden Space,} 2.58837064E8这个文本格式就是 Prometheus 的抓取格式每行一个样本包含指标名、标签和时间戳通常省略。开发模式下可以用 curl 快速确认端点是否正常curl -s http://localhost:8080/actuator/prometheus | grep -i order_create_total如果能看到上面自定义的指标说明整个链路已经通了。4.2 配置 Prometheus 抓取Prometheus 本身的部署不多赘述重点是采集配置。一个最简的 prometheus.yml 片段长这样scrape_configs: - job_name: spring-boot-apps metrics_path: /actuator/prometheus static_configs: - targets: - localhost:8080这里的 metrics_path 必须和 Spring Boot 暴露的路径保持一致。如果应用配置了management.endpoints.web.base-path路径会相应变化。比如你设了management: endpoints: web: base-path: /manage那么 Prometheus 里的 metrics_path 就要改成 /manage/prometheus否则抓取会返回 404。这是集成时最容易犯的错真遇到了别急着怀疑配置先 curl 一下实际路径。配置好之后访问 Prometheus 的 Targets 页面正常情况下能看到 Spring Boot 应用的实例状态为 UP。若状态是 DOWN大概率是网络不通、端口未监听、或路径不对。看抓取报错时Prometheus 页面会给出响应状态码和错误信息排查效率会高很多。4.3 在 Grafana 里搭一块面板Prometheus 负责存储和查询展示则交给 Grafana。建好 Prometheus 数据源之后就可以写查询表达式做面板了。最常用的是 JVM 内存曲线sum(jvm_memory_used_bytes) by (area)这个表达式把所有内存区域的使用量按 area 聚合能直观看到堆内存和非堆内存的变化。配合 jvm_memory_max_bytes 一起看能发现内存是否接近上限。接口响应耗时是另一个核心指标。Spring Boot 默认通过 Micrometer 采集 http.server.requests 指标包含耗时和 HTTP 状态码。在 Grafana 里可以用 PromQL 计算 P95histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le))这里依赖 http_server_requests_seconds_bucket 这个直方图桶数据。如果面板显示无数据先确认指标名是否一致。不同 Spring Boot 版本和 Micrometer 版本之间指标名的前缀可能不同比如有的版本会带_seconds_*后缀有的则不是建议先在 Prometheus 的 Graph 页面敲一个 http_server 看看自动补全出来什么。我自己的习惯是先在 Prometheus 手工查询确认指标存在、标签正确再进 Grafana 做面板不要在 Grafana 里反复试错。毕竟 Prometheus 的查询框能直接看到返回的原始样本比 Grafana 的编辑器直观得多。5. 监控落地时的常见坑5.1 端点暴露安全控制Actuator 好用但也是攻击面。早些年很多团队图方便直接把所有端点暴露出去结果 /actuator/env 和 /actuator/heapdump 被恶意利用带来了不少安全事件。生产环境我建议采用以下策略通过 include 精确列出需要暴露的端点而不是用通配符 *。所有端点不要直接暴露到公网放在内网或通过网关统一管控。如果必须暴露到公网用 Spring Security 做 Basic Auth 或更高级的认证。独立设置 management 端口例如 9090避免监控端口和业务端口混在一起。配置示例management: server: port: 9090 endpoints: web: exposure: include: health,info,prometheus这样监控端点只在 9090 端口上提供服务与业务端口隔离外部网络策略也容易针对单独端口做限制。注意管理端口独立后Prometheus 的抓取 target 端口也要改成 9090。5.2 高基数标签与指标爆炸上一节提到过按 userId 打标签的案例这里再展开讲一下“指标爆炸”的另一个表现动态 tag 的使用。有的业务代码会从请求参数里拿出一个值直接塞进 tag比如接口名如果这个接口名本身是用户可控的那就可能产生无限增长的标签组合比固定标签危害更大。应对思路并不复杂先问这个标签有没有上限持续增长率是多少。标签值承载的如果是反序列化后的对象 ID、随机字符串、URL 路径中的参数基本都要排除。需要按动态维度统计时优先考虑用固定枚举值如状态码、渠道、类型做标签。对于确实要追踪的高基数维度另建一张表或使用日志链路不在 Prometheus 里硬扛。如果你的 Prometheus 内存已经告警第一步不是加机器而是排查哪些指标拥有不合理的标签组合。在 Prometheus 页面执行topk(10, count by (__name__) (up))这类查询很难直接定位更实用的办法是在 Grafana 里按指标名的样本数量排出 Top先找到那些“变种”特别多的高基数源头。5.3 容器环境里的健康检查探针如果把应用部署到 Kubernetes 里健康检查就不能只图“接口返回 200 就算过”。K8s 的探针分为 livenessProbe存活探针和 readinessProbe就绪探针。liveness 探测失败会重启容器readiness 探测失败会把 Pod 从 Service 的端点列表里摘除不再转发新流量。Spring Boot 2.3 开始支持把 health 端点分成两组liveness 和 readiness通过可用性组Availability Groups实现。配置如下management: endpoint: health: group: liveness: include: ping readiness: include: ping,diskSpace,db然后在 K8s 部署文件里分别配置livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 120 periodSeconds: 30 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 periodSeconds: 5为什么要区分这两类因为有些依赖出问题应用并不需要重启而是不应该继续接收新流量。比如数据库连接池耗尽重启应用未必能立刻恢复更合适的做法是先把实例摘除等依赖恢复后再重新接收流量。如果只用同一个 health 端点做 liveness数据库抖动时容器会被频繁重启反而加剧问题。我自己还吃过一个教训livenessProbe 的 initialDelaySeconds 太短。服务启动时如果初始化逻辑比较重前几次探针没就绪就被判定失败容器反复重启形成“启动-被杀-再启动”的循环。所以宁可把初始延迟调大一点也不要让探针过早介入。5.4 多实例与全局监控思路当服务从单实例扩展到多实例后Prometheus 抓到的指标会带 instance 标签同一个指标会有多条时间序列。比如 3 个实例的 order-servicejvm_memory_used_bytes 会有 3 条数据分别对应不同 instance。这带来的第一个问题是看面板时如果不聚合曲线会密密麻麻看不过来。此时应该先用 sum 或 avg 聚合再按 application 分组展示sum(jvm_memory_used_bytes) by (application)第二个问题是单个实例的异常可能被聚合曲线平滑掉。比如 3 个实例中 1 个内存飙升平均曲线可能只是缓慢上升。所以面板上既要保留总览视图也要保留单实例视图或者对单实例指标单独配置告警。多实例场景下Prometheus 的抓取配置也有两种思路静态写死 targets或通过服务发现动态发现实例。静态配置适合实例数量少且不变的环境一旦实例动态扩缩容就得靠服务发现如 Kubernetes 的 pod 发现机制来保证新实例自动被加入抓取范围。6. 踩过这些坑之后的几点建议如果让我带着现在的经验重新搭一套监控我会把顺序调整成这样先配好 health 端点和容器探针保证“死没死”能被准确感知再接入 Prometheus确认 JVM 基础指标能抓到然后才加业务指标而且每次加新指标都要过一遍“标签枚举值有限吗、周期内样本量可预估吗”两个问题最后再做 Grafana 面板和告警。告警规则也别一开始就铺很多先挑最核心的几类进程健康状态、JVM 堆内存使用率、接口 P95 耗时、数据库连接池活跃数。告警的意义不是轰炸而是在问题实际影响用户之前给出信号。建议每个指标都带上可执行的动作说明比如收到”活跃连接数突增“你第一时间应该去看慢查询、看线程栈而不是先去开会。还有一个常被忽略的细节监控配置应该跟着服务走纳入版本管理。application.yml 里那段 management 配置、prometheus 的抓取配置、Grafana 面板的 JSON都是可维护的资产。我之前吃过亏面板在测试环境手动调好了上线前忘了同步配置结果生产环境监控是“半瞎”状态这个教训挺深刻的。最后说一句体会应用监控不是一个“做完就结束”的功能它更像一套需要持续校准的仪表盘。指标会随业务变化而失去意义面板会随架构调整而需要更新但它给你带来的价值往往体现在某次凌晨的告警里——你能早于用户发现问题并且知道该往哪个方向查。这个感觉比任何指标数字都更实在。