1. 为什么要做健康检查与监控:先把"监什么"和"控什么"理清楚
在Spring Boot项目上线之前,很多团队对健康检查的理解就是"服务能启动就行",对监控的理解就是"看一眼堆内存没爆就行"。但真正到了生产环境,你会发现这两个词背后牵扯的东西远比想象中多。
先说一个我踩过的真实场景:有一次凌晨两点,线上一个订单服务进程还在跑,日志也不打报错,但接口全部超时,调用方那边显示大量5xx。当时负载均衡器配的探活方式是TCP端口探测——进程活着,端口也监听着,于是流量还在源源不断打进来。结果就是用户那边体验已经崩了,我们的监控大屏上却一片绿。这就是典型的"只做了存活检查,没做就绪检查"。
健康检查和监控,本质上解决的是三件事:服务进程在不在(Liveness)、服务能不能处理请求(Readiness)、服务各项资源指标健不健康(Metrics)。很多人只做了第一件,后两件完全没做。这篇文章就是把这三件事怎么落地、用什么工具、踩过什么坑,一次性讲清楚。
Spring Boot生态里,健康检查最核心的组件是Actuator,监控指标采集最常用的是Micrometer,配合Prometheus做存储,再用Grafana做可视化,一套从"检查"到"展示"的闭环就齐了。这套组合拳也是目前市面上绝大多数Java微服务项目的标配。适合刚接手Spring Boot项目、准备给系统补监控能力,以及被线上假死问题折磨过的开发者参考,文章内容不挑框架版本,2.x和3.x都适用。
1.1 先搞清楚"假死"才是健康检查要解决的头号问题
"假死"是JAVA服务运维里最头疼的现象之一。进程没有退出,端口还在监听,线程池却全部耗尽,或者数据库连接池被占满,任何请求进来都只能排队等待,此时TCP探测依然显示"端口通",服务继续被路由到流量,情况只会越来越糟。
Spring Boot把这个问题拆成了两个维度:liveness(存活)和readiness(就绪)。存活探针回答"我还在不在运行",就绪探针回答"我能不能接收新请求"。生产环境里两者缺一不可。你可以在Actuator中通过配置把这两个探针暴露出来:
management: endpoint: health: probes: enabled: true endpoints: web: exposure: include: health配置完成后,/actuator/health/liveness和/actuator/health/readiness这两个端点就生效了。Kubernetes里可以直接拿它们当探针,传统部署模式下,负载均衡器的健康检查URL也可以指向readiness端点。注意这里有个细节:liveness返回DOWN时,大概率是进程级别的严重问题,比如磁盘只读、堆内存溢出;而readiness返回DOWN,往往只是暂时的过载或依赖故障,这时候应该摘流量而非重启进程。
用个生活化的类比:liveness是"人有没有心跳",readiness是"人能不能站起来工作"。心跳有,但不代表能干活。很多生产事故的根源,就在于把这两件事混为一谈了。
1.2 监控的三个层次:状态、指标、链路
健康检查只是监控体系的第一层。再往上走,是指标监控——JVM内存、GC频率、线程状态、HTTP接口响应时间、QPS、错误率;再往上是链路追踪——一次请求经过了哪些服务、每个环节耗时多少。这篇文章主要讲前两层,链路追踪是另一个大话题,这里不展开。
这套体系的搭建逻辑是这样的:
- 第一层:用Actuator暴露健康状态和基本信息,Answer"活没活、能不能用"。
- 第二层:用Micrometer把JVM、线程池、HTTP等指标标准化成Prometheus格式,通过
/actuator/prometheus端点暴露出去,Prometheus定时抓取存储。 - 第三层:Grafana消费Prometheus数据,把指标画成面板,配出告警阈值。
实践当中,这三层不一定非得分三个阶段做。很多人一开始只加了个Actuator,发现只能看零散的JSON数据,不舒服;后来接了个Prometheus,终于有了指标趋势图;再后来才被Grafana的可视化和告警圈粉——这个过程几乎是每个团队都会走的路径。成熟的团队通常会直接一次到位,但不影响你从理解的角度去渐进式搭建。
2. Actuator的健康检查:从默认状态到业务自定义状态
2.1 引入依赖、暴露端点一次到位
Actuator的引入方式很简单,Maven项目加一个依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>Gradle对应的写法是implementation 'org.springframework.boot:spring-boot-starter-actuator'。加完依赖,重启应用,访问/actuator就能看到一组JSON端点列表。但这里有个关键的坑:Spring Boot 2.x之后,默认只暴露了health和info两个端点,其他端点即使加了依赖也不开放,需要在配置里显式声明;Spring Boot 3.x同样延续了这个安全策略。
如果你在本地调试,想临时把常用端点全开,可以这么配:
management: endpoints: web: exposure: include: health,info,metrics,env,loggers,threaddump,heapdump但生产环境我强烈建议别全量开放,尤其是env、heapdump、shutdown这几个端点,能不开就不开。env会泄露环境变量和配置项,heapdump会直接导出堆内存快照文件,都是敏感操作。如果你用的是Spring Boot 3.x,还要多注意一个点:management.endpoints.web.exposure.include的默认行为没变,但端点路径、属性名有些微调,升级前翻一下官方迁移文档。
这里给个我实际用的生产配置模板:
management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers,threaddump endpoint: health: show-details: when-authorized probes: enabled: true loggers: enabled: true server: port: 8081注意最后那个management.server.port,把管理端点单独放到一个端口上,和业务端口隔离。好处有两个:一是监控采集系统(比如Prometheus)可以只抓管理端口,不影响正常业务流量;二是可以配合防火墙策略,只允许内网访问管理端口,减少暴露面。
2.2 自定义HealthIndicator:把业务依赖状态暴露给探针
Actuator自带的健康检查会汇总很多自动配置的健康指示器,比如数据库(DataSourceHealthIndicator)、Redis、RabbitMQ、MongoDB等。但很多业务场景里,服务的健康状态还取决于一些外部依赖,比如第三方支付接口、对象存储、某个内部RPC服务。这些依赖不在Spring Boot自动配置范围内,你需要自己写一个HealthIndicator。
自定义的写法在Spring Boot 2.x和3.x里略有差别,但核心思想一样:实现HealthIndicator接口,重写health()方法。2.x示例如下:
@Component public class CustomApiHealthIndicator implements HealthIndicator { private final RestTemplate restTemplate = new RestTemplate(); @Override public Health health() { try { ResponseEntity<String> response = restTemplate.exchange( "https://api.example.com/health", HttpMethod.GET, null, String.class ); if (response.getStatusCode().is2xxSuccessful()) { return Health.up() .withDetail("code", response.getStatusCode().value()) .build(); } return Health.down() .withDetail("code", response.getStatusCode().value()) .build(); } catch (Exception e) { return Health.down(e) .withDetail("reason", e.getMessage()) .build(); } } }在Spring Boot 3.x里HealthIndicator仍然是函数式接口,可以直接用lambda表达式:
@Component public class CustomApiHealthIndicator implements HealthIndicator { @Override public Health health() { // 一样的逻辑 } }这里想强调一个经验:Health.down()要慎用。如果一个不重要的外部依赖挂了,强行把整体健康状态置为DOWN,可能导致负载均衡器把这个实例摘掉,流量全部打到其他实例上,引发连锁反应。我的建议是按依赖的重要程度分级——核心依赖失败返回DOWN,非核心依赖失败可以返回UP但要带上自定义的status和detail,或者使用Health.status("DEGRADED")这种自定义状态码,让监控人员能看到"这实例其实是半残状态"。
2.3 端点的安全防护:别把管理端口当摆设
前面提到管理端口独立出来后,安全防护要做的事还远不止"换个端口"。实际生产经验里,至少有四件坑要提前埋好:
第一件:不要裸奔在公网。管理端点上有很多敏感信息,比如/actuator/env能看配置,/actuator/threaddump能看线程栈,/actuator/mappings能看所有URL映射。这些信息对内部排查问题很有用,被公网看到就是信息泄露。管理端口至少要限制在内网IP段,最好再加一层防火墙规则,别只依赖网络边界。
第二件:如果和Spring Security共存,要单独配置权限。业务接口走一遍安全认证没问题,但监控采集系统(比如Prometheus)通常不方便带Cookie或Token去访问端点,所以需要给管理端点单独的放行规则。常见的做法是加一个SecurityFilterChain来专门处理/actuator/**路径,同时设置IP白名单Header(比如X-Forwarded-For校验),既让采集系统能访问,又避免公网随便调。
第三件:管理端口不要暴露到注册中心。如果服务注册到Nacos或Eureka,默认注册的是主端口。Spring Boot提供了management.server.add-application-context-header等配置项,但更稳妥的办法是让服务把自己的IP和主端口注册到注册中心,管理端口只在内部固定端口访问,不参与服务注册发现。否则外部服务可能通过注册中心拿到管理端口,绕过业务安全逻辑直接访问内部调试信息。
第四件:用shutdown端点时三思。很多人在本地调试喜欢打开shutdown端点图方便,生产环境千万别开。开了就意味着任何能访问管理端口的人都能优雅关停你的服务,等于是给攻击者递了一把刀。真需要远程重启服务的话,走操作系统的systemd或容器平台去做,而不是暴露Spring的shutdown端点。
3. 指标采集进阶:Micrometer配合Prometheus与Grafana
3.1 引入Micrometer注册表,一分钟暴露Prometheus格式指标
Actuator自带的/actuator/metrics端点能查看内存、线程等JVM指标,但它的本职工作是展示而不是存储,历史趋势、聚合分析这些就别指望它了。想要真正做监控,得用Prometheus来抓取指标,而Micrometer就是两者之间的桥。
在Spring Boot 2.x中加一个依赖:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency>如果是Spring Boot 3.x,对应的坐标是io.micrometer:micrometer-registry-prometheus:1.11.x(版本跟随Boot父工程管理,通常不用显式写版本号)。加完依赖后,上面配置的management.endpoints.web.exposure.include里加上prometheus,重启应用,访问/actuator/prometheus,你就能看到一堆Prometheus格式的指标文本。
这个端点输出的就是我们上面配置模板里留了prometheus的原因。有了这个端点,Prometheus服务器的抓取配置就很简单,只需在prometheus.yml里加一个job:
scrape_configs: - job_name: 'spring-boot-app' metrics_path: '/actuator/prometheus' static_configs: - targets: ['192.168.1.10:8081']这里注意一点:Prometheus抓取的是管理端口8081而不是业务端口8080,这样业务流量再大也不会影响指标的抓取。抓取间隔建议默认的15s就够用,不要设置太频繁,否则对高并发服务反而是额外的开销。
3.2 核心指标解读:JVM、线程、HTTP、数据库连接池
暴露了Prometheus指标之后,面对/actuator/prometheus那几百行文本,很多新手会懵。我给你挑几个必须盯住的"硬指标":
| 指标名 | 含义 | 告警参考阈值 |
|---|---|---|
jvm_memory_used_bytes | JVM堆和非堆内存使用量 | 堆使用率持续超过85%要警惕 |
jvm_gc_pause_seconds | GC暂停时间 | 平均值超过100ms需要优化 |
system_cpu_usage | 系统CPU使用率 | 长期超过80%要扩容 |
http_server_requests_seconds | HTTP请求耗时分布 | p99超过200ms要关注 |
tomcat_threads_busy | Tomcat忙线程数 | 接近最大值时说明线程池不够 |
hikaricp_connections_active | HikariCP活跃连接数 | 接近最大连接数时要看慢SQL |
process_uptime_seconds | 进程运行时长 | 异常重启会发现数值突然归零 |
以http_server_requests_seconds为例,这个指标是Micrometer对HTTP请求的自动埋点,默认会按请求URI和方法打标签。查询语句可以这样写:
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri))这条语句算的是5分钟内所有接口的p99耗时。注意一个坑:如果按URI分组,有些URI里带了动态参数(比如/order/123和/order/456),会被分别计数,导致指标基数膨胀。生产环境建议按URI模板做归一化,或者只统计特定几个核心接口,否则Prometheus的存储压力会很大。
tomcat_threads_busy这个指标也值得多说一句。通过tomcat.threads.busy也就是tomcat_threads_busy{name="http-nio-8080"},你可以清楚地看到当前Tomcat线程池的忙碌程度。如果这个值长时间接近tomcat_threads_max,但CPU并不高,很可能是有外部阻塞调用(比如慢HTTP调用、数据库连接等待)卡住了线程,这时候看dump会有大收获。
数据库连接池的指标通过HikariCP自动上报。hikaricp_connections_active接近hikaricp_connections_max时,配合数据库慢查询日志,基本就能定位到问题了。我见过不少项目把最大连接数设到200甚至500,其实很多场景下20~50就够了,连接池过大反而会增加数据库侧的开销。
3.3 Grafana看板:不用从零画,导入现成模板再改细节
Grafana看板从来不需要从零开始画。社区里有大量现成模板可以直接导入,我推荐两个最经典的方向:
- JVM监控看板:比如Grafana社区的4701号看板,基于Micrometer的JVM指标,覆盖堆内存、GC、线程、类加载等,导入后改一下数据源即可。
- Spring Boot综合看板:社区里搜"Spring Boot"有很多带HTTP请求统计和连接池指标的看板模板,比如
12900(适用于Micrometer的Spring Boot 2.x/3.x)。
导入模板之后别急着用,先做几件事:确认数据源指向Prometheus;把面板里的label名称改成实际环境里的(比如有些模板用的是application标签,而你的是job);调整时间范围和刷新间隔。我经常见到有人导入了一个漂亮的看板,结果因为label不匹配,所有查询都是"No Data",浪费了半小时排查配置问题。
自定义看板的话,有几个OpenMetrics标签要注意:application默认取spring.application.name,如果没设置,很多聚合查询会失效。建议每个服务在bootstrap.yml或application.yml里都设置:
spring: application: name: order-service这个application标签在Grafana上可以做多服务聚合对比。比如一张看板里同时看order-service、user-service、payment-service的JVM堆使用率,直接按application分组查询,几行配置就能搞定。
4. 监控不告警等于白监控:Prometheus告警规则与通知链
4.1 核心告警规则:先保证最要紧的几件事
Grafana面板再好看,人不可能24小时盯着屏幕。监控的最后一环是告警,而告警首先要解决的问题是"别乱叫"。我给生产环境配置告警时,遵循一个原则:只告警那些真正需要人工介入的事。
举个例子,Uptime指标如果长期正常,就不用为它配告警;但"实例挂了"这种绝对的大事,必须有。Prometheus里通过up指标来判断实例是否在抓取周期内响应,配合for子句可以过滤掉瞬时抖动:
groups: - name: spring-boot-alerts rules: - alert: InstanceDown expr: up == 0 for: 2m labels: severity: critical annotations: summary: "实例 {{ $labels.instance }} 已离线超过2分钟" - alert: HighJvmMemoryUsage expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.85 for: 10m labels: severity: warning annotations: summary: "JVM堆内存使用率超过85%,持续10分钟" - alert: HighErrorRate expr: sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (job) / sum(rate(http_server_requests_seconds_count[5m])) by (job) > 0.05 for: 5m labels: severity: warning annotations: summary: "5xx错误率超过5%,持续5分钟"这里有个经验想分享:报警阈值宁缺毋滥。一开始配置告警,很容易陷入"所有指标都配一条规则"的误区,结果就是告警风暴,值班同事一天收几十条告警邮件,最后全都忽略,真正出大事反而没人看。我的建议是先只配三个最核心的告警:实例挂了、堆内存超阈值、5xx错误率飙升。跑一阵子,确认告警通知通道稳定了,再逐步增加GC相关、线程池耗尽等更细粒度的规则。
4.2 告警通知链:从AlertManager到钉钉、企业微信或邮件
Prometheus本身只负责存储和规则评估,真正把告警推给人的是AlertManager。AlertManager的安装配置不展开细说,核心思路是定义一条路由,把所有severity=critical的告警发到紧急通道,非紧急的发到常规群。这里给一个最小可用的AlertManager配置:
route: group_by: ['alertname'] group_wait: 10s group_interval: 5m repeat_interval: 4h receiver: 'default-receiver' receivers: - name: 'default-receiver' webhook_configs: - url: 'http://localhost:5000/send' send_resolved: truewebhook_configs是AlertManager最灵活的出口,你可以自己写一个极简Webhook服务接收告警JSON,再转成你们内部IM的消息格式。钉钉、企业微信的机器人文档里都有"自定义Webhook"的接入方式,照着拼一个JSON POST上去就行。
个人建议先把send_resolved设为true,意思是恢复时也发通知,这样值班的人能知道"刚才的告警已经恢复了",而不是提心吊胆地等一波晋级的告警。告警恢复消息很容易淹没在大量告警里,所以强烈建议把恢复和触发分开,按严重程度走不同通道。
4.3 阈值怎么定才不"狼来了":聊聊告警调优的经验
"狼来了"是告警系统最大的敌人。如果告警阈值设置太敏感,一天响几十次但每次都不是真问题,值班的人就会麻掉。如果设置太迟钝,真出事了又没反应。
调优告警阈值时,我会按月为单位复盘:看一周内的告警记录,统计哪些告警最终确认是有效告警,哪些是误报。比如HighJvmMemoryUsage持续告警,查下来是开发环境测试代码在不停地分配大对象,属于环境问题,那这条规则就不该在开发环境启用。再比如HighErrorRate因为某个爬虫在疯狂打接口导致5xx,那就应该考虑把爬虫IP加黑名单,而不是调高错误率阈值。
还有个小技巧:给告警规则打上环境标签,比如environment="生产",然后只让生产环境的规则发往告警接收人,测试和预发环境的告警全部静默或只发到日志。不然开发环境半夜一个OOM告警,照样把运维和开发都喊醒,那就得不偿失了。
5. 常见问题与排查技巧实录
5.1 Actuator端点401或404的排查思路
这是一个高频问题。加了Actuator依赖后访问/actuator/health,结果404或401。排查路径其实是固定的:
- 404:先确认
management.endpoints.web.exposure.include里有没有包含health。Spring Boot 2.x以后默认只暴露health和info,但如果你自定义配置了include,就一定要把health也带上。 - 404还能出现在另一个场景:项目里自定义了
server.servlet.context-path(比如/api),Actuator端点的路径默认是/api/actuator/health,这个要注意。想改成独立路径的话,配置management.endpoints.web.base-path=/health即可。 - 401:说明有Spring Security在拦。解决方案是给
/actuator/**单独放行,但如果你的管理端口只对内网可见,可以直接对/actuator/**关闭认证;否则就用IP白名单保证安全的前提下放行。 - 还有一种容易被忽略的404:Spring Boot 3.x中如果应用配置了
server.forward-headers-strategy,部分反向代理场景下/actuator/health转发路径可能被改写,排查时建议先直接访问应用本身的IP:端口,绕过代理确认端点本身是否可用。
5.2 Prometheus指标太多怎么办:高基数问题的实战处理
Micrometer的标签机制很方便,但用不好就是灾难。最常见的坑是给没上限的值打标签,比如把userId、orderId这种唯一标识作为Tag加到指标上。每个请求都会产生新的标签组合,指标基数无限膨胀,Prometheus的内存和磁盘迟早爆掉。
实际处理中我会把握两个原则:
- 只对有限集合的值打标签,比如
method、status、uri(尽量按URI模板)。 - 高基数数据如果要分析,用日志系统或者链路追踪来解决,而不是硬塞给Prometheus。
如果已经发现Prometheus的tsdb_head_samples_appended_total在不停涨,先看哪些指标的标签基数最大。promtool可以分析TSDB统计,Grafana或者Prometheus的/api/v1/status/tsdb接口能列出Top10高基数标签,定位后改代码重建指标。
5.3 服务假死与探针配置:我把这些坑替你踩过了
回到开头说的假死问题。假如你的服务部署在Kubernetes里,容器探针配置是这么干的:
livenessProbe: httpGet: path: /actuator/health/liveness port: 8081 initialDelaySeconds: 60 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 8081 initialDelaySeconds: 30 periodSeconds: 5传统虚拟机上用负载均衡的话,把健康检查URL指向/actuator/health/readiness就行。但这里有个细微差别要重点提醒:如果自动配置的、且你主动自定义的HealthIndicator返回DOWN,readiness端点会整体返回503,负载均衡就能及时把流量摘走;反之如果你只用TCP端口探测,它永远不知道连接池已耗尽。
还有一个坑是initialDelaySeconds设置。如果你的服务启动很慢(比如依赖外部系统预热),initialDelaySeconds设置太短会导致启动期间反复被探活失败,容器被不断重启,进入"崩坏循环"。建议初值设大一些,按实际启动耗时再加30到60秒的余量。等稳定之后观察一段时间的启动耗时,再来精调这个参数。
5.4 一个经常被忽略的细节:时区与历史数据保留策略
Grafana面板上看指标趋势,经常发现时间轴对不上——"明明现在是下午三点,看板显示凌晨三点"。这不是指标采集错了,而是时区问题。Grafana的默认时区是UTC,需要在看板设置里把Timezone改成Local(或者指定Asia/Shanghai)。Prometheus存储的数据都是UTC时间戳,展示层负责转换成当地时区,千万锁定看板设置,否则不同浏览器进来看的时间可能不一致。
数据保留策略也是一个容易被忽略的日常维护项。Prometheus默认保留retention配置的时间序列数据,如果设置成永久,磁盘会越吃越满。一般生产中建议设置15天左右:
storage: tsdb: retention: time: 15d对这个配置我要多说一句:Prometheus里retention是可以按time和size同时限制的,生产环境建议两个都配。如果只配time不配size,大量高基数指标可能提前把磁盘写满,特别是采集了带高基数标签的指标时。Prometheus官方文档里给出的建议是同时评估这两个维度的上限,避免单方面约束导致烦人的存储问题。
写在最后的一点实际体会
这套健康检查和监控体系,我前后在多个项目里搭过,踩过的坑比写出来的多。最大的体会是:先从最小闭环开始,不要一开始就追求全明星配置。加一个Actuator依赖,把/actuator/health用起来;再挂一个Prometheus,抓JVM和HTTP两个维度的指标;最后接一个Grafana看板,配两到三条核心告警。这个流程一天就能跑通,但带来的价值远超预期。
等这套最小闭环稳定运行两个星期,你会对服务的真实运行状态有非常直观的感受:哪些接口的耗时在悄悄变长,哪个时间段GC特别频繁,线程池的忙碌水位大概在什么水平。有了这些数据做底子,再去调整告警阈值、增加自定义健康检查项、优化连接池参数,就有了依据,而不是凭感觉瞎调。
最后再分享一个小技巧:平时多练习读PromQL。监控体系搭好的头几天,我建议每天写一两个PromQL查询练手,比如"按URI统计5分钟内平均耗时""对比今天和昨天同一时间段的CPU使用率"。这些查询练熟了,遇到线上问题的时候,你能比别人快很多定位到瓶颈。监控的价值不在于面板有多好看,而在于出问题的时候,你能不能从数据里看出门道。